package cabana import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" ) // settingsLinkItem is a settings entry that opens an admin controller instead // of a singleton form. func settingsLinkItem() pact.SettingsItem { return pact.SettingsItem{ Code: "locales", Label: "Locales", Description: "Manage locales", Category: "Demo", Icon: "languages", Order: 30, Permissions: []string{"acme.demo.manage_settings"}, Controller: "acme.demo.widgets", } } // settingsLinkPlugin declares only a link entry and ships no admin assets. func settingsLinkPlugin(items ...pact.SettingsItem) contributionPlugin { return contributionPlugin{ id: "acme.links", permissions: []pact.Permission{{Code: "acme.links.unused", Roles: []string{"developer"}}}, settings: items, } } // settingsLinkRegistry compiles the metadata plugin (one singleton setting) // together with a plugin holding one controller link. func settingsLinkRegistry(t *testing.T) *Registry { t.Helper() reg := metadataRegistry() if err := compileContributions(reg, []party.Plugin{metadataPlugin(), settingsLinkPlugin(settingsLinkItem())}); err != nil { t.Fatal(err) } return reg } func TestSettingsLinkCompilesWithoutAdminFS(t *testing.T) { reg := settingsLinkRegistry(t) stored, ok := reg.settings["locales"] if !ok || stored == nil { t.Fatal("link entry was not registered") } if stored.Form != nil || stored.Writable != nil || stored.Item.Controller != "acme.demo.widgets" || stored.PluginID != "acme.links" { t.Fatalf("link entry = %#v", stored) } if _, ok := reg.Setting("locales"); ok { t.Fatal("Registry.Setting returned a link entry") } if setting, ok := reg.Setting("demo"); !ok || setting.Form == nil { t.Fatalf("singleton lookup = %#v %v", setting, ok) } } func TestSettingsLinkCompileValidation(t *testing.T) { withForm := settingsLinkItem() withForm.Form = "models/settings/fields.yaml" withFactory := settingsLinkItem() withFactory.NewModel = func() any { return &singletonSetting{} } withModel := settingsLinkItem() withModel.Model = "Settings" missing := settingsLinkItem() missing.Controller = "acme.demo.missing" duplicate := settingsLinkItem() duplicate.Code = "demo" unknownPermission := settingsLinkItem() unknownPermission.Permissions = []string{"acme.demo.nope"} cases := []struct { name string item pact.SettingsItem want string }{ {"form", withForm, "cabana: setting locales links controller acme.demo.widgets and declares a singleton form"}, {"model factory", withFactory, "cabana: setting locales links controller acme.demo.widgets and declares a singleton form"}, {"model", withModel, "cabana: setting locales links controller acme.demo.widgets and names a model"}, {"unknown controller", missing, "cabana: setting locales references unknown controller acme.demo.missing"}, {"duplicate code", duplicate, "duplicate setting demo"}, {"unknown permission", unknownPermission, "unknown permission"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := compileContributions(metadataRegistry(), []party.Plugin{metadataPlugin(), settingsLinkPlugin(tc.item)}) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("error = %v, want %q", err, tc.want) } }) } } func TestSettingsLinkMetadata(t *testing.T) { reg := settingsLinkRegistry(t) developer := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: reg.rolePermissions("developer")} _, settings := reg.Metadata(context.Background(), developer, nil) if len(settings) != 2 { t.Fatalf("developer settings = %#v", settings) } byCode := map[string]SettingsEntry{} for _, entry := range settings { byCode[entry.Code] = entry } link, single := byCode["locales"], byCode["demo"] if link.Controller != "acme.demo.widgets" || link.Model != "" || link.Label != "Locales" || link.Order != 30 { t.Fatalf("link entry = %#v", link) } if link.Keywords == nil { t.Fatal("link entry keywords are nil") } if single.Controller != "" || single.Model != "Settings" { t.Fatalf("singleton entry = %#v", single) } // The principal passes the item's permission but cannot open the // controller (it requires acme.demo.access): the link is hidden, the // singleton stays. restricted := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{"acme.demo.manage_settings": true}} _, settings = reg.Metadata(context.Background(), restricted, nil) if len(settings) != 1 || settings[0].Code != "demo" { t.Fatalf("restricted settings = %#v", settings) } // Controller access alone does not pass the item's own permission. controllerOnly := &bouncer.Principal{ID: 3, Backend: true, PermissionGrants: map[string]bool{"acme.demo.access": true}} _, settings = reg.Metadata(context.Background(), controllerOnly, nil) if len(settings) != 0 { t.Fatalf("controller-only settings = %#v", settings) } } func TestSettingsLinkEndpoints(t *testing.T) { reg := settingsLinkRegistry(t) svc := &service{reg: reg} developer := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: reg.rolePermissions("developer")} request := func(method, rel, code string) *http.Request { var body *strings.Reader if method == http.MethodPut { body = strings.NewReader(`{"enabled":true}`) } else { body = strings.NewReader("") } req := httptest.NewRequest(method, adminAPI(rel), body) if code != "" { req.SetPathValue("code", code) } return req.WithContext(bouncer.WithUser(req.Context(), developer)) } for _, tc := range []struct { name string method string rel string handler func(*service, http.ResponseWriter, *http.Request) }{ {"schema", http.MethodGet, "/settings/locales/schema", (*service).settingsSchema}, {"get", http.MethodGet, "/settings/locales", (*service).settingsGet}, {"put", http.MethodPut, "/settings/locales", (*service).settingsPut}, } { t.Run(tc.name, func(t *testing.T) { rec := httptest.NewRecorder() tc.handler(svc, rec, request(tc.method, tc.rel, "locales")) if rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), `"not_found"`) { t.Fatalf("%s %s = %d %s", tc.method, tc.rel, rec.Code, rec.Body.String()) } }) } rec := httptest.NewRecorder() svc.settingsList(rec, request(http.MethodGet, "/settings", "")) if rec.Code != http.StatusOK { t.Fatalf("GET /settings = %d %s", rec.Code, rec.Body.String()) } var envelope struct { Data []map[string]any `json:"data"` } if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil { t.Fatal(err) } if len(envelope.Data) != 2 { t.Fatalf("GET /settings data = %#v", envelope.Data) } for _, entry := range envelope.Data { controller, ok := entry["controller"] if !ok { t.Fatalf("entry without controller key: %#v", entry) } want := "" if entry["code"] == "locales" { want = "acme.demo.widgets" } if controller != want { t.Fatalf("entry %v controller = %v, want %q", entry["code"], controller, want) } } }