Files
summercms/modules/cabana/field_trigger_test.go
Jakub Zych 085179b676 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
2026-10-22 12:11:57 +02:00

116 lines
6.0 KiB
Go

package cabana
import (
"context"
"strings"
"testing"
"testing/fstest"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestFieldTriggerBoot (D-21): every refusal of compileTrigger and
// checkTriggers stops boot with an error naming the controller, the field
// and the trigger key; valid disable and hide triggers reach the localized
// schema unchanged.
func TestFieldTriggerBoot(t *testing.T) {
base := "fields:\n is_admin:\n type: switch\n agree:\n type: checkbox\n title:\n type: text\n"
notes := func(trigger string) string {
return base + " notes:\n type: textarea\n trigger:\n" + trigger
}
full := func(action, field, condition string) string {
return notes(" action: " + action + "\n field: " + field + "\n condition: " + condition + "\n")
}
for _, tc := range []struct {
name, fields string
want []string
}{
{"missing action", notes(" field: is_admin\n condition: checked\n"), []string{"action is required"}},
{"missing field", notes(" action: hide\n condition: checked\n"), []string{"field is required"}},
{"missing condition", notes(" action: hide\n field: is_admin\n"), []string{"condition is required"}},
{"extra sub-key", notes(" action: hide\n field: is_admin\n condition: checked\n value: 1\n"), []string{"unknown key value"}},
{"not a mapping", base + " notes:\n type: textarea\n trigger: is_admin\n", []string{"trigger must be a mapping"}},
{"unknown action", full("show", "is_admin", "checked"), []string{"action show is not supported"}},
{"unknown condition", full("hide", "is_admin", "value[1]"), []string{"condition value[1] is not supported"}},
{"field is not a name", full("hide", "is-admin", "checked"), []string{`field "is-admin" is not a field name`}},
{"self reference", base + " notes:\n type: checkbox\n trigger:\n action: hide\n field: notes\n condition: checked\n", []string{"names the field itself"}},
{"unknown controlling field", full("hide", "active", "checked"), []string{"trigger field active is not a field of this form"}},
{"text controlling field", full("disable", "title", "unchecked"), []string{"trigger field title must be a switch or checkbox", "type text"}},
{"in a repeater row", base + " rows:\n type: repeater\n form:\n fields:\n flag:\n type: checkbox\n notes:\n type: text\n trigger:\n action: hide\n field: flag\n condition: checked\n", []string{"trigger is not supported in a repeater row"}},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := compileTriggerForm(tc.fields)
if err == nil {
t.Fatalf("compiled; want an error containing %v", tc.want)
}
msg := err.Error()
for _, want := range append([]string{"acme.trig.items", "notes", "trigger"}, tc.want...) {
if !strings.Contains(msg, want) {
t.Fatalf("err = %v, want it to contain %q", err, want)
}
}
})
}
t.Run("duplicate sub-key", func(t *testing.T) {
// The YAML decoder refuses a duplicate key before compileTrigger.
_, err := compileTriggerForm(notes(" action: hide\n action: disable\n field: is_admin\n condition: checked\n"))
if err == nil || !strings.Contains(err.Error(), "acme.trig.items") || !strings.Contains(err.Error(), `"action" already defined`) {
t.Fatalf("duplicate sub-key = %v", err)
}
})
t.Run("valid triggers reach the localized schema", func(t *testing.T) {
fields := base +
" notes:\n type: textarea\n trigger:\n action: disable\n field: is_admin\n condition: checked\n" +
" summary:\n type: text\n trigger:\n action: hide\n field: agree\n condition: unchecked\n"
schema, err := compileTriggerForm(fields)
if err != nil {
t.Fatal(err)
}
view, err := schema.Localize(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
want := map[string]FieldTrigger{
"notes": {Action: "disable", Field: "is_admin", Condition: "checked"},
"summary": {Action: "hide", Field: "agree", Condition: "unchecked"},
}
for _, field := range view.Fields {
expected, ok := want[field.Name]
switch {
case ok && (field.Trigger == nil || *field.Trigger != expected):
t.Fatalf("%s trigger = %+v, want %+v", field.Name, field.Trigger, expected)
case !ok && field.Trigger != nil:
t.Fatalf("%s has trigger %+v", field.Name, field.Trigger)
}
delete(want, field.Name)
}
if len(want) != 0 {
t.Fatalf("fields missing from the view: %v", want)
}
})
t.Run("a settings form refuses trigger at boot", func(t *testing.T) {
fsys := fstest.MapFS{"models/settings/fields.yaml": &fstest.MapFile{Data: []byte(
"fields:\n enabled:\n type: switch\n extra:\n type: text\n trigger:\n action: hide\n field: enabled\n condition: checked\n")}}
item := pact.SettingsItem{Code: "demo", Label: "Demo", Form: "models/settings/fields.yaml", Model: "DemoSettings", NewModel: func() any { return &ucSettings{} }}
_, err := compileSetting("acme.unit", item, fsys)
if err == nil || !strings.Contains(err.Error(), "extra") || !strings.Contains(err.Error(), "trigger") {
t.Fatalf("settings form with a trigger = %v", err)
}
})
t.Run("a relation form refuses trigger", func(t *testing.T) {
field := FormField{Name: "notes", Type: "text", Trigger: &FieldTrigger{Action: "hide", Field: "flag", Condition: "checked"}}
if err := controllerFormOnly(field); err == nil || !strings.Contains(err.Error(), "trigger") {
t.Fatalf("controllerFormOnly = %v", err)
}
field.Trigger = nil
if err := controllerFormOnly(field); err != nil {
t.Fatalf("controllerFormOnly without a trigger = %v", err)
}
})
}