- signpost: activation refusals, host overrides, D-02 pattern layers incl. PatternFrom, ReferenceOptions depths - seaglass: removal report, bluemonday bypass corpus, Check - pact Option.Depth omitempty and the menu item type contracts; party activates menu item types after Register and before the first Boot - cabana: repeater compile and decode, dependsOn compile and recheck, richeditor keys and notices, reorder and singleton boot checks (Docker-free); repeater save, dependent recheck, richeditor save, reorder and singleton routes on Postgres; conformance cases for the documented 404, 409 and 422 answers of the new routes - admin: richeditor value contract, link popover, notices and chunk failure; repeater bounds, remove and reorder, collapsed rows; dependent dropdown states and last request wins; reorder announcements, partition switch and tree rules; SingletonForm spec - gate: required names in the go, admin and postgres stages
223 lines
10 KiB
Go
223 lines
10 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// TestDependentDropdownCompile pins D-08's compile rules for dependsOn: a
|
|
// dropdown only, no static options, a sibling of the same form (or repeater
|
|
// row) with a scalar value, never itself, and a controller that implements
|
|
// pact.DependentOptionsProvider.
|
|
func TestDependentDropdownCompile(t *testing.T) {
|
|
t.Run("a valid dependent dropdown compiles", func(t *testing.T) {
|
|
cc := ucMustCompile(t, ucFields+ucRepeater(""), nil)
|
|
if ref := ucField(t, cc, "reference"); ref.DependsOn != "type" || len(ref.Options) != 0 {
|
|
t.Fatalf("reference = %+v", ref)
|
|
}
|
|
if !usesDependsOn(cc.Form.Fields) {
|
|
t.Fatal("usesDependsOn = false")
|
|
}
|
|
})
|
|
|
|
base := "fields:\n title:\n type: text\n type:\n type: dropdown\n options: typeOptions\n"
|
|
for _, tc := range []struct {
|
|
name, fields string
|
|
ctl pact.AdminController
|
|
want string
|
|
}{
|
|
{"on a text field", base + " reference:\n type: text\n dependsOn: type\n", nil, "dependsOn is only valid on type: dropdown"},
|
|
{"on a textarea", base + " reference:\n type: textarea\n dependsOn: type\n", nil, "dependsOn is only valid on type: dropdown"},
|
|
{"with static options", base + " reference:\n type: dropdown\n dependsOn: type\n options: typeOptions\n", nil, "a dropdown with dependsOn takes no options"},
|
|
{"not a field name", base + " reference:\n type: dropdown\n dependsOn: \"a b\"\n", nil, `dependsOn "a b" is not a field name`},
|
|
{"names itself", base + " reference:\n type: dropdown\n dependsOn: reference\n", nil, "field reference: dependsOn names the field itself"},
|
|
{"unknown sibling", base + " reference:\n type: dropdown\n dependsOn: kind\n", nil, "field reference: dependsOn kind is not a field of this form"},
|
|
{"sibling without a scalar value", base + ucRepeater("") + " pick:\n type: dropdown\n dependsOn: rows\n", nil, "field pick: dependsOn rows must be a field with a scalar value, not type repeater"},
|
|
{"no provider", ucFields, ucPlainLinks{}, "field reference: dependsOn needs controller acme.unit.links to implement pact.DependentOptionsProvider"},
|
|
{"no provider for a nested dropdown", base + ucRepeater(""), ucPlainLinks{}, "field rows.reference: dependsOn needs controller acme.unit.links to implement pact.DependentOptionsProvider"},
|
|
{"nested unknown sibling", base + " rows:\n type: repeater\n form:\n fields:\n reference:\n type: dropdown\n dependsOn: title\n", nil, "field rows.reference: dependsOn title is not a field of the same repeater row"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := ucCompile(tc.fields, tc.ctl)
|
|
wantErr(t, err, tc.want)
|
|
})
|
|
}
|
|
|
|
t.Run("a controller without dependsOn needs no provider", func(t *testing.T) {
|
|
if _, err := ucCompile(base, ucPlainLinks{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("dependent fields are found by path", func(t *testing.T) {
|
|
cc := ucMustCompile(t, ucFields+ucRepeater(""), nil)
|
|
for _, tc := range []struct {
|
|
path, want string
|
|
ok bool
|
|
}{
|
|
{"reference", "reference", true},
|
|
{"rows.reference", "rows.reference", true},
|
|
{"title", "", false},
|
|
{"type", "", false},
|
|
{"rows.title", "", false},
|
|
{"rows.missing", "", false},
|
|
{"rows", "", false},
|
|
{"title.reference", "", false},
|
|
{"missing", "", false},
|
|
} {
|
|
field, ok := dependentField(cc, tc.path)
|
|
if ok != tc.ok || (ok && (field.Name != tc.want || field.DependsOn != "type")) {
|
|
t.Fatalf("dependentField(%q) = %+v, %v", tc.path, field, ok)
|
|
}
|
|
}
|
|
if _, ok := dependentField(nil, "reference"); ok {
|
|
t.Fatal("nil controller answered")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestDependentValues pins the Docker-free half of the D-08 save recheck:
|
|
// sibling values are normalized the same way for the options route and a
|
|
// save, null is always accepted, a value outside the options is a 422 on the
|
|
// field (field.index.nested in a row), a type that takes no reference stores
|
|
// null, the stored sibling or value is used when only the other is written,
|
|
// and the provider is asked once per path and value.
|
|
func TestDependentValues(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
in any
|
|
want any
|
|
}{
|
|
{nil, nil}, {"", nil}, {"x", "x"}, {true, "true"}, {false, "false"},
|
|
{json.Number("12"), "12"}, {float64(2), "2"}, {2.5, "2.5"}, {3, "3"}, {int64(4), "4"}, {uint(5), "5"},
|
|
{[]any{1}, "[1]"},
|
|
} {
|
|
if got := dependentValue(tc.in); got != tc.want {
|
|
t.Fatalf("dependentValue(%#v) = %#v, want %#v", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
set := pact.DependentOptionSet{Applies: true, Options: []pact.Option{{Value: "1"}, {Value: "2"}}}
|
|
for _, tc := range []struct {
|
|
value any
|
|
ok bool
|
|
}{{nil, true}, {"", true}, {"1", true}, {float64(2), true}, {json.Number("2"), true}, {"3", false}, {"01", false}, {true, false}} {
|
|
if got := amongOptions(set, tc.value); got != tc.ok {
|
|
t.Fatalf("amongOptions(%#v) = %v", tc.value, got)
|
|
}
|
|
}
|
|
|
|
page, list := "acme-page", "acme-list"
|
|
stale := "99"
|
|
two := "2"
|
|
check := func(t *testing.T, target *ucLink, projected map[string]any) (map[string]any, []string, error) {
|
|
t.Helper()
|
|
var calls []string
|
|
cc := ucMustCompile(t, ucFields+ucRepeater(""), ucLinks{calls: &calls})
|
|
err := checkDependentValues(context.Background(), cc, nil, target, projected, "update")
|
|
var invalid *ValidationError
|
|
if errors.As(err, &invalid) {
|
|
return invalid.Details, calls, nil
|
|
}
|
|
return nil, calls, err
|
|
}
|
|
|
|
t.Run("top level", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
target ucLink
|
|
projected map[string]any
|
|
wantInvalid bool
|
|
wantReference any
|
|
wantCalls string
|
|
}{
|
|
{"a value among the options", ucLink{}, map[string]any{"type": page, "reference": "2"}, false, "2", "[reference=map[type:acme-page]]"},
|
|
{"a number value is compared as text", ucLink{}, map[string]any{"type": page, "reference": float64(1)}, false, float64(1), "[reference=map[type:acme-page]]"},
|
|
{"null is accepted", ucLink{}, map[string]any{"type": page, "reference": nil}, false, nil, "[reference=map[type:acme-page]]"},
|
|
{"a value outside the options", ucLink{}, map[string]any{"type": page, "reference": "99"}, true, "99", "[reference=map[type:acme-page]]"},
|
|
{"a type without references stores null", ucLink{}, map[string]any{"type": list, "reference": "1"}, false, nil, "[reference=map[type:acme-list]]"},
|
|
{"an empty type stores null", ucLink{}, map[string]any{"type": "", "reference": "1"}, false, nil, "[reference=map[type:<nil>]]"},
|
|
{"only the value: the stored sibling decides", ucLink{Type: &page}, map[string]any{"reference": "99"}, true, "99", "[reference=map[type:acme-page]]"},
|
|
{"only the value, stored sibling accepts it", ucLink{Type: &page}, map[string]any{"reference": "1"}, false, "1", "[reference=map[type:acme-page]]"},
|
|
{"only the sibling: the stored value is checked", ucLink{Reference: &stale}, map[string]any{"type": page}, true, nil, "[reference=map[type:acme-page]]"},
|
|
{"only the sibling, stored value still valid", ucLink{Reference: &two}, map[string]any{"type": page}, false, nil, "[reference=map[type:acme-page]]"},
|
|
{"only the sibling to a type without references clears the value", ucLink{Reference: &two}, map[string]any{"type": list}, false, nil, "[reference=map[type:acme-list]]"},
|
|
{"neither written asks nothing", ucLink{Type: &page, Reference: &stale}, map[string]any{"title": "x"}, false, nil, "[]"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
target := tc.target
|
|
details, calls, err := check(t, &target, tc.projected)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if (details != nil) != tc.wantInvalid {
|
|
t.Fatalf("details = %v, want invalid %v", details, tc.wantInvalid)
|
|
}
|
|
if tc.wantInvalid {
|
|
if msgs, _ := details["reference"].([]string); len(msgs) != 1 || msgs[0] != msgDependentInvalid || len(details) != 1 {
|
|
t.Fatalf("details = %v", details)
|
|
}
|
|
}
|
|
if got, present := tc.projected["reference"]; present || tc.wantReference != nil {
|
|
if fmt.Sprint(got) != fmt.Sprint(tc.wantReference) {
|
|
t.Fatalf("projected reference = %#v, want %#v", got, tc.wantReference)
|
|
}
|
|
}
|
|
if strings.Contains(tc.name, "stores null") || strings.Contains(tc.name, "clears the value") {
|
|
if v, present := tc.projected["reference"]; !present || v != nil {
|
|
t.Fatalf("reference not written as null: %#v (present %v)", v, present)
|
|
}
|
|
}
|
|
if fmt.Sprint(calls) != tc.wantCalls {
|
|
t.Fatalf("provider calls = %v, want %s", calls, tc.wantCalls)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("repeater rows", func(t *testing.T) {
|
|
projected := map[string]any{"rows": []any{
|
|
map[string]any{"title": "a", "type": page, "reference": "1"},
|
|
map[string]any{"title": "b", "type": page, "reference": "99"},
|
|
map[string]any{"title": "c", "type": list, "reference": "1"},
|
|
map[string]any{"title": "d", "type": page, "reference": "2"},
|
|
"not a row",
|
|
}}
|
|
details, calls, err := check(t, &ucLink{}, projected)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msgs, _ := details["rows.1.reference"].([]string); len(msgs) != 1 || msgs[0] != msgDependentInvalid || len(details) != 1 {
|
|
t.Fatalf("details = %v", details)
|
|
}
|
|
rows := projected["rows"].([]any)
|
|
if rows[2].(map[string]any)["reference"] != nil || rows[0].(map[string]any)["reference"] != "1" {
|
|
t.Fatalf("rows = %v", rows)
|
|
}
|
|
// One call per distinct path and value: the second acme-page row
|
|
// and the fourth reuse the first answer.
|
|
if fmt.Sprint(calls) != "[rows.reference=map[type:acme-page] rows.reference=map[type:acme-list]]" {
|
|
t.Fatalf("provider calls = %v", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("a provider error is returned, not a 422", func(t *testing.T) {
|
|
broken := "acme-broken"
|
|
_, _, err := check(t, &ucLink{}, map[string]any{"type": broken, "reference": "1"})
|
|
if err == nil || !strings.Contains(err.Error(), "references failed") {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("a form without dependsOn asks nothing", func(t *testing.T) {
|
|
cc := ucMustCompile(t, "fields:\n title:\n type: text\n", ucPlainLinks{})
|
|
if err := checkDependentValues(context.Background(), cc, nil, &ucLink{}, map[string]any{"title": "x"}, "update"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|