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) } }) }