test(17-06): framework and admin SPA unit tests for the blog companions

- 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
This commit is contained in:
2026-10-12 19:29:37 +02:00
parent 490a4e3e6a
commit 91593da1f7
17 changed files with 3136 additions and 17 deletions

View File

@@ -223,6 +223,105 @@ func TestPhase10OpenAPIConformance(t *testing.T) {
t.FailNow()
}
}
// The documented error answers of the Phase 17 routes, after a fresh
// login (the last case logged out).
if rec := env.send(t, http.MethodPost, "/auth/login", map[string]string{"login": env.login, "password": adminTestPassword}, false); rec.Code != http.StatusOK {
t.Fatalf("re-login status=%d body=%s", rec.Code, rec.Body.String())
}
for _, tc := range companionsErrorCases() {
t.Run(fmt.Sprintf("%s %d %s", tc.key, tc.status, tc.name), func(t *testing.T) {
rec := tc.call(t, env)
if rec.Code != tc.status {
t.Fatalf("status=%d want %d body=%s", rec.Code, tc.status, rec.Body.String())
}
dec := json.NewDecoder(bytes.NewReader(rec.Body.Bytes()))
dec.DisallowUnknownFields()
var body cabana.ErrorEnvelope
if err := dec.Decode(&body); err != nil || body.Error.Code == "" {
t.Fatalf("body is not an ErrorEnvelope: %v\n%s", err, rec.Body.String())
}
if tc.code != "" && body.Error.Code != tc.code {
t.Fatalf("error code = %q, want %q", body.Error.Code, tc.code)
}
method, path, _ := strings.Cut(tc.key, " ")
if got := spec.ref(path, strings.ToLower(method), fmt.Sprint(tc.status)); got != "cabana.ErrorEnvelope" {
t.Fatalf("admin.json documents %q for %s %d, the handler writes cabana.ErrorEnvelope", got, tc.key, tc.status)
}
})
}
}
// companionsErrorCase is one documented error answer of a Phase 17 route.
type companionsErrorCase struct {
key string
status int
name string
code string
call func(t *testing.T, env *conformEnv) *httptest.ResponseRecorder
}
// companionsErrorCases are the 404, 409 and 422 answers the dependent
// options, reorder and singleton routes document, and the 404 of the list,
// create and delete routes on a singleton controller.
func companionsErrorCases() []companionsErrorCase {
partitionLinks := func(t *testing.T, e *conformEnv) (uint, []uint) {
t.Helper()
group := companionsGroup{Title: "Errors " + e.stamp}
if err := e.db.Create(&group).Error; err != nil {
t.Fatal(err)
}
var ids []uint
for i := range 2 {
link := companionsLink{Title: fmt.Sprintf("err-%d-%s", i, e.stamp), GroupID: &group.ID, SortOrder: i + 1}
if err := e.db.Create(&link).Error; err != nil {
t.Fatal(err)
}
ids = append(ids, link.ID)
}
return group.ID, ids
}
profileID := func(t *testing.T, e *conformEnv) uint {
t.Helper()
return dataID(t, e.send(t, http.MethodGet, "/acme/companions/profiles/singleton", nil, true).Body.Bytes())
}
return []companionsErrorCase{
{"GET /{vendor}/{plugin}/{controller}/fields/{field}/dependent-options", 404, "field without dependsOn", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, dependentOptionsPath("title", "acme-page"), nil, true)
}},
{"GET /{vendor}/{plugin}/{controller}/fields/{field}/dependent-options", 401, "anonymous", "", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, dependentOptionsPath("reference", "acme-page"), nil, false)
}},
{"GET /{vendor}/{plugin}/{controller}/reorder", 404, "unknown partition", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, "/acme/companions/links/reorder?partition=999999", nil, true)
}},
{"GET /{vendor}/{plugin}/{controller}/reorder", 422, "partition not an id", "validation_failed", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, "/acme/companions/links/reorder?partition=abc", nil, true)
}},
{"POST /{vendor}/{plugin}/{controller}/reorder", 409, "stale set", "reorder_stale", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
group, ids := partitionLinks(t, e)
return e.send(t, http.MethodPost, "/acme/companions/links/reorder", map[string]any{"partition": group, "nodes": []any{reorderNode(ids[0], nil, 1)}}, true)
}},
{"POST /{vendor}/{plugin}/{controller}/reorder", 422, "cycle", "validation_failed", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
group, ids := partitionLinks(t, e)
return e.send(t, http.MethodPost, "/acme/companions/links/reorder", map[string]any{"partition": group, "nodes": []any{reorderNode(ids[0], &ids[1], 1), reorderNode(ids[1], &ids[0], 1)}}, true)
}},
{"POST /{vendor}/{plugin}/{controller}/reorder", 404, "unknown partition", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodPost, "/acme/companions/links/reorder", map[string]any{"partition": 999999, "nodes": []any{}}, true)
}},
{"GET /{vendor}/{plugin}/{controller}/singleton", 404, "not a singleton", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, "/acme/companions/links/singleton", nil, true)
}},
{"GET /{vendor}/{plugin}/{controller}", 404, "list of a singleton", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodGet, "/acme/companions/profiles", nil, true)
}},
{"POST /{vendor}/{plugin}/{controller}", 404, "create on a singleton", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodPost, "/acme/companions/profiles", map[string]any{"title": "x"}, true)
}},
{"DELETE /{vendor}/{plugin}/{controller}/{id}", 404, "delete on a singleton", "not_found", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
return e.send(t, http.MethodDelete, fmt.Sprintf("/acme/companions/profiles/%d", profileID(t, e)), nil, true)
}},
}
}
// conformCases is the conformance case of every inventoried admin route, in