test(19.6.1-03): last-superuser race, lifecycle, revocation, seam and roles tests

- TestAdministratorsLastSuperuser (D-09): A and B reduce each other on the
  five paths while a test transaction holds the guard's advisory lock until
  pg_locks shows both requests waiting; one succeeds, one gets last_superuser
- TestAdministratorsLastSuperuserConcurrent: ten unsynchronized rounds
- TestAdministratorsLifecycle (D-10) and TestAdministratorsRevocation (D-12),
  two-row bulk selections included
- TestActionExtensionBoot (-short) and TestActionExtensionRun (D-14, D-15)
- TestRolesLifecycle (D-16, D-17), TestRolesPermissionCeiling (D-07)
- TestBackendScreensActivation (-short, D-01, D-04)
This commit is contained in:
Jakub Zych
2026-10-22 12:09:12 +02:00
parent 83240706b2
commit 1a56294d9b
5 changed files with 902 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
package cabana_test
import (
"fmt"
"net/http"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
)
// roleURL is the record path of a role.
func roleURL(id uint) string { return fmt.Sprintf("%s/%d", rolesPath, id) }
// roleBody is a role save body.
func roleBody(role cabana.BackendUserRole, extra map[string]any) map[string]any {
body := map[string]any{"name": role.Name, "code": role.Code, "description": role.Description}
for key, value := range extra {
body[key] = value
}
return body
}
// roleExists reports whether a role row with id exists.
func (e *screensEnv) roleExists(t *testing.T, id uint) bool {
t.Helper()
var n int64
if err := e.gdb.Raw(`SELECT count(*) FROM backend_user_roles WHERE id = ?`, id).Scan(&n).Error; err != nil {
t.Fatal(err)
}
return n == 1
}
// TestRolesLifecycle (D-16, D-17): role codes are unique; a system role (is_system
// or one code-declared grants attach to) keeps its code and cannot be
// deleted; a role any administrator holds, deleted ones included, cannot be
// deleted, alone or in a bulk selection.
func TestRolesLifecycle(t *testing.T) {
declared := fmt.Sprintf("plugrole-%d", time.Now().UnixNano())
env := newScreensEnv(t, grantPlugin{id: "acme.granter", grants: []pact.Permission{{Code: "acme.granter.use", Roles: []string{declared}}}})
super := env.seedAdmin(t, adminSeed{Name: "roles-super", Superuser: true})
token := env.login(t, super.Login)
// Create with the slug the form's preset makes from the name.
name := "Editors " + env.stamp
code := "editors-" + env.stamp
created := dataOf(env.expect(t, http.StatusCreated, http.MethodPost, rolesPath, token,
map[string]any{"name": name, "code": code, "description": "Edit things"}))
createdID := uint(created["id"].(float64))
t.Cleanup(func() { env.gdb.Exec(`DELETE FROM backend_user_roles WHERE id = ?`, createdID) })
var stored cabana.BackendUserRole
if err := env.gdb.First(&stored, createdID).Error; err != nil || stored.Code != code || stored.IsSystem {
t.Fatalf("created role = %+v, %v", stored, err)
}
env.expectFieldRefusal(t, http.StatusUnprocessableEntity, http.MethodPost, rolesPath, token,
map[string]any{"name": "Other " + env.stamp, "code": code}, "code", enCodeTaken, false)
before := env.roleRow(t, createdID)
other := env.seedRole(t, roleSeed{Name: "roles-other"})
env.expectFieldRefusal(t, http.StatusUnprocessableEntity, http.MethodPut, roleURL(other.ID), token,
roleBody(other, map[string]any{"code": code}), "code", enCodeTaken, false)
if env.roleRow(t, createdID) != before {
t.Fatal("a refused code change touched the created role")
}
// System roles: is_system, and a role a plugin's code-declared grant
// attaches to.
var developer cabana.BackendUserRole
if err := env.gdb.Where("code = ?", "developer").First(&developer).Error; err != nil {
t.Fatal(err)
}
pluginRole := env.seedRole(t, roleSeed{Name: "roles-declared", Code: declared})
for _, role := range []cabana.BackendUserRole{developer, pluginRole} {
rowBefore := env.roleRow(t, role.ID)
env.expectFieldRefusal(t, http.StatusUnprocessableEntity, http.MethodPut, roleURL(role.ID), token,
roleBody(role, map[string]any{"code": role.Code + "-renamed"}), "code", enCodeLocked, false)
env.expectRefusal(t, http.StatusForbidden, http.MethodDelete, roleURL(role.ID), token, nil, enDeleteSystem)
if env.roleRow(t, role.ID) != rowBefore {
t.Fatalf("a refused change touched system role %s", role.Code)
}
}
// A system role's name and description stay editable.
env.expect(t, http.StatusOK, http.MethodPut, roleURL(pluginRole.ID), token, roleBody(pluginRole, map[string]any{"description": "Changed"}))
// A held role, one holder active and one soft-deleted.
held := env.seedRole(t, roleSeed{Name: "roles-held"})
env.seedAdmin(t, adminSeed{Name: "roles-holder", Role: held.ID})
env.seedAdmin(t, adminSeed{Name: "roles-gone", Role: held.ID, Deleted: true})
env.expectRefusal(t, http.StatusForbidden, http.MethodDelete, roleURL(held.ID), token, nil,
"The role “"+held.Name+"” is assigned to 2 administrators.")
if !env.roleExists(t, held.ID) {
t.Fatal("a refused delete removed the role")
}
// A bulk delete of an unused role and the held one is refused as a whole.
unused := env.seedRole(t, roleSeed{Name: "roles-unused"})
env.expectRefusal(t, http.StatusForbidden, http.MethodPost, rolesPath+"/bulk-delete", token,
map[string]any{"ids": []uint{unused.ID, held.ID}}, "is assigned to 2 administrators")
if !env.roleExists(t, unused.ID) || !env.roleExists(t, held.ID) {
t.Fatal("a refused bulk delete removed a role")
}
// The unused role alone deletes.
env.expect(t, http.StatusOK, http.MethodDelete, roleURL(unused.ID), token, nil)
if env.roleExists(t, unused.ID) {
t.Fatal("the unused role was not deleted")
}
}
// TestRolesPermissionCeiling (D-07): a non-superuser holding
// backend.manage_roles can neither add nor remove a code it does not hold on
// any role; codes it holds toggle.
func TestRolesPermissionCeiling(t *testing.T) {
env := newScreensEnv(t)
editor := env.seedAdmin(t, adminSeed{Name: "rceil-editor", RolePermissions: map[string]int{cabana.PermissionManageRoles: 1, pingPermission: 1}})
token := env.login(t, editor.Login)
field := schemaFields(env.expect(t, http.StatusOK, http.MethodGet, rolesPath+"/schema/form", token, nil))["permissions"]
locked := permissionLocked(field)
if len(locked) < 4 {
t.Fatalf("permission options = %v", locked)
}
for code, isLocked := range locked {
held := code == cabana.PermissionManageRoles || code == pingPermission
if isLocked == held {
t.Fatalf("option %s locked=%v, want %v", code, isLocked, !held)
}
}
strong := env.seedRole(t, roleSeed{Name: "rceil-strong", Permissions: map[string]int{"acme.demo.access": 1}})
empty := env.seedRole(t, roleSeed{Name: "rceil-empty"})
strong, empty = env.reloadRole(t, strong.ID), env.reloadRole(t, empty.ID)
for _, tc := range []struct {
name string
role cabana.BackendUserRole
permissions map[string]any
}{
{"add a locked code", empty, map[string]any{"acme.demo.access": 1}},
{"add a locked code next to a held one", empty, map[string]any{"acme.demo.access": 1, pingPermission: 1}},
{"remove a locked code", strong, map[string]any{}},
{"remove a locked code, add a held one", strong, map[string]any{pingPermission: 1}},
} {
t.Run(tc.name, func(t *testing.T) {
before := env.roleRow(t, tc.role.ID)
env.expectFieldRefusal(t, http.StatusForbidden, http.MethodPut, roleURL(tc.role.ID), token,
roleBody(tc.role, map[string]any{"permissions": tc.permissions}), "permissions", enPermissionLocked, false)
if env.roleRow(t, tc.role.ID) != before {
t.Fatal("a refused permission change touched the role")
}
})
}
env.expect(t, http.StatusOK, http.MethodPut, roleURL(strong.ID), token,
roleBody(strong, map[string]any{"permissions": map[string]any{"acme.demo.access": 1, pingPermission: 1}}))
if got := decodeJSON(t, env.reloadRole(t, strong.ID).Permissions); got["acme.demo.access"] != 1 || got[pingPermission] != 1 || len(got) != 2 {
t.Fatalf("strong role after adding the held code = %v", got)
}
env.expect(t, http.StatusOK, http.MethodPut, roleURL(strong.ID), token,
roleBody(strong, map[string]any{"permissions": map[string]any{"acme.demo.access": 1}}))
if got := decodeJSON(t, env.reloadRole(t, strong.ID).Permissions); got["acme.demo.access"] != 1 || len(got) != 1 {
t.Fatalf("strong role after removing the held code = %v", got)
}
// Without the permissions key the stored grants stay.
env.expect(t, http.StatusOK, http.MethodPut, roleURL(strong.ID), token, roleBody(strong, map[string]any{"description": "Renamed"}))
if got := env.reloadRole(t, strong.ID); got.Description != "Renamed" || got.Permissions != `{"acme.demo.access":1}` {
t.Fatalf("strong role after a description change = %+v", got)
}
}
// reloadRole reads a role row.
func (e *screensEnv) reloadRole(t *testing.T, id uint) cabana.BackendUserRole {
t.Helper()
var role cabana.BackendUserRole
if err := e.gdb.First(&role, id).Error; err != nil {
t.Fatal(err)
}
return role
}