package cabana_test import ( "context" "strings" "testing" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/pact" ) // rosterActions is the roster controller with its registered bulk and record // actions replaced (boot-error tests). type rosterActions struct { rosterController bulk func([]pact.AdminBulkAction) []pact.AdminBulkAction record func([]pact.AdminRecordAction) []pact.AdminRecordAction } func (a rosterActions) AdminBulkActions() []pact.AdminBulkAction { out := a.rosterController.AdminBulkActions() if a.bulk != nil { out = a.bulk(out) } return out } func (a rosterActions) AdminRecordActions() []pact.AdminRecordAction { out := a.rosterController.AdminRecordActions() if a.record != nil { out = a.record(out) } return out } // TestPhase121BootErrors is the table of every boot error Phase 12.1 added // to the framework: each stops activation and names the plugin, the // controller and, where the mistake is in a file, that file. func TestPhase121BootErrors(t *testing.T) { const ( listFile = "controllers/people/config_list.yaml" formFile = "controllers/people/config_form.yaml" listHead = "list: ~/plugins/acme/roster/models/person/columns.yaml\nmodelClass: Person\n" ) noRun := func(context.Context, pact.AdminBulkActionInput) (pact.AdminBulkActionResult, error) { return pact.AdminBulkActionResult{}, nil } bulk := func(edit func([]pact.AdminBulkAction) []pact.AdminBulkAction) func(*rosterPlugin) { return func(p *rosterPlugin) { p.wrap = func(inner rosterController) pact.AdminController { return rosterActions{rosterController: inner, bulk: edit} } } } record := func(edit func([]pact.AdminRecordAction) []pact.AdminRecordAction) func(*rosterPlugin) { return func(p *rosterPlugin) { p.wrap = func(inner rosterController) pact.AdminController { return rosterActions{rosterController: inner, record: edit} } } } relations := func(edit func([]cabana.FieldRelationContract)) func(*rosterPlugin) { return func(p *rosterPlugin) { p.relations = func(in []cabana.FieldRelationContract) []cabana.FieldRelationContract { edit(in) return in } } } notifyBlock := " notify:\n label: acme.roster::lang.people.notify\n type: checkbox\n default: true\n context: create\n" for _, tc := range []struct { name string files map[string]string plugin func(*rosterPlugin) want string file string // "" when the mistake is in Go code, not in a file }{ // config_list.yaml bulkActions (D-09). {name: "bulkActions without showCheckboxes", files: map[string]string{listFile: listHead + "bulkActions: [activate]\n"}, want: "bulkActions needs showCheckboxes: true", file: listFile}, {name: "bulkActions names an unregistered action", files: map[string]string{listFile: listHead + "showCheckboxes: true\nbulkActions: [activate, promote]\n"}, want: "bulkActions: unsupported action promote (want a bulk action the controller registers)", file: listFile}, {name: "bulkActions names the built-in delete", files: map[string]string{listFile: listHead + "showCheckboxes: true\nbulkActions: [delete]\n"}, want: "bulkActions: unsupported action delete (want a bulk action the controller registers)", file: listFile}, {name: "bulkActions names an action twice", files: map[string]string{listFile: listHead + "showCheckboxes: true\nbulkActions: [activate, activate]\n"}, want: "bulkActions: duplicate action activate", file: listFile}, {name: "bulkActions is a scalar", files: map[string]string{listFile: listHead + "showCheckboxes: true\nbulkActions: activate\n"}, want: "bulkActions must be a list of bulk action names the controller registers", file: listFile}, {name: "a declared bulk action has no label", plugin: bulk(func(in []pact.AdminBulkAction) []pact.AdminBulkAction { in[0].Label = "" return in }), want: "bulkActions: action activate needs a label", file: listFile}, // Registered bulk actions. {name: "a bulk action with a reserved name", plugin: bulk(func(in []pact.AdminBulkAction) []pact.AdminBulkAction { return append(in, pact.AdminBulkAction{Name: "delete", Label: "x", Run: noRun}) }), want: "bulk action delete uses a reserved built-in name (create, delete)"}, {name: "a bulk action registered twice", plugin: bulk(func(in []pact.AdminBulkAction) []pact.AdminBulkAction { return append(in, in[0]) }), want: "duplicate bulk action activate"}, {name: "a bulk action without Run", plugin: bulk(func(in []pact.AdminBulkAction) []pact.AdminBulkAction { in[1].Run = nil return in }), want: "bulk action archive has no Run function"}, {name: "a bulk action name that is not an identifier", plugin: bulk(func(in []pact.AdminBulkAction) []pact.AdminBulkAction { return append(in, pact.AdminBulkAction{Name: "ban users", Label: "x", Run: noRun}) }), want: `bulk action name "ban users" is not an identifier`}, // config_form.yaml recordActions and preview (D-10, D-11). {name: "recordActions without preview", files: map[string]string{formFile: rosterFormHead + "recordActions: [activate]\n"}, want: "recordActions needs a preview block (record actions are offered on the preview screen)", file: formFile}, {name: "recordActions names an unregistered action", files: map[string]string{formFile: rosterFormHead + "preview: {}\nrecordActions: [activate, promote]\n"}, want: "recordActions: unsupported action promote (want a record action the controller registers)", file: formFile}, {name: "recordActions names an action twice", files: map[string]string{formFile: rosterFormHead + "preview: {}\nrecordActions: [activate, activate]\n"}, want: "recordActions: duplicate action activate", file: formFile}, {name: "recordActions is a scalar", files: map[string]string{formFile: rosterFormHead + "preview: {}\nrecordActions: activate\n"}, want: "recordActions must be a list of record action names the controller registers", file: formFile}, {name: "a declared record action has no label", plugin: record(func(in []pact.AdminRecordAction) []pact.AdminRecordAction { in[0].Label = "" return in }), want: "recordActions: action activate needs a label", file: formFile}, {name: "a record action with a reserved name", plugin: record(func(in []pact.AdminRecordAction) []pact.AdminRecordAction { in[1].Name = "create" return in }), want: "record action create uses a reserved built-in name (create, delete)"}, {name: "a record action registered twice", plugin: record(func(in []pact.AdminRecordAction) []pact.AdminRecordAction { return append(in, in[0]) }), want: "duplicate record action activate"}, {name: "a record action without Run", plugin: record(func(in []pact.AdminRecordAction) []pact.AdminRecordAction { in[0].Run = nil return in }), want: "record action activate has no Run function"}, {name: "a null preview", files: map[string]string{formFile: rosterFormHead + "preview:\n"}, want: "preview must be a mapping; write preview: {} to enable the preview screen without a header partial", file: formFile}, {name: "a scalar preview", files: map[string]string{formFile: rosterFormHead + "preview: true\n"}, want: "preview must be a mapping", file: formFile}, {name: "an unknown preview key", files: map[string]string{formFile: rosterFormHead + "preview:\n toolbar: x\n"}, want: "preview: unknown field toolbar", file: formFile}, {name: "a preview header partial that is a path", files: map[string]string{formFile: rosterFormHead + "preview:\n headerPartial: $/acme/status.htm\n"}, want: "path must be a partial name", file: formFile}, {name: "a preview header partial without a template", files: map[string]string{formFile: rosterFormHead + "preview:\n headerPartial: missing\n"}, want: "partial missing", file: "controllers/people/_missing.htm"}, // Password and form-only fields (D-27 G1, G2). {name: "a password field outside FormVirtualFields", files: map[string]string{rosterFieldsFile: rosterFields(t, "", " secret:\n type: password\n")}, want: "field secret: type password needs the controller to list it in FormVirtualFields", file: rosterFieldsFile}, {name: "a listed virtual field that is not on the form", files: map[string]string{rosterFieldsFile: rosterFields(t, notifyBlock, "")}, want: "FormVirtualFields: field notify is not a field of this form", file: rosterFieldsFile}, {name: "a virtual field of a wrong type", files: map[string]string{rosterFieldsFile: rosterFields(t, " type: checkbox\n default: true\n", " type: partial\n path: status\n")}, want: "FormVirtualFields: field notify has type partial (want password, text, textarea, number, checkbox, switch or dropdown)", file: rosterFieldsFile}, {name: "a form-only field the controller does not list", files: map[string]string{rosterFieldsFile: rosterFields(t, "", " nickname:\n type: text\n")}, want: "field nickname is not a model column"}, // preset (D-27 G7). {name: "an unsupported preset type", files: map[string]string{rosterFieldsFile: rosterFields(t, " preset: name\n", " preset:\n field: name\n type: camel\n")}, want: "preset type camel is not supported (want slug or exact)", file: rosterFieldsFile}, {name: "an unknown preset key", files: map[string]string{rosterFieldsFile: rosterFields(t, " preset: name\n", " preset:\n field: name\n prefix: x\n")}, want: "preset: unknown field prefix", file: rosterFieldsFile}, {name: "preset on a field that is not text", files: map[string]string{rosterFieldsFile: rosterFields(t, " type: checkbox\n default: true\n", " type: checkbox\n default: true\n preset: name\n")}, want: "preset is only valid on type: text", file: rosterFieldsFile}, {name: "preset from an unknown field", files: map[string]string{rosterFieldsFile: rosterFields(t, " preset: name\n", " preset: title\n")}, want: "field slug: preset field title is not a field of this form", file: rosterFieldsFile}, {name: "preset from a field that is not text", files: map[string]string{rosterFieldsFile: rosterFields(t, " preset: name\n", " preset: notify\n")}, want: "field slug: preset field notify must be a text field", file: rosterFieldsFile}, {name: "preset from the field itself", files: map[string]string{rosterFieldsFile: rosterFields(t, " preset: name\n", " preset: slug\n")}, want: "field slug: preset names the field itself", file: rosterFieldsFile}, // permissioneditor (D-16). {name: "permissioneditor without mode", files: map[string]string{rosterFieldsFile: rosterFields(t, " mode: radio\n", "")}, want: "field permissions: mode must be radio or checkbox on type: permissioneditor", file: rosterFieldsFile}, {name: "permissioneditor with an unknown mode", files: map[string]string{rosterFieldsFile: rosterFields(t, " mode: radio\n", " mode: tabs\n")}, want: "mode must be radio or checkbox on type: permissioneditor", file: rosterFieldsFile}, {name: "permissioneditor with a default", files: map[string]string{rosterFieldsFile: rosterFields(t, " mode: radio\n", " mode: radio\n default: 1\n")}, want: "default is not valid on type: permissioneditor", file: rosterFieldsFile}, {name: "mode on another type", files: map[string]string{rosterFieldsFile: rosterFields(t, " type: checkbox\n default: true\n", " type: checkbox\n default: true\n mode: radio\n")}, want: "mode is only valid on type: fileupload, datepicker or permissioneditor", file: rosterFieldsFile}, // Relation fields (D-27 G3) and controller filter choices. {name: "WritableForeignKey on belongsToMany", plugin: relations(func(in []cabana.FieldRelationContract) { in[1].WritableForeignKey = true }), want: "field tags: WritableForeignKey is only valid on belongsTo"}, {name: "a scope filter without choices", plugin: func(p *rosterPlugin) { p.wrap = func(inner rosterController) pact.AdminController { return rosterBare{inner: inner} } }, want: "scope filter tagged needs FilterOptions on the controller or the model to serve its choices (D-27)", file: "controllers/people/config_filter.yaml"}, } { t.Run(tc.name, func(t *testing.T) { plugin := rosterPlugin{spy: &rosterSpy{}} if tc.files != nil { plugin.fsys = rosterTree(t, tc.files) } if tc.plugin != nil { tc.plugin(&plugin) } err := rosterBootWith(t, plugin) if err == nil { t.Fatalf("the plugin booted, want an error with %q", tc.want) } parts := []string{tc.want, "acme.roster", "acme.roster.people"} if tc.file != "" { parts = append(parts, tc.file) } for _, part := range parts { if !strings.Contains(err.Error(), part) { t.Fatalf("error %q does not name %q", err, part) } } }) } // The provider-less permission editor is reported for the controller // that lacks the provider. t.Run("permissioneditor without the provider", func(t *testing.T) { err := activateFields(t, datepickerFields(" rights:\n type: permissioneditor\n mode: checkbox\n")) const want = "field rights: type permissioneditor needs the controller to implement cabana.PermissionEditorProvider" if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "fields.yaml") { t.Fatalf("error = %v, want %q", err, want) } }) // The unchanged fixture boots, so every failure above is its one change. if err := rosterBoot(t, rosterTree(t, nil)); err != nil { t.Fatalf("the unchanged fixture does not boot: %v", err) } }