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:
218
modules/cabana/phase121_schema_boot_test.go
Normal file
218
modules/cabana/phase121_schema_boot_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user