Files
summercms/modules/cabana/backend_screens_test.go
Jakub Zych 653d95a5b9 test(cabana): cover the Roles form's holders list (quick-261028-rqg)
- TestRolesHoldersPartial (Docker-free): users partial field, view model
  guard, links, state lines, escaped strings, empty line, note
- TestRolesHolders (Postgres): D-03 hiding, links by manage_users, states,
  empty role, 100-row cap with note, 403 without manage_roles
2026-10-28 21:37:22 +01:00

322 lines
12 KiB
Go

package cabana
import (
"context"
"encoding/json"
"io/fs"
"strings"
"testing"
"testing/fstest"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
)
// screensExtender owns one list controller (so the admin is active) and adds
// record actions to other controllers through pact.HasAdminActionExtensions.
type screensExtender struct {
extensions []pact.AdminActionExtension
}
type screensExtenderController struct{}
func (screensExtenderController) ID() string { return "acme.ext.items" }
func (screensExtenderController) ModelName() string { return "Item" }
func (screensExtenderController) ConfigDir() string { return "controllers/items" }
func (screensExtenderController) RequiredPermissions() []string { return []string{"acme.ext.access"} }
func (screensExtender) ID() string { return "acme.ext" }
func (screensExtender) Requires() []string { return nil }
func (screensExtender) Register(*backpack.App) error { return nil }
func (screensExtender) Boot(*backpack.App) error { return nil }
func (screensExtender) AdminControllers() []pact.AdminController {
return []pact.AdminController{screensExtenderController{}}
}
func (screensExtender) AdminFS() fs.FS {
return fstest.MapFS{
"controllers/items/config_list.yaml": &fstest.MapFile{Data: []byte("list: ~/plugins/acme/ext/models/item/columns.yaml\nmodelClass: Item\nrecordsPerPage: 20\n")},
"models/item/columns.yaml": &fstest.MapFile{Data: []byte("columns:\n name:\n label: Name\n")},
}
}
func (screensExtender) Permissions() []pact.Permission {
return []pact.Permission{
{Code: "acme.ext.access", Roles: []string{"developer"}},
{Code: "acme.ext.ping"},
}
}
func (p screensExtender) AdminActionExtensions() []pact.AdminActionExtension { return p.extensions }
func pingAction() pact.AdminRecordAction {
return pact.AdminRecordAction{
Name: "acme_ping", Label: "Ping", Permissions: []string{"acme.ext.ping"},
Run: func(context.Context, pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
return pact.AdminRecordActionResult{}, nil
},
}
}
// compileWithScreens runs the compile steps of Activate that need no config
// or database on plugins plus the admin's own screens.
func compileWithScreens(t *testing.T, plugins []party.Plugin) (*Registry, error) {
t.Helper()
extended, screens := withBackendScreens(nil, plugins)
if len(extended) != len(plugins)+1 || extended[len(extended)-1] != party.Plugin(screens) {
t.Fatalf("withBackendScreens = %v", extended)
}
items, err := collectControllers(extended)
if err != nil {
return nil, err
}
if err := checkReservedSegments(items); err != nil {
return nil, err
}
types, err := compileFieldTypes(extended)
if err != nil {
return nil, err
}
reg, err := compileRegistryWithTypes(items, types)
if err != nil {
return nil, err
}
if err := compileActionExtensions(reg, extended); err != nil {
return nil, err
}
if err := compileContributions(reg, extended); err != nil {
return nil, err
}
screens.reg = reg
return reg, nil
}
func TestBackendScreensSmoke(t *testing.T) {
extender := screensExtender{extensions: []pact.AdminActionExtension{{
Controller: AdministratorsControllerID,
RecordActions: []pact.AdminRecordAction{pingAction()},
}}}
plugins := []party.Plugin{extender}
reg, err := compileWithScreens(t, plugins)
if err != nil {
t.Fatal(err)
}
if len(plugins) != 1 {
t.Fatalf("the caller's plugin slice changed: %v", plugins)
}
cc, ok := reg.Get(AdministratorsControllerID)
if !ok || cc.Form == nil || cc.Form.preview == nil {
t.Fatalf("administrators controller = %#v", cc)
}
names := cc.Form.recordActions
if len(names) == 0 || names[len(names)-1] != "acme_ping" {
t.Fatalf("administrators record actions = %v, want acme_ping last", names)
}
if _, ok := cc.RecordActions["acme_ping"]; !ok || cc.contributed["acme_ping"] != "acme.ext" {
t.Fatalf("RecordActions = %v contributed = %v", cc.RecordActions, cc.contributed)
}
if got := recordActionOfName(cc, "acme_ping"); !got {
t.Fatal("recordActionOf does not resolve the contributed action")
}
for _, code := range []string{PermissionManageUsers, PermissionManageRoles} {
if _, ok := reg.permissions[code]; !ok {
t.Fatalf("catalog lacks %s", code)
}
for _, role := range []string{"developer", "publisher"} {
if reg.rolePermissions(role)[code] {
t.Fatalf("role %s is granted %s in code", role, code)
}
}
}
if order := reg.permissionOrder; len(order) != len(reg.permissions) || order[0] != "acme.ext.access" {
t.Fatalf("permissionOrder = %v", order)
}
ctx := context.Background()
_, settings := reg.Metadata(ctx, &bouncer.Principal{ID: 1, Backend: true, IsSuperuser: true}, nil)
found := false
for _, entry := range settings {
if entry.Code == "administrators" {
found = entry.Controller == AdministratorsControllerID && entry.Category == "backend::lang.settings.category_system" && entry.Order == 100
}
}
if !found {
t.Fatalf("superuser settings = %+v, want administrators", settings)
}
_, settings = reg.Metadata(ctx, &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{"acme.ext.access": true}}, nil)
for _, entry := range settings {
if entry.Code == "administrators" {
t.Fatalf("an administrator without %s sees %+v", PermissionManageUsers, entry)
}
}
_, settings = reg.Metadata(ctx, &bouncer.Principal{ID: 3, Backend: true, PermissionGrants: map[string]bool{PermissionManageUsers: true}}, nil)
if len(settings) != 1 || settings[0].Code != "administrators" {
t.Fatalf("holder of %s sees %+v", PermissionManageUsers, settings)
}
missing := screensExtender{extensions: []pact.AdminActionExtension{{
Controller: "acme.missing.things",
RecordActions: []pact.AdminRecordAction{pingAction()},
}}}
if _, err := compileWithScreens(t, []party.Plugin{missing}); err == nil || !strings.Contains(err.Error(), "acme.missing.things") {
t.Fatalf("unknown controller error = %v", err)
}
routes, err := Activate(nil, []party.Plugin{contributionPlugin{id: "acme.plain", permissions: []pact.Permission{{Code: "acme.plain.use"}}}})
if routes != nil || err != nil {
t.Fatalf("Activate without admin controllers = %v, %v", routes, err)
}
}
// recordActionOfName reports whether the record action route resolves name.
func recordActionOfName(cc *CompiledController, name string) bool {
_, ok := recordActionOf(cc, name)
return ok
}
// TestBackendScreensActivation (D-01, D-04): the admin's own screens join
// only an application that has an admin; then they bring both controllers,
// both settings links (Administrators before Roles), both permission codes
// granted to no role, and no navigation entry.
func TestBackendScreensActivation(t *testing.T) {
// No admin controllers: no admin, and no admin config is read (the app
// has no admin.jwt.secret).
app := backpack.New(nil)
plain := contributionPlugin{id: "acme.plain", permissions: []pact.Permission{{Code: "acme.plain.use"}}}
routes, err := Activate(app, []party.Plugin{plain})
if routes != nil || err != nil {
t.Fatalf("Activate without admin controllers = %v, %v", routes, err)
}
// With a controller the same app needs the secret: the admin is active.
if _, err := Activate(app, []party.Plugin{screensExtender{}}); err == nil || !strings.Contains(err.Error(), "admin.jwt.secret") {
t.Fatalf("Activate with a controller and no secret = %v", err)
}
reg, err := compileWithScreens(t, []party.Plugin{screensExtender{}})
if err != nil {
t.Fatal(err)
}
for _, id := range []string{AdministratorsControllerID, RolesControllerID} {
cc, ok := reg.Get(id)
if !ok || cc.PluginID != "summercms.cabana" || cc.List == nil || cc.Form == nil {
t.Fatalf("controller %s = %#v", id, cc)
}
}
for _, code := range []string{PermissionManageUsers, PermissionManageRoles} {
permission, ok := reg.permissions[code]
if !ok || len(permission.Roles) != 0 {
t.Fatalf("permission %s = %+v (present %v), want no role grants", code, permission, ok)
}
for _, role := range []string{"developer", "publisher"} {
if reg.rolePermissions(role)[code] {
t.Fatalf("role %s holds %s", role, code)
}
}
}
ctx := context.Background()
navigation, settings := reg.Metadata(ctx, &bouncer.Principal{ID: 1, Backend: true, IsSuperuser: true}, nil)
var codes []string
for _, entry := range settings {
if entry.Controller == AdministratorsControllerID || entry.Controller == RolesControllerID {
codes = append(codes, entry.Code)
}
}
if strings.Join(codes, ",") != "administrators,roles" {
t.Fatalf("superuser settings for the screens = %v, want administrators then roles", codes)
}
for _, entry := range navigation {
if strings.HasPrefix(entry.Controller, "summercms.cabana.") {
t.Fatalf("navigation entry %+v from the screens", entry)
}
}
// A holder of backend.manage_roles alone sees the Roles link only.
_, settings = reg.Metadata(ctx, &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{PermissionManageRoles: true}}, nil)
if len(settings) != 1 || settings[0].Code != "roles" {
t.Fatalf("holder of %s sees %+v", PermissionManageRoles, settings)
}
}
// TestRolesHoldersPartial: the Roles form carries the read-only users
// partial on the Administrators tab of the update form, its view model
// passes the curated-model guard, and the template renders links, states,
// the empty line and the note, escaping holder strings.
func TestRolesHoldersPartial(t *testing.T) {
reg, err := compileWithScreens(t, nil)
if err != nil {
t.Fatal(err)
}
cc, ok := reg.Get(RolesControllerID)
if !ok || cc.Form == nil {
t.Fatal("roles controller has no form")
}
var field *FormField
for i := range cc.Form.Fields {
if cc.Form.Fields[i].Name == "users" {
field = &cc.Form.Fields[i]
}
}
if field == nil {
t.Fatal("roles form has no users field")
}
if field.Type != "partial" || field.Path != "users" || field.Tab != "backend::lang.user.menu_label" ||
field.Context == nil || len(field.Context.values) != 1 || field.Context.values[0] != "update" {
t.Fatalf("users field = %+v (context %+v)", *field, field.Context)
}
partial := cc.partials["users"]
if partial == nil || !cc.formPartials["users"] {
t.Fatalf("users partial compiled=%v form=%v", partial != nil, cc.formPartials["users"])
}
if reason := refusedViewModel(cc, roleHoldersView{Rows: []roleHolderRow{{Login: "x", States: []string{"k"}}}}); reason != "" {
t.Fatalf("view model refused: %s", reason)
}
render := func(view roleHoldersView) string {
t.Helper()
nodes, err := partial.render(context.Background(), nil, view)
if err != nil {
t.Fatal(err)
}
return nodesJSON(t, nodes)
}
linked := render(roleHoldersView{Rows: []roleHolderRow{{
Login: "ada", FirstName: "Ada", LastName: "Lovelace", Email: "ada@example.test",
Link: "/backend/summercms/cabana/users/7/preview",
States: []string{"backend::lang.user.superuser", "backend::lang.user.state_deleted"},
}}})
for _, want := range []string{`"tag":"table"`, `"tag":"a"`, `"href":"/backend/summercms/cabana/users/7/preview"`,
`"text":"ada"`, `"text":"ada@example.test"`, `"text":"backend::lang.user.login"`} {
if !strings.Contains(linked, want) {
t.Fatalf("linked render lacks %s: %s", want, linked)
}
}
if got := strings.Count(linked, "summer-table__sub"); got != 2 {
t.Fatalf("state lines = %d, want 2: %s", got, linked)
}
plain := render(roleHoldersView{Rows: []roleHolderRow{{Login: "<script>x</script>"}}})
if strings.Contains(plain, `"tag":"a"`) || strings.Contains(plain, `"tag":"script"`) || strings.Contains(plain, "summer-table__sub") {
t.Fatalf("plain render = %s", plain)
}
escaped, err := json.Marshal("<script>x</script>")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(plain, `"text":`+string(escaped)) {
t.Fatalf("login not rendered as escaped text: %s", plain)
}
if empty := render(roleHoldersView{Empty: true}); !strings.Contains(empty, `"text":"backend::lang.user.role.users_empty"`) || strings.Contains(empty, `"tag":"table"`) {
t.Fatalf("empty render = %s", empty)
}
if noted := render(roleHoldersView{Rows: []roleHolderRow{{Login: "ada"}}, Note: "Showing the first 100 of 101"}); !strings.Contains(noted, `"text":"Showing the first 100 of 101"`) {
t.Fatalf("note render = %s", noted)
}
nodes, err := partial.render(context.Background(), nil, roleHoldersView{})
if err != nil || len(nodes) != 0 {
t.Fatalf("zero view = %v, %v", nodes, err)
}
}