test(19.6.1-03): trigger boot rules and per-record permission locks

- TestFieldTriggerBoot (-short): every compileTrigger and checkTriggers
  refusal names controller, field and trigger; disable and hide reach the
  localized schema; settings and relation forms refuse the key
- TestPermissionRecordLocks (-short): the save rule in radio and checkbox
  modes, no lock lookup on create, meta only for stored records with a
  provider
- TestRolesCodeDeclaredLocks (Postgres): locked_by_code grants on a role,
  a save without them, locked_self on the own record
This commit is contained in:
Jakub Zych
2026-10-22 12:11:57 +02:00
parent 1a56294d9b
commit 085179b676
3 changed files with 370 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ package cabana_test
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
@@ -176,3 +177,96 @@ func (e *screensEnv) reloadRole(t *testing.T, id uint) cabana.BackendUserRole {
}
return role
}
// permissionLocks returns a record response's meta.permissionLocks for the
// permissions field as code to [value, note].
func permissionLocks(body map[string]any) map[string][2]any {
out := map[string][2]any{}
meta, _ := body["meta"].(map[string]any)
locks, _ := meta["permissionLocks"].(map[string]any)
list, _ := locks["permissions"].([]any)
for _, item := range list {
lock, _ := item.(map[string]any)
code, _ := lock["code"].(string)
out[code] = [2]any{lock["value"], lock["note"]}
}
return out
}
// TestRolesCodeDeclaredLocks (D-21 for D-16 and D-08): a role's code-declared
// grants travel as record locks with the locked_by_code note and survive a
// save that leaves them out; an administrator's own record locks every
// offered code with the locked_self note.
func TestRolesCodeDeclaredLocks(t *testing.T) {
env := newScreensEnv(t, grantPlugin{id: "acme.reporter", grants: []pact.Permission{{Code: "acme.screens.report", Roles: []string{"developer"}}}})
super := env.seedAdmin(t, adminSeed{Name: "locks-super", Superuser: true})
token := env.login(t, super.Login)
developer := env.reloadRoleByCode(t, "developer")
t.Cleanup(func() {
env.gdb.Exec(`UPDATE backend_user_roles SET description = ?, permissions = ? WHERE id = ?`, developer.Description, developer.Permissions, developer.ID)
})
show := env.expect(t, http.StatusOK, http.MethodGet, roleURL(developer.ID), token, nil)
locks := permissionLocks(show)
for _, code := range []string{"acme.screens.report", "acme.demo.access"} {
if got, ok := locks[code]; !ok || got[0] != float64(1) || got[1] != enLockedByCode {
t.Fatalf("lock of %s = %v (present %v), all %v", code, got, ok, locks)
}
}
if _, ok := locks[cabana.PermissionManageUsers]; ok {
t.Fatalf("a code the role does not get from a plugin is locked: %v", locks)
}
// The admin's payload leaves the record-locked codes out.
stored, _ := dataOf(show)["permissions"].(map[string]any)
payload := map[string]any{}
for code, value := range stored {
if _, locked := locks[code]; !locked {
payload[code] = value
}
}
payload[pingPermission] = 1
env.expect(t, http.StatusOK, http.MethodPut, roleURL(developer.ID), token,
roleBody(developer, map[string]any{"description": "Changed " + env.stamp, "permissions": payload}))
saved := env.reloadRoleByCode(t, "developer")
if saved.Description != "Changed "+env.stamp || decodeJSON(t, saved.Permissions)[pingPermission] != 1 {
t.Fatalf("developer after the save = %+v", saved)
}
if strings.Contains(saved.Permissions, "acme.screens.report") {
t.Fatalf("a code-declared grant was written to the stored JSON: %s", saved.Permissions)
}
// The delegate's own record: every offered code record-locked.
delegate := env.seedAdmin(t, adminSeed{Name: "locks-delegate", RolePermissions: manageUsers, Permissions: map[string]int{pingPermission: -1}})
delegateToken := env.login(t, delegate.Login)
offered := permissionLocked(schemaFields(env.expect(t, http.StatusOK, http.MethodGet, usersPath+"/schema/form", delegateToken, nil))["permissions"])
own := permissionLocks(env.expect(t, http.StatusOK, http.MethodGet, userURL(delegate.ID), delegateToken, nil))
if len(own) != len(offered) || len(own) == 0 {
t.Fatalf("own record locks %v, offered %v", own, offered)
}
for code := range offered {
got, ok := own[code]
want := float64(0)
if code == pingPermission {
want = -1
}
if !ok || got[0] != want || got[1] != enLockedSelf {
t.Fatalf("own lock of %s = %v (present %v)", code, got, ok)
}
}
// Another administrator's record carries no locks for the delegate.
other := env.seedAdmin(t, adminSeed{Name: "locks-other"})
if locks := permissionLocks(env.expect(t, http.StatusOK, http.MethodGet, userURL(other.ID), delegateToken, nil)); len(locks) != 0 {
t.Fatalf("locks on another record = %v", locks)
}
}
// reloadRoleByCode reads the role with code.
func (e *screensEnv) reloadRoleByCode(t *testing.T, code string) cabana.BackendUserRole {
t.Helper()
var role cabana.BackendUserRole
if err := e.gdb.Where("code = ?", code).First(&role).Error; err != nil {
t.Fatal(err)
}
return role
}