package pact 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{} func (configOnly) ConfigFS() fs.FS { return fstest.MapFS{"config/config.yaml": {Data: []byte("k: 1\n")}} } type commandsOnly struct{} func (commandsOnly) Commands() []bonfire.Command { return []bonfire.Command{{Name: "demo:run"}} } type bothCaps struct { configOnly commandsOnly } type neither struct{} type migrationsOnly struct{} func (migrationsOnly) Migrations() []*gormigrate.Migration { return []*gormigrate.Migration{{ID: "202609170001_create_users"}} } func TestOptionalCapabilitiesDiscoveredByTypeAssertion(t *testing.T) { plugins := []any{configOnly{}, commandsOnly{}, bothCaps{}, neither{}, extraMessage{}, migrationsOnly{}} var configs, commands, messages, migrations, none int for _, p := range plugins { _, hasCfg := p.(HasConfig) _, hasCmd := p.(HasCommands) _, hasMsg := p.(OptionalMessage) _, hasMig := p.(HasMigrations) if hasCfg { configs++ } if hasCmd { commands++ } if hasMsg { messages++ } if hasMig { migrations++ } if !hasCfg && !hasCmd && !hasMsg && !hasMig { none++ } } if configs != 2 { t.Fatalf("HasConfig count = %d, want 2", configs) } if commands != 2 { t.Fatalf("HasCommands count = %d, want 2", commands) } if messages != 1 { t.Fatalf("OptionalMessage count = %d, want 1", messages) } if migrations != 1 { t.Fatalf("HasMigrations count = %d, want 1", migrations) } if none != 1 { t.Fatalf("neither count = %d, want 1", none) } } func TestHasMigrationsReturnsOrderedSet(t *testing.T) { var p HasMigrations = migrationsOnly{} set := p.Migrations() if len(set) != 1 || set[0].ID != "202609170001_create_users" { t.Fatalf("Migrations = %+v", set) } } func TestHasConfigReturnsFilesystem(t *testing.T) { var p HasConfig = configOnly{} fsys := p.ConfigFS() if fsys == nil { t.Fatal("ConfigFS returned nil") } raw, err := fs.ReadFile(fsys, "config/config.yaml") if err != nil { t.Fatal(err) } if string(raw) != "k: 1\n" { t.Fatalf("ConfigFS body = %q", raw) } } func TestHasCommandsReturnsNamespacedCommand(t *testing.T) { var p HasCommands = commandsOnly{} cmds := p.Commands() if len(cmds) != 1 || cmds[0].Name != "demo:run" { t.Fatalf("Commands = %+v", cmds) } } func TestOptionalMessageContract(t *testing.T) { var msg OptionalMessage = extraMessage{s: "hello-from-optional"} if msg.Message() != "hello-from-optional" { t.Fatalf("Message = %q", msg.Message()) } } type extraMessage struct{ s string } func (e extraMessage) Message() string { return e.s } type routesOnly struct{} func (routesOnly) Routes(Router) error { return nil } type middlewareOnly struct{} func (middlewareOnly) Middlewares() map[string]Middleware { return map[string]Middleware{"jwt.auth": nil} } type modelsOnly struct{} func (modelsOnly) Models() []any { return []any{struct{}{}} } func TestHTTPCapabilitiesDiscoveredByTypeAssertion(t *testing.T) { var r HasRoutes = routesOnly{} if err := r.Routes(nil); err != nil { t.Fatal(err) } var m HasMiddleware = middlewareOnly{} if _, ok := m.Middlewares()["jwt.auth"]; !ok { t.Fatal("missing jwt.auth") } var models HasModels = modelsOnly{} if len(models.Models()) != 1 { t.Fatalf("Models = %+v", models.Models()) } } // relationHooks implements every relation child hook with the documented // signature and records the calls. type relationHooks struct{ calls *[]string } func (h relationHooks) record(name, relation string, parent, child any) error { *h.calls = append(*h.calls, name+":"+relation+":"+parent.(string)+":"+child.(string)) if relation == "fail" { return errors.New(name + " refused") } return nil } func (h relationHooks) RelationBeforeCreate(_ context.Context, relation string, parent, child any) error { return h.record("BeforeCreate", relation, parent, child) } func (h relationHooks) RelationAfterCreate(_ context.Context, relation string, parent, child any) error { return h.record("AfterCreate", relation, parent, child) } func (h relationHooks) RelationBeforeUpdate(_ context.Context, relation string, parent, child any) error { return h.record("BeforeUpdate", relation, parent, child) } func (h relationHooks) RelationAfterUpdate(_ context.Context, relation string, parent, child any) error { return h.record("AfterUpdate", relation, parent, child) } func (h relationHooks) RelationBeforeDelete(_ context.Context, relation string, parent, child any) error { return h.record("BeforeDelete", relation, parent, child) } func (h relationHooks) RelationAfterDelete(_ context.Context, relation string, parent, child any) error { return h.record("AfterDelete", relation, parent, child) } var ( _ RelationBeforeCreate = relationHooks{} _ RelationAfterCreate = relationHooks{} _ RelationBeforeUpdate = relationHooks{} _ RelationAfterUpdate = relationHooks{} _ RelationBeforeDelete = relationHooks{} _ RelationAfterDelete = relationHooks{} ) // TestRelationHookInterfaces: each of the six optional relation child hooks // is discovered by a type assertion on the controller value, receives the // relation name, the parent and the child, and passes its error back; a // controller without them asserts false. func TestRelationHookInterfaces(t *testing.T) { var calls []string var ctl any = relationHooks{calls: &calls} hooks := []func(context.Context, string, any, any) error{} if h, ok := ctl.(RelationBeforeCreate); ok { hooks = append(hooks, h.RelationBeforeCreate) } if h, ok := ctl.(RelationAfterCreate); ok { hooks = append(hooks, h.RelationAfterCreate) } if h, ok := ctl.(RelationBeforeUpdate); ok { hooks = append(hooks, h.RelationBeforeUpdate) } if h, ok := ctl.(RelationAfterUpdate); ok { hooks = append(hooks, h.RelationAfterUpdate) } if h, ok := ctl.(RelationBeforeDelete); ok { hooks = append(hooks, h.RelationBeforeDelete) } if h, ok := ctl.(RelationAfterDelete); ok { hooks = append(hooks, h.RelationAfterDelete) } if len(hooks) != 6 { t.Fatalf("discovered %d hooks, want 6", len(hooks)) } for _, hook := range hooks { if err := hook(t.Context(), "parts", "gadget", "part"); err != nil { t.Fatal(err) } if err := hook(t.Context(), "fail", "gadget", "part"); err == nil { t.Fatal("hook error was not passed back") } } want := "BeforeCreate:parts:gadget:part,BeforeCreate:fail:gadget:part,AfterCreate:parts:gadget:part,AfterCreate:fail:gadget:part," + "BeforeUpdate:parts:gadget:part,BeforeUpdate:fail:gadget:part,AfterUpdate:parts:gadget:part,AfterUpdate:fail:gadget:part," + "BeforeDelete:parts:gadget:part,BeforeDelete:fail:gadget:part,AfterDelete:parts:gadget:part,AfterDelete:fail:gadget:part" if got := strings.Join(calls, ","); got != want { t.Fatalf("calls = %s", got) } var plain any = struct{}{} for name, ok := range map[string]bool{ "BeforeCreate": func() bool { _, ok := plain.(RelationBeforeCreate); return ok }(), "AfterCreate": func() bool { _, ok := plain.(RelationAfterCreate); return ok }(), "BeforeUpdate": func() bool { _, ok := plain.(RelationBeforeUpdate); return ok }(), "AfterUpdate": func() bool { _, ok := plain.(RelationAfterUpdate); return ok }(), "BeforeDelete": func() bool { _, ok := plain.(RelationBeforeDelete); return ok }(), "AfterDelete": func() bool { _, ok := plain.(RelationAfterDelete); return ok }(), } { if ok { t.Fatalf("a plain value implements Relation%s", name) } } } // TestRowStateValues pins the wire values of the fixed row-state set: the // admin SPA and plugin CSS match on these exact strings. func TestRowStateValues(t *testing.T) { got := []RowState{RowStateDeleted, RowStateNegative, RowStateDisabled} want := []string{"deleted", "negative", "disabled"} for i, state := range got { if string(state) != want[i] { t.Fatalf("row state %d = %q, want %q", i, state, want[i]) } } } type formSeams struct{} func (formSeams) FormVirtualFields() []string { return []string{"password", "password_confirmation"} } func (formSeams) FormRules(_ context.Context, op string) map[string]string { if op == "create" { return map[string]string{"password": "required|confirmed"} } return map[string]string{"password": "nullable|confirmed"} } func TestFormSeamsDiscoveredByTypeAssertion(t *testing.T) { var ctl any = formSeams{} virtual, ok := ctl.(FormVirtualFields) if !ok || len(virtual.FormVirtualFields()) != 2 { t.Fatalf("FormVirtualFields = %v ok=%v", virtual, ok) } rules, ok := ctl.(FormRules) if !ok { t.Fatal("FormRules is not implemented") } if got := rules.FormRules(context.Background(), "create")["password"]; got != "required|confirmed" { t.Fatalf("create rules = %q", got) } if got := rules.FormRules(context.Background(), "update")["password"]; got != "nullable|confirmed" { t.Fatalf("update rules = %q", got) } if _, ok := any(neither{}).(FormVirtualFields); ok { 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) } } }