diff --git a/client/instances.go b/client/instances.go index 03373144a17..0a018b128af 100644 --- a/client/instances.go +++ b/client/instances.go @@ -35,6 +35,7 @@ type Instance struct { UUID string `json:"uuid,omitempty"` OIDCID string `json:"oidc_id,omitempty"` ContextName string `json:"context,omitempty"` + Sponsorships []string `json:"sponsorships,omitempty"` TOSSigned string `json:"tos,omitempty"` TOSLatest string `json:"tos_latest,omitempty"` AuthMode int `json:"auth_mode,omitempty"` @@ -65,6 +66,7 @@ type InstanceOptions struct { TOSLatest string Timezone string ContextName string + Sponsorships []string Email string PublicName string Settings string @@ -164,6 +166,9 @@ func (ac *AdminClient) CreateInstance(opts *InstanceOptions) (*Instance, error) if opts.DomainAliases != nil { q.Add("DomainAliases", strings.Join(opts.DomainAliases, ",")) } + if opts.Sponsorships != nil { + q.Add("Sponsorships", strings.Join(opts.Sponsorships, ",")) + } if opts.MagicLink != nil && *opts.MagicLink { q.Add("MagicLink", "true") } @@ -237,6 +242,9 @@ func (ac *AdminClient) ModifyInstance(opts *InstanceOptions) (*Instance, error) if opts.DomainAliases != nil { q.Add("DomainAliases", strings.Join(opts.DomainAliases, ",")) } + if opts.Sponsorships != nil { + q.Add("Sponsorships", strings.Join(opts.Sponsorships, ",")) + } if opts.MagicLink != nil { q.Add("MagicLink", strconv.FormatBool(*opts.MagicLink)) } diff --git a/cmd/instances.go b/cmd/instances.go index 4a515cf499d..0dd3af6ce63 100644 --- a/cmd/instances.go +++ b/cmd/instances.go @@ -52,6 +52,7 @@ var flagTOSSigned string var flagTOS string var flagTOSLatest string var flagContextName string +var flagSponsorships []string var flagOnboardingFinished bool var flagTTL time.Duration var flagExpire time.Duration @@ -188,6 +189,7 @@ be used as the error message. TOSSigned: flagTOSSigned, Timezone: flagTimezone, ContextName: flagContextName, + Sponsorships: flagSponsorships, Email: flagEmail, PublicName: flagPublicName, Settings: flagSettings, @@ -276,6 +278,7 @@ settings for a specified domain. TOSLatest: flagTOSLatest, Timezone: flagTimezone, ContextName: flagContextName, + Sponsorships: flagSponsorships, Email: flagEmail, PublicName: flagPublicName, Settings: flagSettings, @@ -1085,6 +1088,7 @@ func init() { addInstanceCmd.Flags().StringVar(&flagTOS, "tos", "", "The TOS version signed") addInstanceCmd.Flags().StringVar(&flagTimezone, "tz", "", "The timezone for the user") addInstanceCmd.Flags().StringVar(&flagContextName, "context-name", "", "Context name of the instance") + addInstanceCmd.Flags().StringSliceVar(&flagSponsorships, "sponsorships", nil, "Sponsorships of the instance (comma separated list)") addInstanceCmd.Flags().StringVar(&flagEmail, "email", "", "The email of the owner") addInstanceCmd.Flags().StringVar(&flagPublicName, "public-name", "", "The public name of the owner") addInstanceCmd.Flags().StringVar(&flagSettings, "settings", "", "A list of settings (eg context:foo,offer:premium)") @@ -1105,6 +1109,7 @@ func init() { modifyInstanceCmd.Flags().StringVar(&flagTOSLatest, "tos-latest", "", "Update the latest TOS version") modifyInstanceCmd.Flags().StringVar(&flagTimezone, "tz", "", "New timezone") modifyInstanceCmd.Flags().StringVar(&flagContextName, "context-name", "", "New context name") + modifyInstanceCmd.Flags().StringSliceVar(&flagSponsorships, "sponsorships", nil, "Sponsorships of the instance (comma separated list)") modifyInstanceCmd.Flags().StringVar(&flagEmail, "email", "", "New email") modifyInstanceCmd.Flags().StringVar(&flagPublicName, "public-name", "", "New public name") modifyInstanceCmd.Flags().StringVar(&flagSettings, "settings", "", "New list of settings (eg offer:premium)") diff --git a/cozy.example.yaml b/cozy.example.yaml index 25ba69a0f30..a5ca7f75cff 100644 --- a/cozy.example.yaml +++ b/cozy.example.yaml @@ -428,7 +428,7 @@ contexts: # Change the limit on the number of members for a sharing max_members_per_sharing: 50 # Use a different wizard for moving a Cozy - move_url: htts://move.cozy.beta/ + move_url: https://move.cozy.beta/ # Feature flags features: - hide_konnector_errors diff --git a/docs/cli/cozy-stack_instances_add.md b/docs/cli/cozy-stack_instances_add.md index 5372d165121..490d8acd669 100644 --- a/docs/cli/cozy-stack_instances_add.md +++ b/docs/cli/cozy-stack_instances_add.md @@ -41,6 +41,7 @@ $ cozy-stack instances add --passphrase cozy --apps drive,photos,settings,home,s --passphrase string Register the instance with this passphrase (useful for tests) --public-name string The public name of the owner --settings string A list of settings (eg context:foo,offer:premium) + --sponsorships strings Sponsorships of the instance (comma separated list) --swift-layout int Specify the layout to use for Swift (from 0 for layout V1 to 2 for layout V3, -1 means the default) (default -1) --tos string The TOS version signed --trace Show where time is spent diff --git a/docs/cli/cozy-stack_instances_modify.md b/docs/cli/cozy-stack_instances_modify.md index 3ef4f39f0d5..51e2e5b9b1e 100644 --- a/docs/cli/cozy-stack_instances_modify.md +++ b/docs/cli/cozy-stack_instances_modify.md @@ -31,6 +31,7 @@ cozy-stack instances modify [flags] --onboarding-finished Force the finishing of the onboarding --public-name string New public name --settings string New list of settings (eg offer:premium) + --sponsorships strings Sponsorships of the instance (comma separated list) --tos string Update the TOS version signed --tos-latest string Update the latest TOS version --tz string New timezone diff --git a/docs/settings.md b/docs/settings.md index 00ff79d21be..953000cce0e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -629,7 +629,8 @@ Cookie: sessionid=xxxx "public_name": "Alice Martin", "auth_mode": "basic", "default_redirection": "drive/#/folder", - "context": "dev" + "context": "dev", + "sponsorships": ["springfield"] } } } diff --git a/model/instance/instance.go b/model/instance/instance.go index 6bc9a1b396e..539eabc1cb3 100644 --- a/model/instance/instance.go +++ b/model/instance/instance.go @@ -1,3 +1,5 @@ +// Package instance is for the instance model, with domain, locale, settings, +// etc. package instance import ( @@ -52,6 +54,7 @@ type Instance struct { OIDCID string `json:"oidc_id,omitempty"` // An identifier to check authentication from OIDC FranceConnectID string `json:"franceconnect_id,omitempty"` // An identifier to check authentication from FranceConnect ContextName string `json:"context,omitempty"` // The context attached to the instance + Sponsorships []string `json:"sponsorships,omitempty"` // The list of sponsorships for the instance TOSSigned string `json:"tos,omitempty"` // Terms of Service signed version TOSLatest string `json:"tos_latest,omitempty"` // Terms of Service latest version AuthMode AuthMode `json:"auth_mode,omitempty"` // 2 factor authentication diff --git a/model/instance/instance_test.go b/model/instance/instance_test.go index 4a64abfd02f..af18fde7b5d 100644 --- a/model/instance/instance_test.go +++ b/model/instance/instance_test.go @@ -1,6 +1,7 @@ package instance_test import ( + "encoding/json" "testing" "github.com/cozy/cozy-stack/model/instance" @@ -8,6 +9,7 @@ import ( "github.com/cozy/cozy-stack/pkg/crypto" jwt "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestInstance(t *testing.T) { @@ -51,4 +53,129 @@ func TestInstance(t *testing.T) { assert.Equal(t, "test-ctx-token.example.com", claims["iss"]) assert.Equal(t, "my-app", claims["sub"]) }) + + t.Run("GetContextWithSponsorships", func(t *testing.T) { + cfg := config.GetConfig() + was := cfg.Contexts + defer func() { cfg.Contexts = was }() + + cfg.Contexts = map[string]interface{}{ + "context": map[string]interface{}{ + "manager_url": "http://manager.example.org", + "logos": map[string]interface{}{ + "coachco2": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, + }, + }, + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud", "type": "main"}, + map[string]interface{}{"src": "/logos/partner1.png", "alt": "Partner1", "type": "secondary"}, + }, + "dark": []interface{}{ + // no main + map[string]interface{}{"src": "/logos/partner1.png", "alt": "Partner1", "type": "secondary"}, + }, + }, + }, + }, + "sponsor1": map[string]interface{}{ + "move_url": "http://move.cozy.beta/", + "logos": map[string]interface{}{ + "coachco2": map[string]interface{}{ + "dark": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, + }, + }, + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud", "type": "main"}, + map[string]interface{}{"src": "/logos/partner1.png", "alt": "Partner1", "type": "secondary"}, + map[string]interface{}{"src": "/logos/partner2.png", "alt": "Partner2", "type": "secondary"}, + }, + "dark": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud", "type": "main"}, + map[string]interface{}{"src": "/logos/partner2.png", "alt": "Partner2"}, + map[string]interface{}{"src": "/logos/partner1.png", "alt": "Partner1"}, + }, + }, + }, + }, + "sponsor2": map[string]interface{}{ + "logos": map[string]interface{}{ + "mespapiers": map[string]interface{}{ + "dark": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, + }, + }, + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud", "type": "main"}, + map[string]interface{}{"src": "/logos/partner3.png", "alt": "Partner3", "type": "secondary"}, + map[string]interface{}{"src": "/logos/partner2.png", "alt": "Partner2", "type": "secondary"}, + }, + }, + }, + }, + } + + inst := &instance.Instance{ + Domain: "foo.example.com", + ContextName: "context", + Sponsorships: []string{"sponsor1", "sponsor2"}, + } + result := inst.GetContextWithSponsorships() + bytes, err := json.MarshalIndent(result, "", " ") + require.NoError(t, err) + expected := `{ + "logos": { + "coachco2": { + "light": [ + { + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud" + } + ] + }, + "home": { + "light": [ + { + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud", + "type": "main" + }, + { + "src": "/logos/partner1.png", + "alt": "Partner1", + "type": "secondary" + }, + { + "src": "/ext/sponsor1/logos/partner2.png", + "alt": "Partner2", + "type": "secondary" + }, + { + "src": "/ext/sponsor2/logos/partner3.png", + "alt": "Partner3", + "type": "secondary" + } + ], + "dark": [ + { + "src": "/logos/partner1.png", + "alt": "Partner1", + "type": "secondary" + }, + { + "src": "/ext/sponsor1/logos/partner2.png", + "alt": "Partner2" + } + ] + } + }, + "manager_url": "http://manager.example.org" +}` + assert.Equal(t, expected, string(bytes)) + }) } diff --git a/model/instance/lifecycle/create.go b/model/instance/lifecycle/create.go index f646abd3a32..2a487f99e9f 100644 --- a/model/instance/lifecycle/create.go +++ b/model/instance/lifecycle/create.go @@ -35,6 +35,7 @@ type Options struct { TOSLatest string Timezone string ContextName string + Sponsorships []string Email string PublicName string Settings string @@ -119,6 +120,7 @@ func Create(opts *Options) (*instance.Instance, error) { i.TOSSigned = opts.TOSSigned i.TOSLatest = opts.TOSLatest i.ContextName = opts.ContextName + i.Sponsorships = opts.Sponsorships i.BytesDiskQuota = opts.DiskQuota i.IndexViewsVersion = couchdb.IndexViewsVersion opts.trace("generate secrets", func() { @@ -311,6 +313,10 @@ func buildSettings(inst *instance.Instance, opts *Options) (*couchdb.JSONDoc, er opts.ContextName = contextName delete(settings.M, "context") } + if sponsorships, ok := settings.M["sponsorships"].([]string); ok { + opts.Sponsorships = sponsorships + delete(settings.M, "sponsorships") + } if locale, ok := settings.M["locale"].(string); ok { opts.Locale = locale delete(settings.M, "locale") diff --git a/model/instance/lifecycle/patch.go b/model/instance/lifecycle/patch.go index 7811dc06b8d..3ad30ca039b 100644 --- a/model/instance/lifecycle/patch.go +++ b/model/instance/lifecycle/patch.go @@ -92,6 +92,11 @@ func Patch(i *instance.Instance, opts *Options) error { needUpdate = true } + if len(opts.Sponsorships) != 0 { + i.Sponsorships = opts.Sponsorships + needUpdate = true + } + if opts.AuthMode != "" { var authMode instance.AuthMode authMode, err = instance.StringToAuthMode(opts.AuthMode) diff --git a/model/instance/sponsors.go b/model/instance/sponsors.go new file mode 100644 index 00000000000..2d43fd0c54f --- /dev/null +++ b/model/instance/sponsors.go @@ -0,0 +1,101 @@ +package instance + +import ( + "path/filepath" + + "github.com/cozy/cozy-stack/pkg/config/config" + "github.com/mitchellh/mapstructure" +) + +type AppLogos struct { + Light []LogoItem `json:"light,omitempty"` + Dark []LogoItem `json:"dark,omitempty"` +} +type LogoItem struct { + Src string `json:"src"` + Alt string `json:"alt"` + Type string `json:"type,omitempty"` +} + +func (i *Instance) GetContextWithSponsorships() map[string]interface{} { + context, ok := i.SettingsContext() + if !ok { + context = map[string]interface{}{} + } + if len(i.Sponsorships) == 0 { + return context + } + + // Avoid changing the global config + clone := map[string]interface{}{} + for k, v := range context { + clone[k] = v + } + context = clone + var logos map[string]AppLogos + if err := mapstructure.Decode(context["logos"], &logos); err != nil || logos == nil { + logos = make(map[string]AppLogos) + } + context["logos"] = logos + + contexts := config.GetConfig().Contexts + if contexts == nil { + return context + } + for _, sponsor := range i.Sponsorships { + if sponsorCtx, ok := contexts[sponsor].(map[string]interface{}); ok { + addHomeLogosForSponsor(context, sponsorCtx, sponsor) + } + } + return context +} + +func addHomeLogosForSponsor(context, sponsorContext map[string]interface{}, sponsor string) { + sponsorLogos, ok := sponsorContext["logos"].(map[string]interface{}) + if !ok { + return + } + var newLogos AppLogos + if err := mapstructure.Decode(sponsorLogos["home"], &newLogos); err != nil { + return + } + + contextLogos := context["logos"].(map[string]AppLogos) + homeLogos := contextLogos["home"] + + for _, logo := range newLogos.Light { + if logo.Type == "main" { + continue + } + found := false + for _, item := range homeLogos.Light { + if filepath.Base(item.Src) == filepath.Base(logo.Src) { + found = true + } + } + if found { + continue + } + logo.Src = "/ext/" + sponsor + logo.Src + homeLogos.Light = append(homeLogos.Light, logo) + } + + for _, logo := range newLogos.Dark { + if logo.Type == "main" { + continue + } + found := false + for _, item := range homeLogos.Dark { + if filepath.Base(item.Src) == filepath.Base(logo.Src) { + found = true + } + } + if found { + continue + } + logo.Src = "/ext/" + sponsor + logo.Src + homeLogos.Dark = append(homeLogos.Dark, logo) + } + + contextLogos["home"] = homeLogos +} diff --git a/pkg/config/config/config_test.go b/pkg/config/config/config_test.go index 008ba99e915..f64d12b6371 100644 --- a/pkg/config/config/config_test.go +++ b/pkg/config/config/config_test.go @@ -116,9 +116,47 @@ func TestConfigUnmarshal(t *testing.T) { map[string]interface{}{"home.konnectors.hide-errors": true}, map[string]interface{}{"home_hidden_apps": []interface{}{"foobar"}}, }, - "home_logos": map[string]interface{}{ - "/logos/1.png": "Title 1", - "/logos/2.png": "Title 2", + "logos": map[string]interface{}{ + "coachco2": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{ + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud", + }, + }, + "dark": []interface{}{ + map[string]interface{}{ + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud", + }, + }, + }, + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{ + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud", + "type": "main", + }, + map[string]interface{}{ + "src": "/logos/1_partner.png", + "alt": "Partner n°1", + "type": "secondary", + }, + }, + "dark": []interface{}{ + map[string]interface{}{ + "src": "/logos/main_cozy.png", + "alt": "Cozy Cloud", + "type": "main", + }, + map[string]interface{}{ + "src": "/logos/1_partner.png", + "alt": "Partner n°1", + "type": "secondary", + }, + }, + }, }, }, }, cfg.Contexts) diff --git a/pkg/config/config/testdata/full_config.yaml b/pkg/config/config/testdata/full_config.yaml index 822c08cb324..426b616418a 100644 --- a/pkg/config/config/testdata/full_config.yaml +++ b/pkg/config/config/testdata/full_config.yaml @@ -167,6 +167,26 @@ contexts: - home.konnectors.hide-errors: true - home_hidden_apps: - foobar - home_logos: - /logos/1.png: "Title 1" - /logos/2.png: "Title 2" + logos: + coachco2: + light: + - src: /logos/main_cozy.png + alt: Cozy Cloud + dark: + - src: /logos/main_cozy.png + alt: Cozy Cloud + home: + light: + - src: /logos/main_cozy.png + alt: Cozy Cloud + type: main + - src: /logos/1_partner.png + alt: Partner n°1 + type: secondary + dark: + - src: /logos/main_cozy.png + alt: Cozy Cloud + type: main + - src: /logos/1_partner.png + alt: Partner n°1 + type: secondary diff --git a/scripts/completion/cozy-stack.bash b/scripts/completion/cozy-stack.bash index 7e8a03df2cf..898a2737d5f 100644 --- a/scripts/completion/cozy-stack.bash +++ b/scripts/completion/cozy-stack.bash @@ -2333,6 +2333,10 @@ _cozy-stack_instances_add() two_word_flags+=("--settings") local_nonpersistent_flags+=("--settings") local_nonpersistent_flags+=("--settings=") + flags+=("--sponsorships=") + two_word_flags+=("--sponsorships") + local_nonpersistent_flags+=("--sponsorships") + local_nonpersistent_flags+=("--sponsorships=") flags+=("--swift-layout=") two_word_flags+=("--swift-layout") local_nonpersistent_flags+=("--swift-layout") @@ -2845,6 +2849,10 @@ _cozy-stack_instances_modify() two_word_flags+=("--settings") local_nonpersistent_flags+=("--settings") local_nonpersistent_flags+=("--settings=") + flags+=("--sponsorships=") + two_word_flags+=("--sponsorships") + local_nonpersistent_flags+=("--sponsorships") + local_nonpersistent_flags+=("--sponsorships=") flags+=("--tos=") two_word_flags+=("--tos") local_nonpersistent_flags+=("--tos") diff --git a/web/instances/instances.go b/web/instances/instances.go index 1c454534493..2830746692d 100644 --- a/web/instances/instances.go +++ b/web/instances/instances.go @@ -73,6 +73,9 @@ func createHandler(c echo.Context) error { if domainAliases := c.QueryParam("DomainAliases"); domainAliases != "" { opts.DomainAliases = strings.Split(domainAliases, ",") } + if sponsorships := c.QueryParam("sponsorships"); sponsorships != "" { + opts.Sponsorships = strings.Split(sponsorships, ",") + } if autoUpdate := c.QueryParam("AutoUpdate"); autoUpdate != "" { b, err := strconv.ParseBool(autoUpdate) if err != nil { @@ -171,6 +174,9 @@ func modifyHandler(c echo.Context) error { if domainAliases := c.QueryParam("DomainAliases"); domainAliases != "" { opts.DomainAliases = strings.Split(domainAliases, ",") } + if sponsorships := c.QueryParam("Sponsorships"); sponsorships != "" { + opts.Sponsorships = strings.Split(sponsorships, ",") + } if quota := c.QueryParam("DiskQuota"); quota != "" { i, err := strconv.ParseInt(quota, 10, 64) if err != nil { diff --git a/web/settings/context.go b/web/settings/context.go index 6da9fee3070..b02dd916a49 100644 --- a/web/settings/context.go +++ b/web/settings/context.go @@ -120,10 +120,6 @@ func (h *HTTPHandler) context(c echo.Context) error { } i := middlewares.GetInstance(c) - ctx, ok := i.SettingsContext() - if !ok { - ctx = map[string]interface{}{} - } - doc := &apiContext{ctx} + doc := &apiContext{i.GetContextWithSponsorships()} return jsonapi.Data(c, http.StatusOK, doc, nil) } diff --git a/web/settings/instance.go b/web/settings/instance.go index 886fb73c881..f5621de002c 100644 --- a/web/settings/instance.go +++ b/web/settings/instance.go @@ -57,6 +57,9 @@ func (h *HTTPHandler) getInstance(c echo.Context) error { doc.M["uuid"] = inst.UUID doc.M["oidc_id"] = inst.OIDCID doc.M["context"] = inst.ContextName + if len(inst.Sponsorships) > 0 { + doc.M["sponsorships"] = inst.Sponsorships + } // XXX we had a bug where the default_redirection was filled by a full URL // instead of slug+path, and we fix it when this endpoint is called. if value, ok := doc.M["default_redirection"].(string); !ok || strings.HasPrefix(value, "http") { @@ -99,6 +102,7 @@ func (h *HTTPHandler) updateInstance(c echo.Context) error { delete(doc.M, "tos_latest") delete(doc.M, "uuid") delete(doc.M, "context") + delete(doc.M, "sponsorships") delete(doc.M, "oidc_id") } diff --git a/web/settings/settings_test.go b/web/settings/settings_test.go index c9b444482ae..cd5b53275fe 100644 --- a/web/settings/settings_test.go +++ b/web/settings/settings_test.go @@ -74,7 +74,16 @@ func TestSettings(t *testing.T) { config.UseTestFile(t) conf := config.GetConfig() conf.Assets = "../../assets" - conf.Contexts[config.DefaultInstanceContext] = map[string]interface{}{"manager_url": "http://manager.example.org"} + conf.Contexts[config.DefaultInstanceContext] = map[string]interface{}{ + "manager_url": "http://manager.example.org", + "logos": map[string]interface{}{ + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, + }, + }, + }, + } was := conf.Subdomains conf.Subdomains = config.NestedSubdomains defer func() { conf.Subdomains = was }() @@ -104,11 +113,27 @@ func TestSettings(t *testing.T) { t.Run("GetContext", func(t *testing.T) { e := testutils.CreateTestClient(t, tsURL) - e.GET("/settings/context"). + obj := e.GET("/settings/context"). WithCookie(sessCookie, "connected"). WithHeader("Accept", "application/vnd.api+json"). WithHeader("Authorization", "Bearer "+token). - Expect().Status(200) + Expect().Status(200). + JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). + Object() + + data := obj.Value("data").Object() + data.ValueEqual("type", "io.cozy.settings") + data.ValueEqual("id", "io.cozy.settings.context") + + attrs := data.Value("attributes").Object() + attrs.ValueEqual("manager_url", "http://manager.example.org") + attrs.ValueEqual("logos", map[string]interface{}{ + "home": map[string]interface{}{ + "light": []interface{}{ + map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, + }, + }, + }) }) t.Run("PatchWithGoodRev", func(t *testing.T) {