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:
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type configOnly struct{}
|
||||
@@ -291,3 +293,140 @@ func TestFormSeamsDiscoveredByTypeAssertion(t *testing.T) {
|
||||
t.Fatal("a plugin without the method implements FormVirtualFields")
|
||||
}
|
||||
}
|
||||
|
||||
// sampleRoster is a controller value that implements every admin contract of
|
||||
// Phase 12.1: declared bulk and record actions, row states and controller
|
||||
// filter choices.
|
||||
type sampleRoster struct{ ran *[]string }
|
||||
|
||||
func (s sampleRoster) AdminBulkActions() []AdminBulkAction {
|
||||
return []AdminBulkAction{{
|
||||
Name: "activate", Label: "acme::lang.activate", Confirm: "acme::lang.activate_confirm",
|
||||
Permissions: []string{"acme.manage"},
|
||||
Run: func(_ context.Context, in AdminBulkActionInput) (AdminBulkActionResult, error) {
|
||||
*s.ran = append(*s.ran, "bulk")
|
||||
return AdminBulkActionResult{Message: "acme::lang.activated", Affected: len(in.Records)}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (s sampleRoster) AdminRecordActions() []AdminRecordAction {
|
||||
return []AdminRecordAction{{
|
||||
Name: "reinstate", Label: "acme::lang.reinstate",
|
||||
Applies: func(_ context.Context, record any) (bool, error) {
|
||||
if record == nil {
|
||||
return false, errors.New("no record")
|
||||
}
|
||||
return record.(string) == "banned", nil
|
||||
},
|
||||
Run: func(_ context.Context, in AdminRecordActionInput) (AdminRecordActionResult, error) {
|
||||
*s.ran = append(*s.ran, "record")
|
||||
return AdminRecordActionResult{Message: "acme::lang.reinstated"}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (sampleRoster) ListRowStates(_ context.Context, _ *gorm.DB, records []any) ([][]RowState, error) {
|
||||
out := make([][]RowState, len(records))
|
||||
for i, record := range records {
|
||||
if record.(string) == "banned" {
|
||||
out[i] = []RowState{RowStateNegative}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (sampleRoster) FilterOptions(scope string) []Option {
|
||||
if scope != "tagged" {
|
||||
return nil
|
||||
}
|
||||
return []Option{{Value: "1", Label: "acme::lang.tag"}}
|
||||
}
|
||||
|
||||
var (
|
||||
_ HasAdminBulkActions = sampleRoster{}
|
||||
_ HasAdminRecordActions = sampleRoster{}
|
||||
_ ListRowStates = sampleRoster{}
|
||||
_ FilterOptions = sampleRoster{}
|
||||
)
|
||||
|
||||
// TestPhase121ContractsOnSampleController: the bulk action, record action,
|
||||
// row state and filter choice contracts are discovered by a type assertion
|
||||
// on a controller value and carry exactly the documented fields.
|
||||
func TestPhase121ContractsOnSampleController(t *testing.T) {
|
||||
var ran []string
|
||||
var ctl any = sampleRoster{ran: &ran}
|
||||
|
||||
bulk, ok := ctl.(HasAdminBulkActions)
|
||||
if !ok || len(bulk.AdminBulkActions()) != 1 {
|
||||
t.Fatal("HasAdminBulkActions is not discovered")
|
||||
}
|
||||
action := bulk.AdminBulkActions()[0]
|
||||
result, err := action.Run(t.Context(), AdminBulkActionInput{Records: []any{"a", "b"}})
|
||||
if err != nil || result.Affected != 2 || result.Message != "acme::lang.activated" {
|
||||
t.Fatalf("bulk result = %+v err=%v", result, err)
|
||||
}
|
||||
if action.Name != "activate" || action.Confirm == "" || len(action.Permissions) != 1 {
|
||||
t.Fatalf("bulk action = %+v", action)
|
||||
}
|
||||
// The input of a bulk action is the loaded records and nothing else: an id
|
||||
// list would let a plugin skip the list scope.
|
||||
if n := reflect.TypeOf(AdminBulkActionInput{}).NumField(); n != 1 {
|
||||
t.Fatalf("AdminBulkActionInput has %d fields, want only Records", n)
|
||||
}
|
||||
|
||||
record, ok := ctl.(HasAdminRecordActions)
|
||||
if !ok || len(record.AdminRecordActions()) != 1 {
|
||||
t.Fatal("HasAdminRecordActions is not discovered")
|
||||
}
|
||||
one := record.AdminRecordActions()[0]
|
||||
for input, want := range map[string]bool{"banned": true, "active": false} {
|
||||
if applies, err := one.Applies(t.Context(), input); err != nil || applies != want {
|
||||
t.Fatalf("Applies(%s) = %v err=%v", input, applies, err)
|
||||
}
|
||||
}
|
||||
if _, err := one.Applies(t.Context(), nil); err == nil {
|
||||
t.Fatal("Applies did not pass its error back")
|
||||
}
|
||||
out, err := one.Run(t.Context(), AdminRecordActionInput{RecordID: 7, Record: "banned"})
|
||||
if err != nil || out.Message != "acme::lang.reinstated" || one.Confirm != "" {
|
||||
t.Fatalf("record result = %+v err=%v", out, err)
|
||||
}
|
||||
if fields := reflect.TypeOf(AdminRecordActionInput{}).NumField(); fields != 2 {
|
||||
t.Fatalf("AdminRecordActionInput has %d fields, want RecordID and Record", fields)
|
||||
}
|
||||
if strings.Join(ran, ",") != "bulk,record" {
|
||||
t.Fatalf("ran = %v", ran)
|
||||
}
|
||||
|
||||
states, ok := ctl.(ListRowStates)
|
||||
if !ok {
|
||||
t.Fatal("ListRowStates is not discovered")
|
||||
}
|
||||
got, err := states.ListRowStates(t.Context(), nil, []any{"active", "banned"})
|
||||
if err != nil || len(got) != 2 || got[0] != nil || len(got[1]) != 1 || got[1][0] != RowStateNegative {
|
||||
t.Fatalf("row states = %v err=%v", got, err)
|
||||
}
|
||||
// The fixed set has exactly three values, each its own string.
|
||||
set := map[RowState]bool{RowStateDeleted: true, RowStateNegative: true, RowStateDisabled: true}
|
||||
if len(set) != 3 || string(RowStateDeleted) != "deleted" || string(RowStateNegative) != "negative" || string(RowStateDisabled) != "disabled" {
|
||||
t.Fatalf("row state set = %v", set)
|
||||
}
|
||||
|
||||
options, ok := ctl.(FilterOptions)
|
||||
if !ok || len(options.FilterOptions("tagged")) != 1 || options.FilterOptions("other") != nil {
|
||||
t.Fatal("FilterOptions is not discovered or answers an unknown scope")
|
||||
}
|
||||
|
||||
var plain any = neither{}
|
||||
for name, implemented := range map[string]bool{
|
||||
"HasAdminBulkActions": func() bool { _, ok := plain.(HasAdminBulkActions); return ok }(),
|
||||
"HasAdminRecordActions": func() bool { _, ok := plain.(HasAdminRecordActions); return ok }(),
|
||||
"ListRowStates": func() bool { _, ok := plain.(ListRowStates); return ok }(),
|
||||
"FilterOptions": func() bool { _, ok := plain.(FilterOptions); return ok }(),
|
||||
} {
|
||||
if implemented {
|
||||
t.Fatalf("a plain value implements %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user