- 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
162 lines
6.0 KiB
Go
162 lines
6.0 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|