package cabana import ( "context" "encoding/json" "errors" "strings" "testing" ) // modeLockEditor is a permission editor in mode radio or checkbox over a.x // and a.y. With locks, a stored record (ID set) has a.x record-locked at // lockValue; a new record has no locks, as cabana's own providers do. type modeLockEditor struct { requestLocked bool lockValue int stored map[string]int asked int } func (*modeLockEditor) ID() string { return "acme.locks.items" } func (*modeLockEditor) ModelName() string { return `Acme\Locks\Models\Item` } func (*modeLockEditor) ConfigDir() string { return "controllers/items" } func (c *modeLockEditor) AdminPermissionOptions(context.Context, string) ([]PermissionOption, error) { return []PermissionOption{{Code: "a.x", Label: "X", Locked: c.requestLocked}, {Code: "a.y", Label: "Y"}}, nil } func (c *modeLockEditor) AdminPermissionValues(_ context.Context, _ string, record any) (map[string]int, error) { out := map[string]int{} for code, n := range record.(*lockRecord).Permissions { out[code] = n } return out, nil } func (c *modeLockEditor) AdminSetPermissionValues(_ context.Context, _ string, record any, values map[string]int) error { c.stored = values record.(*lockRecord).Permissions = values return nil } // modeLocking adds the record-lock provider. type modeLocking struct{ *modeLockEditor } func (c modeLocking) AdminPermissionRecordLocks(_ context.Context, _ string, record any) ([]PermissionRecordLock, error) { c.asked++ if record.(*lockRecord).ID == 0 { return nil, nil } return []PermissionRecordLock{{Code: "a.x", Value: c.lockValue, Note: permissionLockedByCodeKey}}, nil } func modeController(c *modeLockEditor, mode string, locks bool) *CompiledController { cc := &CompiledController{ Form: &FormSchema{Fields: []FormField{{Name: "permissions", Type: permissionFieldType, Mode: mode}}}, permissions: map[string]string{"permissions": mode}, } if locks { cc.Controller = modeLocking{c} } else { cc.Controller = c } return cc } // TestPermissionRecordLocks (D-21): the record-lock save rule in both editor // modes, and meta.permissionLocks only for a stored record of a controller // with the provider. func TestPermissionRecordLocks(t *testing.T) { ctx := context.Background() for _, mode := range []struct { name string lockValue int other int }{{"checkbox", 1, 0}, {"radio", -1, 1}} { lift := func(values map[string]int) []permissionValue { return []permissionValue{{field: "permissions", mode: mode.name, values: values}} } stored := func() *lockRecord { return &lockRecord{ID: 3, Permissions: map[string]int{"a.x": mode.lockValue, "a.y": 1}} } t.Run(mode.name, func(t *testing.T) { for _, tc := range []struct { name string requestLocked bool body map[string]int refused bool want map[string]int }{ {name: "absent locked code keeps the stored value", body: map[string]int{"a.y": 1}, want: map[string]int{"a.x": mode.lockValue, "a.y": 1}}, {name: "an equal locked value passes", body: map[string]int{"a.x": mode.lockValue}, want: map[string]int{"a.x": mode.lockValue}}, {name: "a different locked value is refused", body: map[string]int{"a.x": mode.other, "a.y": 1}, refused: true}, {name: "request-locked and record-locked, absent", requestLocked: true, body: map[string]int{"a.y": 1}, want: map[string]int{"a.x": mode.lockValue, "a.y": 1}}, {name: "request-locked and record-locked, different", requestLocked: true, body: map[string]int{"a.x": mode.other}, refused: true}, } { t.Run(tc.name, func(t *testing.T) { editor := &modeLockEditor{requestLocked: tc.requestLocked, lockValue: mode.lockValue} record := stored() err := applyPermissionValues(ctx, modeController(editor, mode.name, true), record, lift(tc.body), true) if tc.refused { var forbidden *ForbiddenError if !errors.As(err, &forbidden) || forbidden.Details["permissions"] == nil { t.Fatalf("err = %v, want a ForbiddenError on permissions", err) } if got, _ := forbidden.Details["permissions"].([]string); len(got) != 1 || got[0] != permissionLockedKey { t.Fatalf("details = %v", forbidden.Details) } if editor.stored != nil { t.Fatalf("stored %v after a refusal", editor.stored) } return } if err != nil { t.Fatal(err) } if len(editor.stored) != len(tc.want) { t.Fatalf("stored = %v, want %v", editor.stored, tc.want) } for code, n := range tc.want { if editor.stored[code] != n { t.Fatalf("stored = %v, want %v", editor.stored, tc.want) } } }) } t.Run("record locks are not asked on create", func(t *testing.T) { editor := &modeLockEditor{lockValue: mode.lockValue} locking := modeController(editor, mode.name, true) if err := applyPermissionValues(ctx, locking, &lockRecord{}, lift(map[string]int{"a.x": mode.other}), false); err != nil { t.Fatal(err) } if editor.asked != 0 { t.Fatalf("locks asked %d times on create", editor.asked) } }) }) } t.Run("meta carries permissionLocks only for a stored record with a provider", func(t *testing.T) { for _, tc := range []struct { name string locks bool id uint expect bool }{{"provider, stored record", true, 3, true}, {"provider, new record", true, 0, false}, {"no provider", false, 3, false}} { editor := &modeLockEditor{lockValue: -1} record := &lockRecord{ID: tc.id, Permissions: map[string]int{"a.x": -1}} result, err := projectFullRecord(ctx, nil, modeController(editor, "radio", tc.locks), record) if err != nil { t.Fatalf("%s: %v", tc.name, err) } raw, _ := json.Marshal(result.Meta) has := strings.Contains(string(raw), `"permissionLocks":{"permissions":[{"code":"a.x","value":-1,"note":"backend::lang.permissioneditor.locked_by_code"}]}`) if has != tc.expect || (!tc.expect && strings.Contains(string(raw), "permissionLocks")) { t.Fatalf("%s: meta JSON = %s", tc.name, raw) } } }) }