diff --git a/changelog/unreleased/app-registry-order.md b/changelog/unreleased/app-registry-order.md new file mode 100644 index 0000000000..7e306f6578 --- /dev/null +++ b/changelog/unreleased/app-registry-order.md @@ -0,0 +1,12 @@ +Enhancement: Default AppProvider on top of the providers list +for each mime type + +Now for each mime type, when asking for the list of mime types, +the default AppProvider, set both using the config and the +SetDefaultProviderForMimeType method, is always in the top of the +list of AppProviders. +The config for the Providers and Mime Types for the AppRegistry changed, +using a list instead of a map. In fact the list of mime types returned by +ListSupportedMimeTypes is now ordered according the config. + +https://github.com/cs3org/reva/pull/2138 \ No newline at end of file diff --git a/go.mod b/go.mod index d51883b173..ad2913cdb9 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/thanhpk/randstr v1.0.4 github.com/tidwall/pretty v1.2.0 // indirect github.com/tus/tusd v1.6.0 + github.com/wk8/go-ordered-map v0.2.0 go.mongodb.org/mongo-driver v1.7.2 // indirect go.opencensus.io v0.23.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.24.0 diff --git a/go.sum b/go.sum index 080f212784..19296c1e1a 100644 --- a/go.sum +++ b/go.sum @@ -465,6 +465,8 @@ github.com/tus/tusd v1.6.0/go.mod h1:ygrT4B9ZSb27dx3uTnobX5nOFDnutBL6iWKLH4+KpA0 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/vimeo/go-util v1.2.0/go.mod h1:s13SMDTSO7AjH1nbgp707mfN5JFIWUFDU5MDDuRRtKs= +github.com/wk8/go-ordered-map v0.2.0 h1:KlvGyHstD1kkGZkPtHCyCfRYS0cz84uk6rrW/Dnhdtk= +github.com/wk8/go-ordered-map v0.2.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= diff --git a/internal/grpc/services/appregistry/appregistry_test.go b/internal/grpc/services/appregistry/appregistry_test.go index e5d2a87b99..4230a1ffe8 100644 --- a/internal/grpc/services/appregistry/appregistry_test.go +++ b/internal/grpc/services/appregistry/appregistry_test.go @@ -39,30 +39,32 @@ func (a ByAddress) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func Test_ListAppProviders(t *testing.T) { tests := []struct { name string - providers map[string]interface{} - mimeTypes map[string]map[string]string + providers []map[string]interface{} + mimeTypes []map[string]interface{} want *registrypb.ListAppProvidersResponse }{ { name: "simple test", - providers: map[string]interface{}{ - "some Address": map[string]interface{}{ + providers: []map[string]interface{}{ + { "address": "some Address", "mimetypes": []string{"text/json"}, }, - "another address": map[string]interface{}{ + { "address": "another address", "mimetypes": []string{"currently/ignored"}, }, }, - mimeTypes: map[string]map[string]string{ - "text/json": { + mimeTypes: []map[string]interface{}{ + { + "mime_type": "text/json", "extension": "json", "name": "JSON File", "icon": "https://example.org/icons&file=json.png", "default_app": "some Address", }, - "currently/ignored": { + { + "mime_type": "currently/ignored", "extension": "unknown", "name": "Ignored file", "icon": "https://example.org/icons&file=unknown.png", @@ -103,8 +105,8 @@ func Test_ListAppProviders(t *testing.T) { }, { name: "empty providers", - providers: map[string]interface{}{}, - mimeTypes: map[string]map[string]string{}, + providers: []map[string]interface{}{}, + mimeTypes: []map[string]interface{}{}, // only Status and Providers will be asserted in the tests want: ®istrypb.ListAppProvidersResponse{ @@ -116,23 +118,6 @@ func Test_ListAppProviders(t *testing.T) { Providers: []*registrypb.ProviderInfo{}, }, }, - { - name: "provider value is nil", - providers: map[string]interface{}{ - "some Address": nil, - }, - mimeTypes: map[string]map[string]string{}, - - // only Status and Providers will be asserted in the tests - want: ®istrypb.ListAppProvidersResponse{ - Status: &rpcv1beta1.Status{ - Code: 1, - Trace: "00000000000000000000000000000000", - Message: "", - }, - Providers: []*registrypb.ProviderInfo{nil}, - }, - }, } for _, tt := range tests { @@ -161,53 +146,59 @@ func Test_ListAppProviders(t *testing.T) { } func Test_GetAppProviders(t *testing.T) { - providers := map[string]interface{}{ - "text appprovider addr": map[string]interface{}{ + providers := []map[string]interface{}{ + { "address": "text appprovider addr", "mimetypes": []string{"text/json", "text/xml"}, }, - "image appprovider addr": map[string]interface{}{ + { "address": "image appprovider addr", "mimetypes": []string{"image/bmp"}, }, - "misc appprovider addr": map[string]interface{}{ + { "address": "misc appprovider addr", "mimetypes": []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.presentation", "application/vnd.apple.installer+xml"}, }, } - mimeTypes := map[string]map[string]string{ - "text/json": { + mimeTypes := []map[string]string{ + { + "mime_type": "text/json", "extension": "json", "name": "JSON File", "icon": "https://example.org/icons&file=json.png", "default_app": "some Address", }, - "text/xml": { + { + "mime_type": "text/xml", "extension": "xml", "name": "XML File", "icon": "https://example.org/icons&file=xml.png", "default_app": "some Address", }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { + { + "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "extension": "doc", "name": "Word File", "icon": "https://example.org/icons&file=doc.png", "default_app": "some Address", }, - "application/vnd.oasis.opendocument.presentation": { + { + "mime_type": "application/vnd.oasis.opendocument.presentation", "extension": "odf", "name": "OpenDocument File", "icon": "https://example.org/icons&file=odf.png", "default_app": "some Address", }, - "application/vnd.apple.installer+xml": { + { + "mime_type": "application/vnd.apple.installer+xml", "extension": "mpkg", "name": "Mpkg File", "icon": "https://example.org/icons&file=mpkg.png", "default_app": "some Address", }, - "image/bmp": { + { + "mime_type": "image/bmp", "extension": "bmp", "name": "Image File", "icon": "https://example.org/icons&file=bmp.png", diff --git a/pkg/app/registry/static/static.go b/pkg/app/registry/static/static.go index 1e9d34fbd9..8f800e17a1 100644 --- a/pkg/app/registry/static/static.go +++ b/pkg/app/registry/static/static.go @@ -20,6 +20,8 @@ package static import ( "context" + "fmt" + "reflect" "strings" "sync" @@ -27,7 +29,10 @@ import ( "github.com/cs3org/reva/pkg/app" "github.com/cs3org/reva/pkg/app/registry/registry" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/mime" "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog/log" + orderedmap "github.com/wk8/go-ordered-map" ) func init() { @@ -35,37 +40,29 @@ func init() { } type mimeTypeConfig struct { + MimeType string `mapstructure:"mime_type"` Extension string `mapstructure:"extension"` Name string `mapstructure:"name"` Description string `mapstructure:"description"` Icon string `mapstructure:"icon"` DefaultApp string `mapstructure:"default_app"` AllowCreation bool `mapstructure:"allow_creation"` -} - -type mimeTypeIndex struct { - mimeConf mimeTypeConfig - apps []string + // apps keeps the Providers able to open this mime type. + // the list will always keep the default AppProvider at the head + apps []*registrypb.ProviderInfo } type config struct { - Providers map[string]*registrypb.ProviderInfo `mapstructure:"providers"` - MimeTypes map[string]mimeTypeConfig `mapstructure:"mime_types"` + Providers []*registrypb.ProviderInfo `mapstructure:"providers"` + MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"` } func (c *config) init() { if len(c.Providers) == 0 { - c.Providers = map[string]*registrypb.ProviderInfo{} + c.Providers = []*registrypb.ProviderInfo{} } } -type manager struct { - config *config - providers map[string]*registrypb.ProviderInfo - mimetypesIdx map[string]*mimeTypeIndex // map the mime type to the addresses of the corresponding providers - sync.RWMutex -} - func parseConfig(m map[string]interface{}) (*config, error) { c := &config{} if err := mapstructure.Decode(m, c); err != nil { @@ -74,55 +71,73 @@ func parseConfig(m map[string]interface{}) (*config, error) { return c, nil } +type manager struct { + providers map[string]*registrypb.ProviderInfo + mimetypes *orderedmap.OrderedMap // map[string]*mimeTypeConfig -> map the mime type to the addresses of the corresponding providers + sync.RWMutex +} + // New returns an implementation of the app.Registry interface. func New(m map[string]interface{}) (app.Registry, error) { - conf, err := parseConfig(m) + c, err := parseConfig(m) if err != nil { return nil, err } - conf.init() + c.init() + + mimetypes := orderedmap.New() - mimetypes := make(map[string]*mimeTypeIndex) + for _, mime := range c.MimeTypes { + mimetypes.Set(mime.MimeType, mime) + } + + providerMap := make(map[string]*registrypb.ProviderInfo) + for _, p := range c.Providers { + providerMap[p.Address] = p + } - for addr, p := range conf.Providers { + // register providers configured manually from the config + // (different from the others that are registering themselves - + // dinamically added invoking the AddProvider function) + for _, p := range c.Providers { if p != nil { for _, m := range p.MimeTypes { - _, ok := mimetypes[m] - if ok { - mimetypes[m].apps = append(mimetypes[m].apps, addr) + if v, ok := mimetypes.Get(m); ok { + mtc := v.(*mimeTypeConfig) + registerProvider(p, mtc) } else { - mimetypes[m] = &mimeTypeIndex{apps: []string{addr}} - if mimeConf, ok := conf.MimeTypes[m]; ok { - mimetypes[m].mimeConf = mimeConf - } - } - // set this as default app for mime types if configured via name - if mimeConf, ok := conf.MimeTypes[m]; ok && mimeConf.DefaultApp == p.Name { - mimetypes[m].mimeConf.DefaultApp = addr + return nil, errtypes.NotFound(fmt.Sprintf("mimetype %s not found in the configuration", m)) } } } } - return &manager{ - config: conf, - providers: conf.Providers, - mimetypesIdx: mimetypes, - }, nil + newManager := manager{ + providers: providerMap, + mimetypes: mimetypes, + } + return &newManager, nil } -func (regManager *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) { - regManager.RLock() - defer regManager.RUnlock() +func registerProvider(p *registrypb.ProviderInfo, mime *mimeTypeConfig) { + if providerIsDefaultForMimeType(p, mime) { + mime.apps = prependProvider(p, mime.apps) + } else { + mime.apps = append(mime.apps, p) + } +} +func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) { // find longest match var match string - var apps []string - for prefix, idx := range regManager.mimetypesIdx { + m.RLock() + defer m.RUnlock() + + for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() { + prefix := pair.Key.(string) if strings.HasPrefix(mimeType, prefix) && len(prefix) > len(match) { match = prefix - apps = idx.apps } } @@ -130,112 +145,169 @@ func (regManager *manager) FindProviders(ctx context.Context, mimeType string) ( return nil, errtypes.NotFound("application provider not found for mime type " + mimeType) } - providers := make([]*registrypb.ProviderInfo, 0, len(apps)) - for _, p := range apps { - providers = append(providers, regManager.providers[p]) + mimeInterface, _ := m.mimetypes.Get(match) + mimeMatch := mimeInterface.(*mimeTypeConfig) + var providers = make([]*registrypb.ProviderInfo, 0, len(mimeMatch.apps)) + for _, p := range mimeMatch.apps { + providers = append(providers, m.providers[p.Address]) } - return providers, nil } -func (regManager *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error { - regManager.Lock() - defer regManager.Unlock() +func providerIsDefaultForMimeType(p *registrypb.ProviderInfo, mime *mimeTypeConfig) bool { + return p.Address == mime.DefaultApp || p.Name == mime.DefaultApp +} - regManager.providers[p.Address] = p +func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error { + m.Lock() + defer m.Unlock() - for _, m := range p.MimeTypes { - if idx, ok := regManager.mimetypesIdx[m]; ok { - idx.apps = append(idx.apps, p.Address) - } else { - regManager.mimetypesIdx[m] = &mimeTypeIndex{apps: []string{p.Address}} - if mimetypeConfig, ok := regManager.config.MimeTypes[m]; ok { - regManager.mimetypesIdx[m].mimeConf = mimetypeConfig - } - } + m.providers[p.Address] = p - // set this as default app for mime types if configured via name - if mimetypeConfig, ok := regManager.config.MimeTypes[m]; ok && mimetypeConfig.DefaultApp == p.Name { - regManager.mimetypesIdx[m].mimeConf.DefaultApp = p.Address + // log := appctx.GetLogger(ctx) + + for _, mime := range p.MimeTypes { + if mimeTypeInterface, ok := m.mimetypes.Get(mime); ok { + // TODO (gdelmont): don't add to the list of apps an AppProvider + // that was already registered + mimeType := mimeTypeInterface.(*mimeTypeConfig) + registerProvider(p, mimeType) + } else { + // the mime type should be already registered as config in the AppRegistry + // we will create a new entry fot the mimetype, but leaving a warning for + // future log inspection for weird behaviour + // log.Warn().Msgf("config for mimetype '%s' not found while adding a new AppProvider", m) + m.mimetypes.Set(mime, dummyMimeType(mime, []*registrypb.ProviderInfo{p})) } } - return nil } -func (regManager *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) { - regManager.RLock() - defer regManager.RUnlock() +func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) { + m.RLock() + defer m.RUnlock() - providers := make([]*registrypb.ProviderInfo, 0, len(regManager.providers)) - for _, p := range regManager.providers { + providers := make([]*registrypb.ProviderInfo, 0, len(m.providers)) + for _, p := range m.providers { providers = append(providers, p) } return providers, nil } -func (regManager *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) { - regManager.RLock() - defer regManager.RUnlock() +func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) { + m.RLock() + defer m.RUnlock() - res := []*registrypb.MimeTypeInfo{} + res := make([]*registrypb.MimeTypeInfo, 0, m.mimetypes.Len()) + + for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() { + + mime := pair.Value.(*mimeTypeConfig) + + res = append(res, ®istrypb.MimeTypeInfo{ + MimeType: mime.MimeType, + Ext: mime.Extension, + Name: mime.Name, + Description: mime.Description, + Icon: mime.Icon, + AppProviders: mime.apps, + AllowCreation: mime.AllowCreation, + }) - for m, mime := range regManager.mimetypesIdx { - info := ®istrypb.MimeTypeInfo{ - MimeType: m, - Ext: mime.mimeConf.Extension, - Name: mime.mimeConf.Name, - Description: mime.mimeConf.Description, - Icon: mime.mimeConf.Icon, - AllowCreation: mime.mimeConf.AllowCreation, - } - for _, p := range mime.apps { - if provider, ok := regManager.providers[p]; ok { - t := *provider - t.MimeTypes = nil - info.AppProviders = append(info.AppProviders, &t) - } - } - res = append(res, info) } return res, nil } -func (regManager *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error { - regManager.Lock() - defer regManager.Unlock() +// prepend an AppProvider obj to the list +func prependProvider(n *registrypb.ProviderInfo, lst []*registrypb.ProviderInfo) []*registrypb.ProviderInfo { + lst = append(lst, ®istrypb.ProviderInfo{}) + copy(lst[1:], lst) + lst[0] = n + return lst +} - idx, ok := regManager.mimetypesIdx[mimeType] - if ok { - idx.mimeConf.DefaultApp = p.Address - - // Add to list of apps if not present - var present bool - for _, pr := range idx.apps { - if pr == p.Address { - present = true - break - } +func getIndex(lst []*registrypb.ProviderInfo, s *registrypb.ProviderInfo) (int, bool) { + for i, e := range lst { + if equalsProviderInfo(e, s) { + return i, true } - if !present { - idx.apps = append(idx.apps, p.Address) + } + return -1, false +} + +func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error { + m.Lock() + defer m.Unlock() + + mimeInterface, ok := m.mimetypes.Get(mimeType) + if ok { + mime := mimeInterface.(*mimeTypeConfig) + mime.DefaultApp = p.Address + + if index, in := getIndex(mime.apps, p); in { + // the element is in the list, we will remove it + // TODO (gdelmont): not the best way to remove an element from a slice + // but maybe we want to keep the order? + mime.apps = append(mime.apps[:index], mime.apps[index+1:]...) } + // prepend it to the front of the list + mime.apps = prependProvider(p, mime.apps) + + } else { + // the mime type should be already registered as config in the AppRegistry + // we will create a new entry fot the mimetype, but leaving a warning for + // future log inspection for weird behaviour + log.Warn().Msgf("config for mimetype '%s' not found while setting a new default AppProvider", mimeType) + m.mimetypes.Set(mimeType, dummyMimeType(mimeType, []*registrypb.ProviderInfo{p})) } + return nil +} + +func dummyMimeType(m string, apps []*registrypb.ProviderInfo) *mimeTypeConfig { + ext := mime.Detect(false, m) - return errtypes.NotFound("mime type not found " + mimeType) + return &mimeTypeConfig{ + MimeType: m, + apps: apps, + Extension: ext, + Name: fmt.Sprintf("%s file", strings.ToUpper(ext)), + Description: fmt.Sprintf("%s file", strings.ToUpper(ext)), + } } -func (regManager *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) { - regManager.RLock() - defer regManager.RUnlock() +func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) { + m.RLock() + defer m.RUnlock() - m, ok := regManager.mimetypesIdx[mimeType] + mime, ok := m.mimetypes.Get(mimeType) if ok { - if p, ok := regManager.providers[m.mimeConf.DefaultApp]; ok { + if p, ok := m.providers[mime.(*mimeTypeConfig).DefaultApp]; ok { return p, nil } } return nil, errtypes.NotFound("default application provider not set for mime type " + mimeType) } + +func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool { + return p1.Address == p2.Address && + p1.Name == p2.Name && + reflect.DeepEqual(p1.MimeTypes, p2.MimeTypes) && + p1.Description == p2.Description && + p1.DesktopOnly == p2.DesktopOnly +} + +// check that all providers in the two lists are equals +func providersEquals(l1, l2 []*registrypb.ProviderInfo) bool { + if len(l1) != len(l2) { + return false + } + + for i := 0; i < len(l1); i++ { + if !equalsProviderInfo(l1[i], l2[i]) { + return false + } + } + return true +} diff --git a/pkg/app/registry/static/static_test.go b/pkg/app/registry/static/static_test.go index 6288dc1d29..dcab5fc66a 100644 --- a/pkg/app/registry/static/static_test.go +++ b/pkg/app/registry/static/static_test.go @@ -20,165 +20,730 @@ package static import ( "context" + "reflect" "testing" registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" - "github.com/stretchr/testify/assert" + "github.com/cs3org/reva/pkg/errtypes" ) -var ( - ctx = context.Background() - - microsoftProvider = ®istrypb.ProviderInfo{ - Address: "localhost:19000", - Name: "Microsoft Office", - Description: "MS office 365", - Icon: "https://msp2l1160225102310.blob.core.windows.net/ms-p2-l1-160225-1023-13-assets/office_365_icon_en-US.png", - MimeTypes: []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.presentation", "application/pdf"}, - } +func TestFindProviders(t *testing.T) { - collaboraProvider = ®istrypb.ProviderInfo{ - Address: "localhost:18000", - Name: "Collabora", - Description: "Collabora office editing apps", - Icon: "https://www.collaboraoffice.com/wp-content/uploads/2019/01/CP-icon.png", - MimeTypes: []string{"application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.presentation", "text/markdown"}, + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + regProviders []*registrypb.ProviderInfo + mimeType string + expectedRes []*registrypb.ProviderInfo + expectedErr error + }{ + { + name: "no mime types registered", + mimeTypes: []*mimeTypeConfig{}, + mimeType: "SOMETHING", + expectedErr: errtypes.NotFound("application provider not found for mime type SOMETHING"), + }, + { + name: "one provider registered for one mime type", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "some Address", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "some Address", + Name: "some Name", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "some Address", + Name: "some Name", + }, + }, + }, + { + name: "more providers registered for one mime type", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider3", + Name: "provider3", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider3", + Name: "provider3", + }, + }, + }, + { + name: "more providers registered for different mime types", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "provider1", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "ip-provider1", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "ip-provider3", + Name: "provider3", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "ip-provider1", + Name: "provider1", + }, + }, + }, } - codimdProvider = ®istrypb.ProviderInfo{ - Address: "localhost:17000", - Name: "CodiMD", - Description: "App for markdown files", - Icon: "https://avatars.githubusercontent.com/u/48181221?s=200&v=4", - MimeTypes: []string{"text/markdown", "application/compressed-markdown"}, + for _, tt := range testCases { + + t.Run(tt.name, func(t *testing.T) { + + ctx := context.TODO() + + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + "providers": tt.regProviders, + }) + if err != nil { + t.Error("unexpected error creating the registry:", err) + } + + providers, err := registry.FindProviders(ctx, tt.mimeType) + + // check that the error returned by FindProviders is the same as the expected + if tt.expectedErr != err { + t.Errorf("different error returned: got=%v expected=%v", err, tt.expectedErr) + } + + if !providersEquals(providers, tt.expectedRes) { + t.Errorf("providers list different from expected: \n\tgot=%v\n\texp=%v", providers, tt.expectedRes) + } + + }) + } - mimeTypesForCreation = []string{"application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"} - - testConfig = map[string]interface{}{ - "mime_types": map[string]interface{}{ - "application/pdf": map[string]interface{}{ - "extension": "pdf", - "name": "PDF", - "description": "PDF document", - "icon": "", - }, - "application/vnd.oasis.opendocument.text": map[string]interface{}{ - "extension": "odt", - "name": "Open Document", - "description": "OpenDocument text document", - "icon": "", - "default_app": "Collabora", - "allow_creation": true, - }, - "application/vnd.oasis.opendocument.spreadsheet": map[string]interface{}{ - "extension": "ods", - "name": "Open Spreadsheet", - "description": "OpenDocument spreadsheet document", - "icon": "", - "default_app": "Collabora", - "allow_creation": true, - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": map[string]interface{}{ - "extension": "docx", - "name": "Word Document", - "description": "Microsoft Word document", - "icon": "", - "default_app": "Microsoft Office", - "allow_creation": true, - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": map[string]interface{}{ - "extension": "xlsx", - "name": "Excel Spreadsheet", - "description": "Microsoft Excel document", - "icon": "", - "default_app": "Microsoft Office", - "allow_creation": true, +} + +func TestAddProvider(t *testing.T) { + + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + initProviders []*registrypb.ProviderInfo + newProvider *registrypb.ProviderInfo + expectedProviders map[string][]*registrypb.ProviderInfo + }{ + { + name: "no mime types defined - no initial providers", + mimeTypes: []*mimeTypeConfig{}, + initProviders: []*registrypb.ProviderInfo{}, + newProvider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + expectedProviders: map[string][]*registrypb.ProviderInfo{ + "text/json": { + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + }, + }, + }, + { + name: "one mime types defined - no initial providers - registering provider is the default", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider1", + }, + }, + initProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + }, + newProvider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + expectedProviders: map[string][]*registrypb.ProviderInfo{ + "text/json": { + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + }, + }, + }, + { + name: "one mime types defined - no initial providers - default already registered", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + initProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + }, + newProvider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + expectedProviders: map[string][]*registrypb.ProviderInfo{ + "text/json": { + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + }, }, }, } -) -func mimeTypeAllowedForCreation(mimeType string) bool { - for _, m := range mimeTypesForCreation { - if m == mimeType { - return true - } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + + ctx := context.TODO() + + registry, err := New(map[string]interface{}{ + "providers": tt.initProviders, + "mime_types": tt.mimeTypes, + }) + if err != nil { + t.Error("unexpected error creating the registry:", err) + } + + err = registry.AddProvider(ctx, tt.newProvider) + if err != nil { + t.Error("unexpected error adding a new provider:", err) + } + + // test that the internal set of providers keep the new provider + // and the key is the provider's address + staticReg := registry.(*manager) + + in, ok := staticReg.providers[tt.newProvider.Address] + if !ok { + t.Error("cannot find a provider in the internal map with address", tt.newProvider.Address) + } + + // check that the provider in the set is the same as the new one + if !equalsProviderInfo(tt.newProvider, in) { + t.Errorf("providers are different: got=%v expected=%v", in, tt.newProvider) + } + + for mime, expAddrs := range tt.expectedProviders { + mimeConfInterface, _ := staticReg.mimetypes.Get(mime) + addrsReg := mimeConfInterface.(*mimeTypeConfig).apps + + if !reflect.DeepEqual(expAddrs, addrsReg) { + t.Errorf("list of addresses different from expected: \n\tgot=%v\n\texp=%v", addrsReg, expAddrs) + } + } + + }) } - return false + } -func TestWithoutMimeTypesConfig(t *testing.T) { - manager, err := New(map[string]interface{}{}) - assert.Empty(t, err) +func TestListSupportedMimeTypes(t *testing.T) { + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + newProviders []*registrypb.ProviderInfo + expected []*registrypb.MimeTypeInfo + }{ + { + name: "one mime type - no provider registered", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + newProviders: []*registrypb.ProviderInfo{}, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{}, + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + }, + }, + { + name: "one mime type - only default provider registered", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider1", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "provider1", + }, + }, + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + }, + }, + { + name: "one mime type - more providers", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "NOT_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "JSON_DEFAULT_PROVIDER", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider1", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "ip-provider2", + Name: "NOT_DEFAULT_PROVIDER", + }, + }, + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + }, + }, + { + name: "multiple mime types", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "XML_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "1", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "2", + Name: "NOT_DEFAULT_PROVIDER1", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "3", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "4", + Name: "XML_DEFAULT_PROVIDER", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "3", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "1", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "4", + Name: "XML_DEFAULT_PROVIDER", + }, + }, + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + { + MimeType: "text/xml", + Ext: "xml", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "4", + Name: "XML_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "1", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "2", + Name: "NOT_DEFAULT_PROVIDER1", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "3", + Name: "JSON_DEFAULT_PROVIDER", + }, + }, + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + }, + }, + }, + } - err = manager.AddProvider(ctx, microsoftProvider) - assert.Empty(t, err) + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { - err = manager.AddProvider(ctx, collaboraProvider) - assert.Empty(t, err) + ctx := context.TODO() - mimeTypes, err := manager.ListSupportedMimeTypes(ctx) - assert.Empty(t, err) - assert.Equal(t, len(mimeTypes), 8) + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + }) + if err != nil { + t.Error("unexpected error creating the registry:", err) + } - err = manager.AddProvider(ctx, codimdProvider) - assert.Empty(t, err) + // add all the providers + for _, p := range tt.newProviders { + err = registry.AddProvider(ctx, p) + if err != nil { + t.Error("unexpected error creating adding new providers:", err) + } + } - providers, err := manager.FindProviders(ctx, "text/markdown") - assert.Empty(t, err) - assert.ElementsMatch(t, []*registrypb.ProviderInfo{collaboraProvider, codimdProvider}, providers) + got, err := registry.ListSupportedMimeTypes(ctx) + if err != nil { + t.Error("unexpected error listing supported mime types:", err) + } - mimeTypes, err = manager.ListSupportedMimeTypes(ctx) - assert.Empty(t, err) - assert.Equal(t, len(mimeTypes), 9) + if !mimeTypesEquals(got, tt.expected) { + t.Errorf("mime types list different from expected: \n\tgot=%v\n\texp=%v", got, tt.expected) + } - // default app is not set - _, err = manager.GetDefaultProviderForMimeType(ctx, "application/vnd.oasis.opendocument.text") - assert.Equal(t, err.Error(), "error: not found: default application provider not set for mime type application/vnd.oasis.opendocument.text") + }) + } } -func TestWithConfiguredMimeTypes(t *testing.T) { - manager, err := New(testConfig) - assert.Empty(t, err) +func TestSetDefaultProviderForMimeType(t *testing.T) { + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + initProviders []*registrypb.ProviderInfo + newDefault struct { + mimeType string + provider *registrypb.ProviderInfo + } + newProviders []*registrypb.ProviderInfo + }{ + { + name: "set new default - no new providers", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + initProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "1", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "2", + Name: "NEW_DEFAULT", + }, + }, + newDefault: struct { + mimeType string + provider *registrypb.ProviderInfo + }{ + mimeType: "text/json", + provider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "2", + Name: "NEW_DEFAULT", + }, + }, + newProviders: []*registrypb.ProviderInfo{}, + }, + { + name: "set default - other providers (one is the previous default)", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + initProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "4", + Name: "NO_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "2", + Name: "NEW_DEFAULT", + }, + }, + newDefault: struct { + mimeType string + provider *registrypb.ProviderInfo + }{ + mimeType: "text/json", + provider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "2", + Name: "NEW_DEFAULT", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "1", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "3", + Name: "OTHER_PROVIDER", + }, + }, + }, + } - err = manager.AddProvider(ctx, microsoftProvider) - assert.Empty(t, err) + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { - err = manager.AddProvider(ctx, collaboraProvider) - assert.Empty(t, err) + ctx := context.TODO() - mimeTypes, err := manager.ListSupportedMimeTypes(ctx) - assert.Empty(t, err) - assert.Equal(t, len(mimeTypes), 8) - for _, m := range mimeTypes { - assert.Equal(t, m.AllowCreation, mimeTypeAllowedForCreation(m.MimeType)) - } + registry, err := New(map[string]interface{}{ + "providers": tt.initProviders, + "mime_types": tt.mimeTypes, + }) + if err != nil { + t.Error("unexpected error creating a new registry:", err) + } + + err = registry.SetDefaultProviderForMimeType(ctx, tt.newDefault.mimeType, tt.newDefault.provider) + if err != nil { + t.Error("unexpected error setting a default provider for mime type:", err) + } + + // add other provider to move things around internally :) + for _, p := range tt.newProviders { + err = registry.AddProvider(ctx, p) + if err != nil { + t.Error("unexpected error adding a new provider:", err) + } + } + + // check if the new default is the one set + got, err := registry.GetDefaultProviderForMimeType(ctx, tt.newDefault.mimeType) + if err != nil { + t.Error("unexpected error getting the default app provider:", err) + } - err = manager.AddProvider(ctx, codimdProvider) - assert.Empty(t, err) + if !equalsProviderInfo(got, tt.newDefault.provider) { + t.Errorf("provider differ from expected:\n\tgot=%v\n\texp=%v", got, tt.newDefault.provider) + } - providers, err := manager.FindProviders(ctx, "application/vnd.oasis.opendocument.spreadsheet") - assert.Empty(t, err) - assert.ElementsMatch(t, []*registrypb.ProviderInfo{collaboraProvider, microsoftProvider}, providers) + }) + } +} - mimeTypes, err = manager.ListSupportedMimeTypes(ctx) - assert.Empty(t, err) - assert.Equal(t, len(mimeTypes), 9) - for _, m := range mimeTypes { - assert.Equal(t, m.AllowCreation, mimeTypeAllowedForCreation(m.MimeType)) +func mimeTypesEquals(l1, l2 []*registrypb.MimeTypeInfo) bool { + if len(l1) != len(l2) { + return false } - // default app is set - defaultAppSet, err := manager.GetDefaultProviderForMimeType(ctx, "application/vnd.oasis.opendocument.text") - assert.Empty(t, err) - assert.Equal(t, collaboraProvider, defaultAppSet) + for i := 0; i < len(l1); i++ { + if !equalsMimeTypeInfo(l1[i], l2[i]) { + return false + } + } + return true +} - // default app is not set - _, err = manager.GetDefaultProviderForMimeType(ctx, "application/compressed-markdown") - assert.Equal(t, err.Error(), "error: not found: default application provider not set for mime type application/compressed-markdown") +func equalsMimeTypeInfo(m1, m2 *registrypb.MimeTypeInfo) bool { + return m1.Description == m2.Description && + m1.AllowCreation == m2.AllowCreation && + providersEquals(m1.AppProviders, m2.AppProviders) && + m1.Ext == m2.Ext && + m1.MimeType == m2.MimeType && + m1.Name == m2.Name }