Files
summercms/modules/cabana/record_locks_smoke_test.go
Jakub Zych 705f94d9e5 feat(19.6.1-02): per-record locked permission values
- cabana.PermissionRecordLock and the optional PermissionRecordLockProvider
- RecordMeta.PermissionLocks (meta.permissionLocks) on show and save responses, notes localized
- save rule: a record-locked code keeps its stored value, a differing present value is 403
- Roles lock code-declared grants (locked_by_code); Administrators lock every override on your own record (locked_self)
- SPA: FORM_PERMISSION_LOCKS, locked codes never sent, record-locked rows in the permission editor
- regenerated admin OpenAPI and schema.d.ts; docs and README
2026-10-22 11:29:25 +02:00

156 lines
5.7 KiB
Go

package cabana
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
)
// lockRecord is the in-test record: its stored permissions.
type lockRecord struct {
ID uint
Permissions map[string]int
}
// lockEditor offers a.x and a.y; withLocks also record-locks a.x at 1.
type lockEditor struct {
withLocks bool
locked bool // a.x also request-Locked
stored map[string]int
}
func (lockEditor) ID() string { return "acme.locks.items" }
func (lockEditor) ModelName() string { return `Acme\Locks\Models\Item` }
func (lockEditor) ConfigDir() string { return "controllers/items" }
func (c *lockEditor) AdminPermissionOptions(context.Context, string) ([]PermissionOption, error) {
return []PermissionOption{{Code: "a.x", Label: "X", Locked: c.locked}, {Code: "a.y", Label: "Y"}}, nil
}
func (c *lockEditor) AdminPermissionValues(_ context.Context, _ string, record any) (map[string]int, error) {
r := record.(*lockRecord)
out := map[string]int{}
for code, n := range r.Permissions {
out[code] = n
}
return out, nil
}
func (c *lockEditor) AdminSetPermissionValues(_ context.Context, _ string, record any, values map[string]int) error {
c.stored = values
record.(*lockRecord).Permissions = values
return nil
}
// lockingEditor adds the record-lock provider.
type lockingEditor struct{ *lockEditor }
func (lockingEditor) AdminPermissionRecordLocks(context.Context, string, any) ([]PermissionRecordLock, error) {
return []PermissionRecordLock{{Code: "a.x", Value: 1, Note: "backend::lang.permissioneditor.locked_by_code"}}, nil
}
func lockController(c *lockEditor, locks bool) *CompiledController {
cc := &CompiledController{
Form: &FormSchema{Fields: []FormField{{Name: "permissions", Type: permissionFieldType, Mode: "checkbox"}}},
permissions: map[string]string{"permissions": "checkbox"},
}
if locks {
cc.Controller = lockingEditor{c}
} else {
cc.Controller = c
}
return cc
}
func TestPermissionRecordLocksSmoke(t *testing.T) {
ctx := context.Background()
lift := func(values map[string]int) []permissionValue {
return []permissionValue{{field: "permissions", mode: "checkbox", values: values}}
}
t.Run("absent record-locked code keeps the stored value", func(t *testing.T) {
editor := &lockEditor{}
record := &lockRecord{ID: 1, Permissions: map[string]int{"a.y": 1}}
if err := applyPermissionValues(ctx, lockController(editor, true), record, lift(map[string]int{"a.y": 1}), true); err != nil {
t.Fatal(err)
}
if _, ok := editor.stored["a.x"]; ok || editor.stored["a.y"] != 1 || len(editor.stored) != 1 {
t.Fatalf("stored = %v, want only a.y: 1 (a.x unchanged, absent)", editor.stored)
}
})
t.Run("record-locked code that differs from the stored value is refused", func(t *testing.T) {
editor := &lockEditor{}
record := &lockRecord{ID: 1, Permissions: map[string]int{"a.y": 1}}
err := applyPermissionValues(ctx, lockController(editor, true), record, lift(map[string]int{"a.x": 1, "a.y": 1}), true)
var forbidden *ForbiddenError
if !errors.As(err, &forbidden) || forbidden.Details["permissions"] == nil {
t.Fatalf("err = %v, want ForbiddenError on permissions", err)
}
if editor.stored != nil {
t.Fatalf("stored = %v after a refusal", editor.stored)
}
})
t.Run("a body equal to the stored values passes", func(t *testing.T) {
editor := &lockEditor{}
record := &lockRecord{ID: 1, Permissions: map[string]int{"a.x": 1, "a.y": 1}}
if err := applyPermissionValues(ctx, lockController(editor, true), record, lift(map[string]int{"a.x": 1, "a.y": 1}), true); err != nil {
t.Fatal(err)
}
if editor.stored["a.x"] != 1 || editor.stored["a.y"] != 1 {
t.Fatalf("stored = %v", editor.stored)
}
})
t.Run("request-locked and record-locked code absent from the body is not a 403", func(t *testing.T) {
editor := &lockEditor{locked: true}
record := &lockRecord{ID: 1, Permissions: map[string]int{}}
if err := applyPermissionValues(ctx, lockController(editor, true), record, lift(map[string]int{"a.y": 1}), true); err != nil {
t.Fatalf("err = %v, want nil", err)
}
// Without the record lock the same body changes nothing on a.x
// either: a.x is stored absent and submitted absent.
if err := applyPermissionValues(ctx, lockController(&lockEditor{locked: true}, false), &lockRecord{ID: 1, Permissions: map[string]int{"a.x": 1}}, lift(map[string]int{"a.y": 1}), true); err == nil {
t.Fatal("request-locked a.x removed without a record lock: want 403")
}
})
t.Run("record-locked codes are not asked on create", func(t *testing.T) {
editor := &lockEditor{}
if err := applyPermissionValues(ctx, lockController(editor, true), &lockRecord{}, lift(map[string]int{"a.x": 1}), false); err != nil {
t.Fatal(err)
}
if editor.stored["a.x"] != 1 {
t.Fatalf("stored = %v, want a.x on create", editor.stored)
}
})
t.Run("record meta carries permissionLocks only with a provider", func(t *testing.T) {
record := &lockRecord{ID: 7, Permissions: map[string]int{"a.y": 1}}
result, err := projectFullRecord(ctx, nil, lockController(&lockEditor{}, true), record)
if err != nil {
t.Fatal(err)
}
locks := result.Meta.PermissionLocks["permissions"]
if len(locks) != 1 || locks[0].Code != "a.x" || locks[0].Value != 1 || locks[0].Note == "" {
t.Fatalf("permissionLocks = %+v", result.Meta.PermissionLocks)
}
raw, _ := json.Marshal(result.Meta)
if !strings.Contains(string(raw), `"permissionLocks":{"permissions":[{"code":"a.x","value":1,"note":`) {
t.Fatalf("meta JSON = %s", raw)
}
plain, err := projectFullRecord(ctx, nil, lockController(&lockEditor{}, false), record)
if err != nil {
t.Fatal(err)
}
raw, _ = json.Marshal(plain.Meta)
if strings.Contains(string(raw), "permissionLocks") {
t.Fatalf("meta JSON without a provider = %s", raw)
}
})
}