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

@@ -136,6 +136,8 @@ type dfRecorder struct {
mu sync.Mutex
calls []string
fail map[string]bool
// refuse makes a named hook answer a cabana.ForbiddenError.
refuse map[string]bool
// probe, when set, runs inside the Form hooks with the write's
// transaction and its result is recorded after the hook name.
probe func(tx *gorm.DB) string
@@ -152,6 +154,7 @@ func (r *dfRecorder) reset() {
defer r.mu.Unlock()
r.calls = nil
r.fail = map[string]bool{}
r.refuse = map[string]bool{}
r.probe = nil
}
@@ -161,6 +164,19 @@ func (r *dfRecorder) failOn(name string) {
r.fail[name] = true
}
// refuseOn makes the named hook refuse with a ForbiddenError.
func (r *dfRecorder) refuseOn(name string) {
r.mu.Lock()
defer r.mu.Unlock()
if r.refuse == nil {
r.refuse = map[string]bool{}
}
r.refuse[name] = true
}
// dfRefusal is the message of a hook refusal asked for with refuseOn.
const dfRefusal = "This change is not allowed here."
func (r *dfRecorder) snapshot() []string {
r.mu.Lock()
defer r.mu.Unlock()
@@ -172,6 +188,7 @@ func (r *dfRecorder) hook(ctx context.Context, name string) error {
r.mu.Lock()
probe := r.probe
fail := r.fail[name]
refuse := r.refuse[name]
r.mu.Unlock()
call := name
if probe != nil {
@@ -180,6 +197,9 @@ func (r *dfRecorder) hook(ctx context.Context, name string) error {
}
}
r.add(call)
if refuse {
return &cabana.ForbiddenError{Message: dfRefusal, Details: map[string]any{"hook": []string{name}}}
}
if fail {
return errors.New("fixture hook " + name + " failed")
}