package cabana import ( "context" "fmt" "strings" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" ) // actionContributor owns no admin controller; it declares permissions and // adds record actions to other controllers. type actionContributor struct { id string permissions []pact.Permission extensions []pact.AdminActionExtension } func (p actionContributor) ID() string { return p.id } func (actionContributor) Requires() []string { return nil } func (actionContributor) Register(*backpack.App) error { return nil } func (actionContributor) Boot(*backpack.App) error { return nil } func (p actionContributor) Permissions() []pact.Permission { return p.permissions } func (p actionContributor) AdminActionExtensions() []pact.AdminActionExtension { return p.extensions } // namedAction is a valid record action named name behind permission. func namedAction(name, permission string) pact.AdminRecordAction { action := pingAction() action.Name = name action.Label = "Run " + name action.Permissions = []string{permission} return action } func toUsers(actions ...pact.AdminRecordAction) []pact.AdminActionExtension { return []pact.AdminActionExtension{{Controller: AdministratorsControllerID, RecordActions: actions}} } // TestActionExtensionBoot (D-14, SC-3): every refusal of the seam stops boot // with an error naming the plugin, the action and the controller; valid // contributions land after the owner's actions in plugin and declared order. func TestActionExtensionBoot(t *testing.T) { base := screensExtender{} // owns acme.ext.items (list only) and acme.ext.ping contributor := func(id string, ext []pact.AdminActionExtension) actionContributor { return actionContributor{id: id, permissions: []pact.Permission{{Code: id + ".run"}}, extensions: ext} } noRun := namedAction("acme_norun", "acme.one.run") noRun.Run = nil noLabel := namedAction("acme_nolabel", "acme.one.run") noLabel.Label = " " for _, tc := range []struct { name string plugins []party.Plugin want []string }{ {"unknown controller", []party.Plugin{base, contributor("acme.one", []pact.AdminActionExtension{{ Controller: "acme.missing.things", RecordActions: []pact.AdminRecordAction{namedAction("acme_x", "acme.one.run")}, }})}, []string{"acme.one", "acme.missing.things", "unknown admin controller"}}, {"target without a preview block", []party.Plugin{base, contributor("acme.one", []pact.AdminActionExtension{{ Controller: "acme.ext.items", RecordActions: []pact.AdminRecordAction{namedAction("acme_x", "acme.one.run")}, }})}, []string{"acme.one", "acme.ext.items", "no preview block"}}, {"duplicate of the owner's action", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("deactivate", "acme.one.run")))}, []string{"acme.one", `"deactivate"`, AdministratorsControllerID, "plugin summercms.cabana already registers"}}, {"duplicate between two contributors", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("acme_same", "acme.one.run"))), contributor("acme.two", toUsers(namedAction("acme_same", "acme.two.run"))), }, []string{"plugin acme.two adds", `"acme_same"`, "plugin acme.one already registers"}}, {"reserved name create", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("create", "acme.one.run")))}, []string{"acme.one", `"create"`, "reserved"}}, {"reserved name delete", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("delete", "acme.one.run")))}, []string{"acme.one", `"delete"`, "reserved"}}, {"name is not an identifier", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("acme-ping", "acme.one.run")))}, []string{"acme.one", `"acme-ping"`, "not an identifier"}}, {"nil Run", []party.Plugin{base, contributor("acme.one", toUsers(noRun))}, []string{"acme.one", `"acme_norun"`, "no Run function"}}, {"empty label", []party.Plugin{base, contributor("acme.one", toUsers(noLabel))}, []string{"acme.one", `"acme_nolabel"`, "needs a label"}}, {"unknown permission", []party.Plugin{base, contributor("acme.one", toUsers(namedAction("acme_x", "acme.nobody.run")))}, []string{"acme.nobody.run"}}, } { t.Run(tc.name, func(t *testing.T) { _, err := compileWithScreens(t, tc.plugins) if err == nil { t.Fatalf("compiled; want an error naming %v", tc.want) } for _, want := range tc.want { if !strings.Contains(err.Error(), want) { t.Fatalf("err = %v, want it to contain %q", err, want) } } }) } t.Run("valid contributions land after the owner's in order", func(t *testing.T) { plugins := []party.Plugin{base, contributor("acme.one", toUsers(namedAction("acme_a", "acme.one.run"), namedAction("acme_b", "acme.one.run"))), contributor("acme.two", toUsers(namedAction("acme_c", "acme.two.run"))), } reg, err := compileWithScreens(t, plugins) if err != nil { t.Fatal(err) } cc, _ := reg.Get(AdministratorsControllerID) if got := fmt.Sprint(cc.Form.recordActions); got != "[activate deactivate restore acme_a acme_b acme_c]" { t.Fatalf("record actions = %s", got) } for name, plugin := range map[string]string{"acme_a": "acme.one", "acme_b": "acme.one", "acme_c": "acme.two"} { if cc.contributed[name] != plugin || !recordActionOfName(cc, name) { t.Fatalf("%s contributed by %q (want %s), resolvable %v", name, cc.contributed[name], plugin, recordActionOfName(cc, name)) } } if _, ok := cc.contributed["activate"]; ok { t.Fatal("the owner's action is marked contributed") } // The roles screen is untouched. roles, _ := reg.Get(RolesControllerID) if roles.contributed != nil { t.Fatalf("roles contributed = %v", roles.contributed) } }) t.Run("an extension without record actions is ignored", func(t *testing.T) { plugins := []party.Plugin{base, contributor("acme.one", []pact.AdminActionExtension{{Controller: "acme.missing.things"}})} if _, err := compileWithScreens(t, plugins); err != nil { t.Fatal(err) } }) t.Run("a contributed action runs with the owner's record", func(t *testing.T) { var got pact.AdminRecordActionInput action := namedAction("acme_spy", "acme.one.run") action.Run = func(_ context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) { got = in return pact.AdminRecordActionResult{}, nil } reg, err := compileWithScreens(t, []party.Plugin{base, contributor("acme.one", toUsers(action))}) if err != nil { t.Fatal(err) } cc, _ := reg.Get(AdministratorsControllerID) resolved, ok := recordActionOf(cc, "acme_spy") if !ok { t.Fatal("acme_spy not resolvable") } if _, err := resolved.Run(context.Background(), pact.AdminRecordActionInput{RecordID: 7, Record: &BackendUser{ID: 7}}); err != nil || got.RecordID != 7 { t.Fatalf("run = %+v, %v", got, err) } }) }