test(12.1-05): unit tests for bulk and record actions, row state, forbidden, preview and the form seams

- bulk action: empty, duplicate, unordered, absent, partial, out-of-scope, rollback, concurrent runs, permissions, CSRF, body cap
- record action: scope, Applies, strict body, offered order, rollback, Applies error
- ForbiddenError from every Form hook, the bulk delete and the relation link and child hooks
- permission editor modes, locked codes and provider errors; relation locks on create, update and belongsTo
- TestPhase121BootErrors: every boot error of plans 01 and 02 with plugin, controller and file
- pact: the action, row state and filter contracts on a sample controller
This commit is contained in:
Jakub Zych
2026-10-05 14:48:34 +02:00
parent c076b4c059
commit 2e94cbf9f8
13 changed files with 2455 additions and 12 deletions

View File

@@ -0,0 +1,101 @@
package cabana_test
import (
"fmt"
"net/http"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestInvisibleColumnSearchAndRows: a column with invisible: true is flagged
// in the schema, searched and sorted on the server, and never part of a row
// (D-27 G6).
func TestInvisibleColumnSearchAndRows(t *testing.T) {
env, gdb := newRosterEnv(t)
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Ada", Email: "ada@hidden.example.test", Slug: "ada", Active: true})
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bob", Email: "bob@example.test", Slug: "bob", Active: true})
rosterInsert(t, gdb, rosterPerson{Tenant: "other", Name: "Zed", Email: "zed@hidden.example.test", Active: true})
schema := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/schema/list", "", "bearer").Body.String()
if !strings.Contains(schema, `{"key":"email","label":"Email","searchable":true,"sortable":true,"invisible":true}`) || strings.Count(schema, `"invisible"`) != 1 {
t.Fatalf("list schema = %s", schema)
}
for _, query := range []string{"", "?search=ada", "?search=hidden.example", "?sort=email&dir=asc"} {
raw := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+query, "", "bearer").Body.String()
if strings.Contains(raw, `"email"`) || strings.Contains(raw, "example.test") {
t.Fatalf("rows of %q carry the invisible column: %s", query, raw)
}
// The visible columns are there.
if !strings.Contains(raw, `"slug":"ada"`) {
t.Fatalf("rows of %q lack a visible column: %s", query, raw)
}
}
// Search by the invisible column finds the row, inside the list scope only.
found := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?search=hidden.example", "", "bearer").Body.String()
if !strings.Contains(found, `"name":"Ada"`) || strings.Contains(found, `"name":"Bob"`) || strings.Contains(found, `"name":"Zed"`) || !strings.Contains(found, `"total":1`) {
t.Fatalf("search by the invisible column = %s", found)
}
// It sorts in both directions.
asc := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?sort=email&dir=asc", "", "bearer").Body.String()
desc := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?sort=email&dir=desc", "", "bearer").Body.String()
if strings.Index(asc, `"name":"Ada"`) > strings.Index(asc, `"name":"Bob"`) || strings.Index(desc, `"name":"Ada"`) < strings.Index(desc, `"name":"Bob"`) {
t.Fatalf("sort by the invisible column:\nasc %s\ndesc %s", asc, desc)
}
// The record response is the form's, not the list's: email is a form field.
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Cy", Email: "cy@example.test", Active: true})
if shown := env.expect(t, http.StatusOK, http.MethodGet, rosterPath(id, ""), "", "bearer").Body.String(); !strings.Contains(shown, `"email":"cy@example.test"`) {
t.Fatalf("show = %s", shown)
}
}
// TestFilterOptionsControllerFirst: a controller that implements
// pact.FilterOptions serves a scope filter's choices, from the database,
// although the model does not.
func TestFilterOptionsControllerFirst(t *testing.T) {
env, gdb := newRosterEnv(t)
news, beta := rosterTag{Name: "news"}, rosterTag{Name: "beta"}
rosterSeed(t, gdb, &news)
rosterSeed(t, gdb, &beta)
tagged := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Tagged", Active: true})
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bare", Active: true})
rosterSeed(t, gdb, &rosterPersonTag{PersonID: tagged, TagID: news.ID})
options := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "limited").Body.String()
want := fmt.Sprintf(`"data":[{"value":"%d","label":"beta"},{"value":"%d","label":"news"}]`, beta.ID, news.ID)
if !strings.Contains(options, want) {
t.Fatalf("options = %s, want %s", options, want)
}
// A row added after boot is offered: the choices are read per request.
late := rosterTag{Name: "alpha"}
rosterSeed(t, gdb, &late)
if options = env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "bearer").Body.String(); !strings.Contains(options, `"label":"alpha"`) {
t.Fatalf("options after an insert = %s", options)
}
// The scope is still the model's.
filtered := env.expect(t, http.StatusOK, http.MethodGet, fmt.Sprintf("%s?filter[tagged]=%d", rosterPeople, news.ID), "", "bearer").Body.String()
if !strings.Contains(filtered, `"name":"Tagged"`) || strings.Contains(filtered, `"name":"Bare"`) {
t.Fatalf("filtered list = %s", filtered)
}
env.expect(t, http.StatusNotFound, http.MethodGet, rosterPeople+"/filters/missing/options", "", "bearer")
if _, ok := any(rosterPerson{}).(pact.FilterOptions); ok {
t.Fatal("the fixture model serves filter options itself")
}
}
// TestFilterOptionsModelFallback: without FilterOptions on the controller the
// model serves the choices, and its labels are translated.
func TestFilterOptionsModelFallback(t *testing.T) {
env, _ := newRosterEnvWith(t, func(p *rosterPlugin) {
p.wrap = func(inner rosterController) pact.AdminController {
return rosterBare{inner: inner, newRecord: func() any { return &rosterOptionsPerson{} }}
}
})
options := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "bearer").Body.String()
if !strings.Contains(options, `"data":[{"value":"1","label":"Tags"},{"value":"2","label":"Plain label"}]`) {
t.Fatalf("model options = %s", options)
}
if _, ok := any(rosterBare{}).(pact.FilterOptions); ok {
t.Fatal("the bare controller serves filter options")
}
}