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,144 @@
package cabana_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/cabana"
)
// TestPreviewSchema: the form schema reports the preview block, the
// preview-only field with its context, and nothing of either for a form
// without a preview (D-11).
func TestPreviewSchema(t *testing.T) {
env, _ := newRosterEnv(t)
view, raw := rosterFormSchema(t, env, "bearer")
if view.Preview == nil || view.Preview.HeaderPartial != "status" || !strings.Contains(raw, `"preview":{"headerPartial":"status"}`) {
t.Fatalf("preview block = %+v in %s", view.Preview, raw)
}
if !strings.Contains(raw, `"name":"joined_ip","type":"text","label":"Joined from IP address","context":"preview"}`) {
t.Fatalf("the preview-only field is not in the schema: %s", raw)
}
if !strings.Contains(raw, `{"name":"name","type":"text","label":"Name","span":"left"}`) {
t.Fatalf("a field without a context got one: %s", raw)
}
// The same schema for the limited administrator: the preview is part of
// the form, not a permission.
if limited, _ := rosterFormSchema(t, env, "limited"); limited.Preview == nil {
t.Fatal("the limited administrator gets no preview block")
}
// preview: {} enables the screen without a header partial.
bare, _ := newRosterEnvWith(t, func(p *rosterPlugin) {
p.fsys = rosterTree(t, map[string]string{"controllers/people/config_form.yaml": rosterFormHead + "preview: {}\n"})
})
view, raw = rosterFormSchema(t, bare, "bearer")
if view.Preview == nil || view.Preview.HeaderPartial != "" || !strings.Contains(raw, `"preview":{}`) {
t.Fatalf("preview: {} = %+v in %s", view.Preview, raw)
}
// A form without the key has no preview at all.
none, _ := newRosterEnvWith(t, func(p *rosterPlugin) {
p.fsys = rosterTree(t, map[string]string{"controllers/people/config_form.yaml": rosterFormHead})
})
view, raw = rosterFormSchema(t, none, "bearer")
if view.Preview != nil || strings.Contains(raw, `"preview":{}`) || strings.Contains(raw, "headerPartial") {
t.Fatalf("a form without preview reports one: %s", raw)
}
}
// TestPreviewFieldNeverWritten: a field with context preview is returned by
// show and ignored by create and update (T-12.1-14).
func TestPreviewFieldNeverWritten(t *testing.T) {
env, gdb := newRosterEnv(t)
ip := "203.0.113.7"
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Ada", Active: true, JoinedIP: &ip})
rec := env.expect(t, http.StatusOK, http.MethodGet, rosterPath(id, ""), "", "bearer")
if got := rosterRecord(t, rec.Body.Bytes()).Data["joined_ip"]; got != ip {
t.Fatalf("show joined_ip = %v", got)
}
for _, body := range []string{`{"joined_ip":"198.51.100.1"}`, `{"name":"Ada L","joined_ip":null}`, `{"name":"Ada L","joined_ip":""}`} {
rec = env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), body, "bearer")
if got := rosterRecord(t, rec.Body.Bytes()).Data["joined_ip"]; got != ip {
t.Fatalf("update %s answered joined_ip = %v", body, got)
}
if stored := rosterLoad(t, gdb, id); stored.JoinedIP == nil || *stored.JoinedIP != ip {
t.Fatalf("update %s wrote joined_ip = %v", body, stored.JoinedIP)
}
}
if rosterLoad(t, gdb, id).Name != "Ada L" {
t.Fatal("the writable part of the body was not saved")
}
rec = env.expect(t, http.StatusCreated, http.MethodPost, rosterPeople, `{"name":"New","joined_ip":"198.51.100.2",`+rosterPair+`}`, "bearer")
created, _ := rosterRecord(t, rec.Body.Bytes()).Data["id"].(float64)
if stored := rosterLoad(t, gdb, uint(created)); stored.JoinedIP != nil {
t.Fatalf("create wrote joined_ip = %q", *stored.JoinedIP)
}
// A nested value for it is ignored like any other value.
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"joined_ip":{"x":1}}`, "bearer")
}
// TestPreviewHeaderPartialScoped: the status hint is served through the
// partial route with the form scope and the controller's permission, as
// nodes (T-12.1-15).
func TestPreviewHeaderPartialScoped(t *testing.T) {
env, gdb := newRosterEnv(t)
banned := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bea", Active: true, Banned: true})
gone := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Gone", Active: true, DeletedAt: rosterDeleted()})
idle := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Idle"})
foreign := rosterInsert(t, gdb, rosterPerson{Tenant: "other", Name: "Zed", Banned: true})
hint := func(id uint) string { return fmt.Sprintf("%s/partials/status?id=%d", rosterPeople, id) }
for id, want := range map[uint][2]string{
banned: {"summer-callout--danger", "This person is banned"},
gone: {"summer-callout--danger", "This person is archived"},
idle: {"summer-callout--warning", "This person is not active"},
} {
rec := env.expect(t, http.StatusOK, http.MethodGet, hint(id), "", "limited")
if body := rec.Body.String(); !strings.Contains(body, want[0]) || !strings.Contains(body, want[1]) || strings.Contains(body, "<") {
t.Fatalf("hint of %d = %s", id, body)
}
var view cabana.Envelope[cabana.PartialView]
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil || len(view.Data.Nodes) != 1 || view.Data.Nodes[0].Attrs["role"] != "status" {
t.Fatalf("hint nodes = %+v (%v)", view.Data.Nodes, err)
}
}
rec := env.expect(t, http.StatusNotFound, http.MethodGet, hint(foreign), "", "bearer")
actErrorCode(t, rec.Body.Bytes(), "not_found")
env.expect(t, http.StatusNotFound, http.MethodGet, hint(999999), "", "bearer")
env.expect(t, http.StatusNotFound, http.MethodGet, fmt.Sprintf("%s/partials/missing?id=%d", rosterPeople, banned), "", "bearer")
// The Polish request gets Polish text.
if pl := rosterLocale(env, http.MethodGet, hint(banned), "", "pl"); !strings.Contains(pl.Body.String(), "Ta osoba jest zablokowana") {
t.Fatalf("pl hint = %s", pl.Body.String())
}
// Without a token there is no hint.
req := httptest.NewRequest(http.MethodGet, adminAPI(hint(banned)), nil)
anonymous := httptest.NewRecorder()
env.h.ServeHTTP(anonymous, req)
if anonymous.Code != http.StatusUnauthorized {
t.Fatalf("anonymous hint = %d", anonymous.Code)
}
}
// TestPreviewMessagesDefaults: a form that names no preview or edit message
// gets the framework's, in the request locale.
func TestPreviewMessagesDefaults(t *testing.T) {
env, _ := newRosterEnvWith(t, func(p *rosterPlugin) {
p.fsys = rosterTree(t, map[string]string{"controllers/people/config_form.yaml": rosterFormHead + "preview: {}\n"})
})
view, _ := rosterFormSchema(t, env, "bearer")
if view.Messages.Preview["other"] != "Record preview" || view.Messages.Edit["other"] != "Edit record" {
t.Fatalf("default messages = %v / %v", view.Messages.Preview, view.Messages.Edit)
}
pl := rosterLocale(env, http.MethodGet, rosterPeople+"/schema/form", "", "pl")
if !strings.Contains(pl.Body.String(), `"preview":{"other":"Podgląd rekordu"}`) {
t.Fatalf("pl default messages = %s", pl.Body.String())
}
// The fixture's own messages replace them.
own, _ := newRosterEnv(t)
view, _ = rosterFormSchema(t, own, "bearer")
if view.Messages.Preview["other"] != "Person details" || view.Messages.Edit["other"] != "Edit person" {
t.Fatalf("plugin messages = %v / %v", view.Messages.Preview, view.Messages.Edit)
}
}