- TestTaglistFieldValidate: both modes, trim, dedupe, empty, null, wrong shapes, customTags false, required; TestTaglistFieldHydrate: stored NULL and bad text as [] - TestReorderFlatNodes: parent 422, duplicate and zero id 422, missing or extra 409, no depth or cycle check; TestReorderFlatCompile checks the localized list schema - TestReorderFlatSmoke/partitioned: flat reorder within one partition on Postgres - vitest: script markup as text, Backspace, blur, paste, datalist, customTags, string mode; flat ArrowUp/ArrowDown and the before/after drop zones - export_test.go wrappers for the taglist save check, hydration and node check - check-phase18.1.sh requires the new names in the go, admin and postgres stages
214 lines
8.5 KiB
Go
214 lines
8.5 KiB
Go
package cabana_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
|
"git.golem15.com/golem15/summercms/modules/towel"
|
|
)
|
|
|
|
// Smoke tests of the flat (WinterCMS Sortable) reorder (Phase 18.1 D-17),
|
|
// Docker-free: the inference rule and the flat flag. The Postgres routes are
|
|
// TestReorderFlatSmoke in catalog_smoke_test.go; TestReorderFlatNodes is the
|
|
// unit matrix of the save's node check (plan 18.1-06). No application names
|
|
// appear in framework tests.
|
|
|
|
// TestReorderFlatCompile pins the flat inference: a config_reorder.yaml
|
|
// without parentColumn on a record without a parent_id column compiles as
|
|
// flat and the list schema says so; maxDepth on it and an explicit
|
|
// parentColumn that is not a column fail boot; the tree fixture still
|
|
// compiles as a tree whose list schema carries no flat key.
|
|
func TestReorderFlatCompile(t *testing.T) {
|
|
const cfg = "controllers/badges/config_reorder.yaml"
|
|
list, err := cabana.CompiledListSchema([]party.Plugin{catalogFSPlugin{fsys: catalogOverlay(t, nil)}}, "acme.catalog.badges")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list.Reorder == nil || !list.Reorder.Flat || list.Reorder.Title != "Reorder badges" {
|
|
t.Fatalf("badges reorder = %+v", list.Reorder)
|
|
}
|
|
raw, err := json.Marshal(list.Reorder)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(raw), `"flat":true`) {
|
|
t.Fatalf("flat list schema JSON: %s", raw)
|
|
}
|
|
if err := activateCatalog(t, catalogFSPlugin{fsys: catalogOverlay(t, nil)}); err != nil {
|
|
t.Fatalf("catalog boot: %v", err)
|
|
}
|
|
|
|
src := catalogFile(t, cfg)
|
|
for _, tc := range []struct {
|
|
name, add string
|
|
want []string
|
|
}{
|
|
{"maxDepth on a flat reorder", "maxDepth: 2\n", []string{cfg, "maxDepth needs a tree reorder"}},
|
|
{"an explicit parentColumn that is not a column", "parentColumn: parent_id\n", []string{cfg, "parentColumn parent_id is not a column of the record"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := activateCatalog(t, catalogFSPlugin{fsys: catalogOverlay(t, map[string]string{cfg: src + tc.add})})
|
|
if err == nil {
|
|
t.Fatal("boot accepted the reorder config")
|
|
}
|
|
for _, part := range tc.want {
|
|
if !strings.Contains(err.Error(), part) {
|
|
t.Fatalf("err = %v, want %q", err, part)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// The localized list schema (GET .../schema/list) carries the flat flag
|
|
// too, not only the compiled one (deferred defect from plan 18.1-04).
|
|
t.Run("the localized list schema keeps flat", func(t *testing.T) {
|
|
app := backpack.New(nil)
|
|
if err := phrasebook.Activate[party.Plugin](app, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr, ok := app.Lookup[*phrasebook.Translator]()
|
|
if !ok {
|
|
t.Fatal("translator missing")
|
|
}
|
|
view, err := list.Localize(towel.WithLocale(context.Background(), "en"), tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if view.Reorder == nil || !view.Reorder.Flat || view.Reorder.Title != "Reorder badges" {
|
|
t.Fatalf("localized reorder = %+v", view.Reorder)
|
|
}
|
|
raw, err := json.Marshal(view)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(raw), `"reorder":{"title":"Reorder badges","flat":true}`) {
|
|
t.Fatalf("localized list schema JSON lacks flat: %s", raw)
|
|
}
|
|
})
|
|
|
|
t.Run("the tree fixture stays a tree", func(t *testing.T) {
|
|
tree, err := cabana.CompiledListSchema([]party.Plugin{companionsFSPlugin{fsys: companionsOverlay(t, nil)}}, "acme.companions.links")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tree.Reorder == nil || tree.Reorder.Flat {
|
|
t.Fatalf("links reorder = %+v", tree.Reorder)
|
|
}
|
|
raw, err := json.Marshal(tree)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), `"flat"`) {
|
|
t.Fatalf("tree list schema carries a flat key: %s", raw)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestReorderFlatNodes is the unit matrix of the reorder save's node check
|
|
// on a flat reorder (D-17), Docker-free: any non-null parent is a 422 on
|
|
// nodes with the flat_parent message, a duplicate id or a zero id is a 422,
|
|
// a missing or an extra node is the 409 stale set, and a valid set in any
|
|
// order passes with no depth or cycle evaluation. The same node lists on a
|
|
// tree reorder show what flat mode changes. TestReorderFlatSmoke drives the
|
|
// rules over HTTP on Postgres.
|
|
func TestReorderFlatNodes(t *testing.T) {
|
|
const flatParent = "A flat reorder has no parents."
|
|
locked := []cabana.AdminReorderNode{{ID: 1, SortOrder: 1}, {ID: 2, SortOrder: 2}, {ID: 3, SortOrder: 3}}
|
|
node := func(id uint64, parent *uint64, sort int64) cabana.AdminReorderNodeInput {
|
|
return cabana.AdminReorderNodeInput{ID: id, ParentID: parent, SortOrder: sort}
|
|
}
|
|
ptr := func(v uint64) *uint64 { return &v }
|
|
nodesError := func(t *testing.T, err error, want string) {
|
|
t.Helper()
|
|
var ve *cabana.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("err = %v, want a 422 validation error", err)
|
|
}
|
|
msgs, _ := ve.Details["nodes"].([]string)
|
|
if len(msgs) != 1 || msgs[0] != want || len(ve.Details) != 1 {
|
|
t.Fatalf("details = %#v, want nodes: %q", ve.Details, want)
|
|
}
|
|
}
|
|
|
|
t.Run("a reversed set passes", func(t *testing.T) {
|
|
nodes := []cabana.AdminReorderNodeInput{node(3, nil, 1), node(2, nil, 2), node(1, nil, 3)}
|
|
if err := cabana.CheckReorderNodes(true, 0, locked, nodes, flatParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
t.Run("equal and negative sort values pass", func(t *testing.T) {
|
|
nodes := []cabana.AdminReorderNodeInput{node(1, nil, -5), node(2, nil, 0), node(3, nil, 0)}
|
|
if err := cabana.CheckReorderNodes(true, 0, locked, nodes, flatParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
t.Run("a long flat list has no depth or cycle check", func(t *testing.T) {
|
|
var many []cabana.AdminReorderNode
|
|
var nodes []cabana.AdminReorderNodeInput
|
|
for i := uint(1); i <= 200; i++ {
|
|
many = append(many, cabana.AdminReorderNode{ID: i})
|
|
nodes = append(nodes, node(uint64(201-i), nil, int64(i)))
|
|
}
|
|
if err := cabana.CheckReorderNodes(true, 1, many, nodes, flatParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
t.Run("an empty set over no records passes", func(t *testing.T) {
|
|
if err := cabana.CheckReorderNodes(true, 0, nil, nil, flatParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
nodes []cabana.AdminReorderNodeInput
|
|
want string
|
|
}{
|
|
{"a parent inside the set", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, ptr(1), 2), node(3, nil, 3)}, flatParent},
|
|
{"a parent outside the set", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2), node(3, ptr(99), 3)}, flatParent},
|
|
{"a parent of zero", []cabana.AdminReorderNodeInput{node(1, ptr(0), 1), node(2, nil, 2), node(3, nil, 3)}, flatParent},
|
|
{"its own parent", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2), node(3, ptr(3), 3)}, flatParent},
|
|
{"a duplicate id", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2), node(2, nil, 3)}, "The nodes field lists a record twice."},
|
|
{"a zero id", []cabana.AdminReorderNodeInput{node(0, nil, 1), node(2, nil, 2), node(3, nil, 3)}, "The nodes field must list record ids."},
|
|
} {
|
|
t.Run(tc.name+" is a 422", func(t *testing.T) {
|
|
nodesError(t, cabana.CheckReorderNodes(true, 0, locked, tc.nodes, flatParent), tc.want)
|
|
})
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
nodes []cabana.AdminReorderNodeInput
|
|
}{
|
|
{"a missing node", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2)}},
|
|
{"an extra node", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2), node(3, nil, 3), node(4, nil, 4)}},
|
|
{"a node swapped for another", []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, nil, 2), node(4, nil, 3)}},
|
|
{"no nodes", nil},
|
|
} {
|
|
t.Run(tc.name+" is the 409 stale set", func(t *testing.T) {
|
|
err := cabana.CheckReorderNodes(true, 0, locked, tc.nodes, flatParent)
|
|
if !cabana.IsReorderStale(err) {
|
|
t.Fatalf("err = %v, want the stale set", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("the same parent is accepted on a tree", func(t *testing.T) {
|
|
nodes := []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, ptr(1), 2), node(3, nil, 3)}
|
|
if err := cabana.CheckReorderNodes(false, 0, locked, nodes, flatParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cycle := []cabana.AdminReorderNodeInput{node(1, ptr(2), 1), node(2, ptr(1), 2), node(3, nil, 3)}
|
|
nodesError(t, cabana.CheckReorderNodes(false, 0, locked, cycle, flatParent), "The tree has a cycle.")
|
|
deep := []cabana.AdminReorderNodeInput{node(1, nil, 1), node(2, ptr(1), 2), node(3, ptr(2), 3)}
|
|
nodesError(t, cabana.CheckReorderNodes(false, 2, locked, deep, flatParent), "The tree may be at most 2 levels deep.")
|
|
})
|
|
}
|