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:
134
modules/cabana/phase121_rowstate_test.go
Normal file
134
modules/cabana/phase121_rowstate_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package cabana_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRowStateOrder: states are sent in the fixed order deleted, negative,
|
||||
// disabled, whatever order the hook answers in, and a repeated state is sent
|
||||
// once (D-12).
|
||||
func TestRowStateOrder(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
// The fixture answers disabled twice, then negative, then deleted.
|
||||
all := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "All", Banned: true, DeletedAt: rosterDeleted()})
|
||||
two := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Two", Banned: true})
|
||||
one := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "One"})
|
||||
body, raw := rosterList(t, env, "")
|
||||
want := map[string][]string{
|
||||
fmt.Sprint(all): {"deleted", "negative", "disabled"},
|
||||
fmt.Sprint(two): {"negative", "disabled"},
|
||||
fmt.Sprint(one): {"disabled"},
|
||||
}
|
||||
if !reflect.DeepEqual(body.Meta.RowStates, want) {
|
||||
t.Fatalf("row_states = %v, want %v\n%s", body.Meta.RowStates, want, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStateUnknownDropped: a value outside the fixed set is never sent
|
||||
// (T-12.1-07); the known states of the same row stay.
|
||||
func TestRowStateUnknownDropped(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
logs := captureLog(t)
|
||||
only := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Odd", Active: true})
|
||||
mixed := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Odd", Banned: true})
|
||||
body, raw := rosterList(t, env, "")
|
||||
if strings.Contains(raw, "starred") {
|
||||
t.Fatalf("an unknown state was sent: %s", raw)
|
||||
}
|
||||
if _, ok := body.Meta.RowStates[fmt.Sprint(only)]; ok {
|
||||
t.Fatalf("a row whose only state is unknown is listed: %v", body.Meta.RowStates)
|
||||
}
|
||||
if got := body.Meta.RowStates[fmt.Sprint(mixed)]; !reflect.DeepEqual(got, []string{"negative", "disabled"}) {
|
||||
t.Fatalf("known states next to an unknown one = %v", got)
|
||||
}
|
||||
if dropped := logs.matching("cabana: unknown list row state dropped"); len(dropped) != 2 || !strings.Contains(dropped[0], "state=starred") {
|
||||
t.Fatalf("server log = %q", dropped)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStateLengthMismatch: a hook result that is not aligned with the page
|
||||
// fails the list with the generic 500.
|
||||
func TestRowStateLengthMismatch(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Ada"})
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: rosterShort})
|
||||
rec := env.expect(t, http.StatusInternalServerError, http.MethodGet, rosterPeople, "", "bearer")
|
||||
got := rosterError(t, rec)
|
||||
if got.Code != "error" || len(got.Details) != 0 || strings.Contains(rec.Body.String(), "row states") || strings.Contains(rec.Body.String(), "Ada") {
|
||||
t.Fatalf("500 body = %s", rec.Body.String())
|
||||
}
|
||||
// A page without the misaligned answer still lists.
|
||||
env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?search=Ada", "", "bearer")
|
||||
}
|
||||
|
||||
// TestRowStateHookError: a hook error fails the list with the generic 500 and
|
||||
// no error text.
|
||||
func TestRowStateHookError(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
logs := captureLog(t)
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: rosterStateErr})
|
||||
rec := env.expect(t, http.StatusInternalServerError, http.MethodGet, rosterPeople, "", "bearer")
|
||||
if got := rosterError(t, rec); got.Code != "error" || strings.Contains(rec.Body.String(), "hunter2") || strings.Contains(rec.Body.String(), "state table") {
|
||||
t.Fatalf("500 body = %s", rec.Body.String())
|
||||
}
|
||||
if failed := logs.matching("cabana: list row states failed"); len(failed) != 1 || !strings.Contains(failed[0], "controller=acme.roster.people") {
|
||||
t.Fatalf("server log = %q", failed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStateAbsentKey: meta has no row_states key when no row has a state,
|
||||
// when the page is empty and when the controller has no hook.
|
||||
func TestRowStateAbsentKey(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Plain", Active: true})
|
||||
for _, query := range []string{"", "?search=nobody-matches-this"} {
|
||||
rec := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+query, "", "bearer")
|
||||
if strings.Contains(rec.Body.String(), "row_states") {
|
||||
t.Fatalf("row_states sent for %q: %s", query, rec.Body.String())
|
||||
}
|
||||
}
|
||||
demo, demoDB := newActEnv(t)
|
||||
actInsert(t, demoDB, "plain", "acme")
|
||||
rec := demo.expect(t, http.StatusOK, http.MethodGet, "/acme/demo/gadgets", "", "bearer")
|
||||
if strings.Contains(rec.Body.String(), "row_states") {
|
||||
t.Fatalf("a controller without the hook sent row_states: %s", rec.Body.String())
|
||||
}
|
||||
// Row states are meta, never a column of the row.
|
||||
body, _ := rosterList(t, env, "")
|
||||
for _, row := range body.Data {
|
||||
if _, ok := row["row_states"]; ok {
|
||||
t.Fatalf("a row carries row_states: %v", row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStateOncePerPage: the hook is called once per served page with that
|
||||
// page's records, and not at all for an empty page.
|
||||
func TestRowStateOncePerPage(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
for i := range 21 {
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: fmt.Sprintf("Person %02d", i)})
|
||||
}
|
||||
rosterInsert(t, gdb, rosterPerson{Tenant: "other", Name: "Foreign"})
|
||||
env.spy.takeStates()
|
||||
first, _ := rosterList(t, env, "")
|
||||
second, _ := rosterList(t, env, "?page=2")
|
||||
rosterList(t, env, "?search=nobody-matches-this")
|
||||
if calls := env.spy.takeStates(); !reflect.DeepEqual(calls, []int{20, 1}) {
|
||||
t.Fatalf("ListRowStates calls = %v, want one call of 20 and one of 1", calls)
|
||||
}
|
||||
if len(first.Meta.RowStates) != 20 || len(second.Meta.RowStates) != 1 {
|
||||
t.Fatalf("row_states sizes = %d and %d", len(first.Meta.RowStates), len(second.Meta.RowStates))
|
||||
}
|
||||
// The keys of a page are the ids of that page's rows.
|
||||
for _, row := range second.Data {
|
||||
id, _ := row["id"].(float64)
|
||||
if _, ok := second.Meta.RowStates[fmt.Sprint(uint(id))]; !ok {
|
||||
t.Fatalf("page 2 row %v has no state entry: %v", row["id"], second.Meta.RowStates)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user