- bouncer TestPhase10CookieGuard: cookie read without Bearer, Bearer wins, empty cookie, frontend audience and blacklisted jti rejected - boardwalk TestPhase10BoardwalkServing: HEAD, query strings, encoded traversal, index by name, nested prefix, MIME fallback, constructor errors - cabana TestPhase10Coverage: mounted unsafe routes vs the CSRF walk, option and filter edges, read-only labels, relation message defaults, bundle fallback locale, cookie refresh of an expired token in the refresh window - phrasebook override precedence, new locale, Bundle merge order, Forms shapes - surf prefix collision for deeper paths and the default /backend prefix - swagger2openapi TestUnionRewrite and converter branch tests - framework tests no longer name the application (acme fixtures instead)
598 lines
21 KiB
Go
598 lines
21 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/pact"
|
|
"git.golem15.com/golem15/summercms/phrasebook"
|
|
"git.golem15.com/golem15/summercms/towel"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const allColumnsConfig = `title: demo.lang.widgets
|
|
modelClass: Widget
|
|
list: ~/plugins/acme/demo/models/widget/columns.yaml
|
|
recordUrl: acme/demo/widgets/update/:id
|
|
noRecordsMessage: backend::lang.list.no_records
|
|
recordsPerPage: 20
|
|
perPageOptions:
|
|
- 20
|
|
- 50
|
|
- 100
|
|
showSetup: true
|
|
showCheckboxes: true
|
|
showSorting: true
|
|
defaultSort:
|
|
column: name
|
|
direction: asc
|
|
toolbar:
|
|
buttons: [create, delete]
|
|
search:
|
|
prompt: backend::lang.list.search_prompt
|
|
`
|
|
|
|
const allColumnsJSON = `{"title":"demo.lang.widgets","modelClass":"Widget","recordUrl":"acme/demo/widgets/update/:id","noRecordsMessage":"backend::lang.list.no_records","recordsPerPage":20,"perPageOptions":[20,50,100],"showSearch":true,"showSetup":true,"showCheckboxes":true,"showSorting":true,"searchTerm":"search","searchPrompt":"backend::lang.list.search_prompt","defaultSort":{"column":"name","direction":"asc"},"toolbarButtons":["create","delete"],"columns":[{"key":"name","label":"demo.lang.name","searchable":true,"sortable":true,"type":"text"},{"key":"created_at","label":"demo.lang.created","searchable":false,"sortable":true,"type":"datetime"},{"key":"active","label":"demo.lang.active","searchable":false,"sortable":false,"type":"switch"},{"key":"genre","label":"demo.lang.genre","searchable":true,"sortable":false,"relation":"genre","select":"name"}],"filters":[],"rowActions":[{"name":"update","label":"backend::lang.form.update","url":"acme/demo/widgets/update/:id"}],"bulkActions":[{"name":"delete","label":"backend::lang.list.delete_selected"}],` + defaultListMessagesJSON + `}`
|
|
|
|
// defaultListMessagesJSON is a compiled list's messages block when the YAML
|
|
// declares none: every key is a framework default phrase key (D-13).
|
|
const defaultListMessagesJSON = `"messages":{"recordCount":{"other":"backend::lang.messages.list.record_count"},"create":{"other":"backend::lang.messages.list.create"},"searchPrompt":{"other":"backend::lang.list.search_prompt"},"empty":{"other":"backend::lang.list.no_records"},"emptySearch":{"other":"backend::lang.messages.list.empty_search"},"emptySearchHint":{"other":"backend::lang.messages.list.empty_search_hint"},"selected":{"other":"backend::lang.messages.list.selected"},"deleteSelected":{"other":"backend::lang.list.delete_selected"},"deleteConfirm":{"other":"backend::lang.messages.list.delete_confirm"},"deleted":{"other":"backend::lang.messages.list.deleted"}}`
|
|
|
|
func TestListSchemaCompile(t *testing.T) {
|
|
columns := readListFixture(t, "testdata/list/all_columns.yaml")
|
|
got := compileListJSON(t, schemaController{model: "Widget"}, allColumnsConfig, columns)
|
|
if got != allColumnsJSON {
|
|
t.Fatalf("compiled list =\n%s\nwant\n%s", got, allColumnsJSON)
|
|
}
|
|
again := compileListJSON(t, schemaController{model: "Widget"}, allColumnsConfig, columns)
|
|
if again != got {
|
|
t.Fatalf("recompilation changed JSON:\n%s\n%s", got, again)
|
|
}
|
|
for _, want := range []string{
|
|
`"searchable":false`, `"sortable":false`, `"type":"datetime"`, `"type":"switch"`,
|
|
`"relation":"genre"`, `"select":"name"`, `"searchTerm":"search"`, `"showSetup":true`,
|
|
`"perPageOptions":[20,50,100]`, `"toolbarButtons":["create","delete"]`, `"filters":[]`,
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("compiled list missing %s:\n%s", want, got)
|
|
}
|
|
}
|
|
|
|
t.Run("winter genre list still compiles", func(t *testing.T) {
|
|
const config = `list: ~/plugins/golem15/acme/models/genre/columns.yaml
|
|
modelClass: Golem15\Acme\Models\Genre
|
|
title: golem15.acme::lang.genre.label_plural
|
|
recordUrl: golem15/acme/genres/update/:id
|
|
noRecordsMessage: backend::lang.list.no_records
|
|
recordsPerPage: 20
|
|
showCheckboxes: true
|
|
toolbar:
|
|
buttons: [create, delete]
|
|
search:
|
|
prompt: backend::lang.list.search_prompt
|
|
`
|
|
const cols = `columns:
|
|
name:
|
|
label: golem15.acme::lang.genre.name
|
|
searchable: true
|
|
slug:
|
|
label: golem15.acme::lang.genre.slug
|
|
searchable: true
|
|
`
|
|
ctl := schemaController{model: `Golem15\Acme\Models\Genre`}
|
|
fsys := fstest.MapFS{
|
|
"controllers/genres/config_list.yaml": &fstest.MapFile{Data: []byte(config)},
|
|
"models/genre/columns.yaml": &fstest.MapFile{Data: []byte(cols)},
|
|
}
|
|
genre := genreConfigController{schemaController: ctl}
|
|
if _, err := CompileList("golem15.acme", genre, fsys); err != nil {
|
|
t.Fatalf("genre list: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListSchemaEmpty(t *testing.T) {
|
|
const config = `list: ~/plugins/acme/demo/models/widget/columns.yaml
|
|
modelClass: Widget
|
|
recordsPerPage: 20
|
|
`
|
|
got := compileListJSON(t, schemaController{model: "Widget"}, config, "columns: {}\n")
|
|
want := `{"modelClass":"Widget","recordsPerPage":20,"perPageOptions":[20],"showSearch":false,"showSetup":false,"showCheckboxes":false,"showSorting":true,"searchTerm":"search","toolbarButtons":[],"columns":[],"filters":[],"rowActions":[],"bulkActions":[],` + defaultListMessagesJSON + `}`
|
|
if got != want {
|
|
t.Fatalf("empty list =\n%s\nwant\n%s", got, want)
|
|
}
|
|
for _, nullKey := range []string{`"columns":null`, `"filters":null`, `"rowActions":null`, `"bulkActions":null`, `"perPageOptions":null`, `"toolbarButtons":null`} {
|
|
if strings.Contains(got, nullKey) {
|
|
t.Fatalf("empty collection serialized as null: %s", nullKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListSchemaSingle(t *testing.T) {
|
|
const config = `list: ~/plugins/acme/demo/models/widget/columns.yaml
|
|
modelClass: Widget
|
|
recordsPerPage: 20
|
|
`
|
|
got := compileListJSON(t, schemaController{model: "Widget"}, config, "columns:\n name:\n label: Name\n searchable: true\n")
|
|
want := `{"modelClass":"Widget","recordsPerPage":20,"perPageOptions":[20],"showSearch":false,"showSetup":false,"showCheckboxes":false,"showSorting":true,"searchTerm":"search","toolbarButtons":[],"columns":[{"key":"name","label":"Name","searchable":true,"sortable":true}],"filters":[],"rowActions":[],"bulkActions":[],` + defaultListMessagesJSON + `}`
|
|
if got != want {
|
|
t.Fatalf("single list =\n%s\nwant\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestListSchemaOrdering(t *testing.T) {
|
|
const config = `list: ~/plugins/acme/demo/models/widget/columns.yaml
|
|
modelClass: Widget
|
|
recordsPerPage: 20
|
|
perPageOptions:
|
|
- 100
|
|
- 20
|
|
- 50
|
|
`
|
|
const columns = `columns:
|
|
zeta:
|
|
label: Z
|
|
type: text
|
|
alpha:
|
|
label: A
|
|
type: datetime
|
|
mid:
|
|
label: M
|
|
type: switch
|
|
`
|
|
first := compileListJSON(t, schemaController{model: "Widget"}, config, columns)
|
|
second := compileListJSON(t, schemaController{model: "Widget"}, config, columns)
|
|
if first != second {
|
|
t.Fatalf("order changed between compiles:\n%s\n%s", first, second)
|
|
}
|
|
zeta := strings.Index(first, `"key":"zeta"`)
|
|
alpha := strings.Index(first, `"key":"alpha"`)
|
|
mid := strings.Index(first, `"key":"mid"`)
|
|
if zeta < 0 || alpha < 0 || mid < 0 || !(zeta < alpha && alpha < mid) {
|
|
t.Fatalf("source order lost: %s", first)
|
|
}
|
|
if !strings.Contains(first, `"perPageOptions":[100,20,50]`) {
|
|
t.Fatalf("perPageOptions were reordered: %s", first)
|
|
}
|
|
}
|
|
|
|
func TestListSchemaRejects(t *testing.T) {
|
|
columns := readListFixture(t, "testdata/list/all_columns.yaml")
|
|
cases := []struct {
|
|
name string
|
|
model string
|
|
config string
|
|
columns string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "duplicate column",
|
|
config: allColumnsConfig,
|
|
columns: "columns:\n name:\n label: A\n name:\n label: B\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/columns.yaml", "duplicate"},
|
|
},
|
|
{
|
|
name: "unknown key",
|
|
config: allColumnsConfig,
|
|
columns: "columns:\n name:\n label: Name\n cssClass: bad\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/columns.yaml", "cssClass"},
|
|
},
|
|
{
|
|
name: "unsupported type",
|
|
config: allColumnsConfig,
|
|
columns: "columns:\n name:\n label: Name\n type: partial\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/columns.yaml", "partial"},
|
|
},
|
|
{
|
|
name: "sql select",
|
|
config: allColumnsConfig,
|
|
columns: "columns:\n genre:\n label: Genre\n relation: genre\n select: \"@.name\"\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/columns.yaml", "select"},
|
|
},
|
|
{
|
|
name: "invalid default column",
|
|
config: strings.Replace(allColumnsConfig, "column: name", "column: missing", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "missing"},
|
|
},
|
|
{
|
|
name: "invalid default direction",
|
|
config: strings.Replace(allColumnsConfig, "direction: asc", "direction: ASC", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "ASC"},
|
|
},
|
|
{
|
|
name: "path escape",
|
|
config: strings.Replace(allColumnsConfig, "list: ~/plugins/acme/demo/models/widget/columns.yaml", "list: ../../etc/passwd", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "escape"},
|
|
},
|
|
{
|
|
name: "mismatched modelClass",
|
|
model: "Widget",
|
|
config: strings.Replace(allColumnsConfig, "modelClass: Widget", "modelClass: widget", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "modelClass"},
|
|
},
|
|
{
|
|
name: "unsupported action",
|
|
config: strings.Replace(allColumnsConfig, "buttons: [create, delete]", "buttons: [create, drop_database]", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "drop_database"},
|
|
},
|
|
{
|
|
name: "invalid page size",
|
|
config: strings.Replace(allColumnsConfig, " - 100\n", " - 0\n", 1),
|
|
columns: columns,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_list.yaml", "perPageOptions"},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
model := tc.model
|
|
if model == "" {
|
|
model = "Widget"
|
|
}
|
|
_, err := CompileList("acme.demo", schemaController{model: model}, listFS(tc.config, tc.columns))
|
|
if err == nil {
|
|
t.Fatal("expected boot error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range tc.want {
|
|
if want == "duplicate" {
|
|
if strings.Contains(msg, "duplicate") || strings.Contains(msg, "already defined") {
|
|
continue
|
|
}
|
|
t.Fatalf("error %q missing duplicate", msg)
|
|
}
|
|
if want == "escape" {
|
|
if strings.Contains(msg, "escape") || strings.Contains(msg, "..") {
|
|
continue
|
|
}
|
|
t.Fatalf("error %q missing escape", msg)
|
|
}
|
|
if !strings.Contains(msg, want) {
|
|
t.Fatalf("error %q missing %q", msg, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type genreConfigController struct{ schemaController }
|
|
|
|
func (c genreConfigController) ConfigDir() string { return "controllers/genres" }
|
|
func (c genreConfigController) ID() string { return "golem15.acme.genres" }
|
|
|
|
func listFS(config, columns string) fstest.MapFS {
|
|
return fstest.MapFS{
|
|
"controllers/widgets/config_list.yaml": &fstest.MapFile{Data: []byte(config)},
|
|
"models/widget/columns.yaml": &fstest.MapFile{Data: []byte(columns)},
|
|
}
|
|
}
|
|
|
|
func compileListJSON(t *testing.T, ctl pact.AdminController, config, columns string) string {
|
|
t.Helper()
|
|
schema, err := CompileList("acme.demo", ctl, listFS(config, columns))
|
|
if err != nil {
|
|
t.Fatalf("compile: %v", err)
|
|
}
|
|
raw, err := json.Marshal(schema)
|
|
if err != nil {
|
|
t.Fatalf("json: %v", err)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
func readListFixture(t *testing.T, name string) string {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(name)
|
|
if err != nil {
|
|
t.Fatalf("fixture %s: %v", name, err)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
const filterListConfig = `title: demo.lang.widgets
|
|
modelClass: Widget
|
|
list: ~/plugins/acme/demo/models/widget/columns.yaml
|
|
recordsPerPage: 20
|
|
filter: config_filter.yaml
|
|
`
|
|
|
|
const filterColumns = `columns:
|
|
name:
|
|
label: Name
|
|
searchable: true
|
|
`
|
|
|
|
const allFiltersJSON = `{"title":"demo.lang.widgets","modelClass":"Widget","recordsPerPage":20,"perPageOptions":[20],"showSearch":false,"showSetup":false,"showCheckboxes":false,"showSorting":true,"searchTerm":"search","toolbarButtons":[],"columns":[{"key":"name","label":"Name","searchable":true,"sortable":true}],"filters":[{"name":"activated","label":"demo.lang.activated","type":"switch","column":"active","options":[{"value":true,"label":"demo.lang.yes"},{"value":false,"label":"demo.lang.no"}]},{"name":"created","label":"demo.lang.created_range","type":"daterange","column":"created_at"},{"name":"grouped","label":"demo.lang.grouped","type":"scope","scope":"filterByGroup","modelClass":"Group","nameFrom":"name"}],"rowActions":[],"bulkActions":[],` + defaultListMessagesJSON + `}`
|
|
|
|
func TestListSchemaFilter(t *testing.T) {
|
|
filters := readListFixture(t, "testdata/list/all_filters.yaml")
|
|
ctl := filterController{schemaController: schemaController{model: "Widget"}, rec: &filterRow{}}
|
|
got := compileFilterJSON(t, ctl, filterListConfig, filterColumns, filters)
|
|
if got != allFiltersJSON {
|
|
t.Fatalf("compiled filters =\n%s\nwant\n%s", got, allFiltersJSON)
|
|
}
|
|
again := compileFilterJSON(t, ctl, filterListConfig, filterColumns, filters)
|
|
if again != got {
|
|
t.Fatalf("recompilation changed JSON:\n%s\n%s", got, again)
|
|
}
|
|
if strings.Contains(got, `"value":"true"`) || strings.Contains(got, `"value":"false"`) || strings.Contains(got, "conditions") {
|
|
t.Fatalf("filter values lost their type or carried a condition: %s", got)
|
|
}
|
|
yes := strings.Index(got, `{"value":true,"label":"demo.lang.yes"}`)
|
|
no := strings.Index(got, `{"value":false,"label":"demo.lang.no"}`)
|
|
if yes < 0 || no < 0 || yes > no {
|
|
t.Fatalf("switch option order drifted: %s", got)
|
|
}
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
raw := compileFilterJSON(t, ctl, filterListConfig, filterColumns, "scopes: {}\n")
|
|
if !strings.Contains(raw, `"filters":[]`) || strings.Contains(raw, `"filters":null`) {
|
|
t.Fatalf("empty filters = %s", raw)
|
|
}
|
|
})
|
|
t.Run("single", func(t *testing.T) {
|
|
const one = `scopes:
|
|
activated:
|
|
label: Activated
|
|
type: switch
|
|
column: active
|
|
`
|
|
raw := compileFilterJSON(t, ctl, filterListConfig, filterColumns, one)
|
|
want := `"filters":[{"name":"activated","label":"Activated","type":"switch","column":"active","trueValue":true,"falseValue":false}]`
|
|
if !strings.Contains(raw, want) {
|
|
t.Fatalf("single filter =\n%s\nwant substring\n%s", raw, want)
|
|
}
|
|
})
|
|
t.Run("labels localize", func(t *testing.T) {
|
|
const localized = `scopes:
|
|
activated:
|
|
label: acme.demo::lang.activated
|
|
type: switch
|
|
column: active
|
|
options:
|
|
true: acme.demo::lang.yes
|
|
false: acme.demo::lang.no
|
|
grouped:
|
|
label: acme.demo::lang.grouped
|
|
modelClass: Group
|
|
nameFrom: name
|
|
scope: filterByGroup
|
|
`
|
|
schema, err := CompileList("acme.demo", ctl, filterFS(filterListConfig, filterColumns, localized))
|
|
if err != nil {
|
|
t.Fatalf("compile: %v", err)
|
|
}
|
|
before, err := json.Marshal(schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := phrasebook.NewTranslator(filterCatalog(t), phrasebook.Options{Locale: "pl", Fallback: "en"})
|
|
view, err := schema.Localize(towel.WithLocale(context.Background(), "pl"), tr)
|
|
if err != nil {
|
|
t.Fatalf("localize: %v", err)
|
|
}
|
|
raw, err := json.Marshal(view)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(raw)
|
|
for _, want := range []string{`"locale":"pl"`, `"label":"Aktywny"`, `"label":"Tak"`, `"label":"Grupa"`, `"column":"active"`, `"scope":"filterByGroup"`, `"value":true`} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("localized filters missing %s:\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, `"value":"true"`) || strings.Contains(got, "acme.demo::lang.activated") {
|
|
t.Fatalf("localization changed a value or left a label key: %s", got)
|
|
}
|
|
after, err := json.Marshal(schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(before) != string(after) || strings.Contains(string(after), "Aktywny") {
|
|
t.Fatalf("localization mutated the cached schema:\n%s", after)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListSchemaScope(t *testing.T) {
|
|
const scopes = `scopes:
|
|
grouped:
|
|
label: Group
|
|
modelClass: Group
|
|
nameFrom: name
|
|
scope: filterByGroup
|
|
`
|
|
ctl := filterController{schemaController: schemaController{model: "Widget"}, rec: &filterRow{}}
|
|
raw := compileFilterJSON(t, ctl, filterListConfig, filterColumns, scopes)
|
|
if !strings.Contains(raw, `"type":"scope"`) || !strings.Contains(raw, `"scope":"filterByGroup"`) || strings.Contains(raw, "DestroyAll") {
|
|
t.Fatalf("registered scope = %s", raw)
|
|
}
|
|
|
|
t.Run("missing provider", func(t *testing.T) {
|
|
_, err := CompileList("acme.demo", schemaController{model: "Widget"}, filterFS(filterListConfig, filterColumns, scopes))
|
|
if err == nil || !strings.Contains(err.Error(), "filterByGroup") || !strings.Contains(err.Error(), "FilterScope") || !strings.Contains(err.Error(), "config_filter.yaml") {
|
|
t.Fatalf("missing provider err = %v", err)
|
|
}
|
|
})
|
|
t.Run("case sensitive scope", func(t *testing.T) {
|
|
body := strings.Replace(scopes, "scope: filterByGroup", "scope: FilterByGroup", 1)
|
|
_, err := CompileList("acme.demo", ctl, filterFS(filterListConfig, filterColumns, body))
|
|
if err == nil || !strings.Contains(err.Error(), "FilterByGroup") {
|
|
t.Fatalf("case variant err = %v", err)
|
|
}
|
|
})
|
|
t.Run("unknown column", func(t *testing.T) {
|
|
const body = `scopes:
|
|
activated:
|
|
label: Activated
|
|
type: switch
|
|
column: missing
|
|
`
|
|
_, err := CompileList("acme.demo", ctl, filterFS(filterListConfig, filterColumns, body))
|
|
if err == nil || !strings.Contains(err.Error(), "missing") || !strings.Contains(err.Error(), "acme.demo.widgets") {
|
|
t.Fatalf("unknown column err = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListSchemaRejectsRawCondition(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
body string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "conditions list",
|
|
body: `scopes:
|
|
activated:
|
|
label: Activated
|
|
type: switch
|
|
column: active
|
|
conditions:
|
|
- "active = true"
|
|
`,
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_filter.yaml", "conditions"},
|
|
},
|
|
{
|
|
name: "conditions string",
|
|
body: `scopes:
|
|
created:
|
|
label: Created
|
|
type: daterange
|
|
conditions: "created_at >= ':after'"
|
|
`,
|
|
want: []string{"conditions", "config_filter.yaml"},
|
|
},
|
|
{
|
|
name: "arbitrary method",
|
|
body: `scopes:
|
|
grouped:
|
|
label: Group
|
|
modelClass: Group
|
|
nameFrom: name
|
|
scope: DestroyAll
|
|
`,
|
|
want: []string{"DestroyAll", "config_filter.yaml"},
|
|
},
|
|
{
|
|
name: "duplicate filter",
|
|
body: `scopes:
|
|
activated:
|
|
label: A
|
|
type: switch
|
|
column: active
|
|
activated:
|
|
label: B
|
|
type: switch
|
|
column: active
|
|
`,
|
|
want: []string{"duplicate", "config_filter.yaml"},
|
|
},
|
|
{
|
|
name: "unknown type",
|
|
body: `scopes:
|
|
activated:
|
|
label: Activated
|
|
type: checkbox
|
|
column: active
|
|
`,
|
|
want: []string{"checkbox", "config_filter.yaml"},
|
|
},
|
|
}
|
|
ctl := filterController{schemaController: schemaController{model: "Widget"}, rec: &filterRow{}}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := CompileList("acme.demo", ctl, filterFS(filterListConfig, filterColumns, tc.body))
|
|
if err == nil {
|
|
t.Fatal("expected boot error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range tc.want {
|
|
if want == "duplicate" {
|
|
if strings.Contains(msg, "duplicate") || strings.Contains(msg, "already defined") {
|
|
continue
|
|
}
|
|
t.Fatalf("error %q missing duplicate", msg)
|
|
}
|
|
if !strings.Contains(msg, want) {
|
|
t.Fatalf("error %q missing %q", msg, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type filterRow struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Name string `gorm:"column:name"`
|
|
Active bool `gorm:"column:active"`
|
|
CreatedAt time.Time `gorm:"column:created_at"`
|
|
}
|
|
|
|
func (filterRow) FilterScopes() []string { return []string{"filterByGroup"} }
|
|
|
|
// FilterOptions serves the model-backed scope's choices (D-27).
|
|
func (filterRow) FilterOptions(scope string) []pact.Option {
|
|
if scope != "filterByGroup" {
|
|
return nil
|
|
}
|
|
return []pact.Option{{Value: "4", Label: "demo::lang.group_four"}, {Value: "9", Label: "Group nine"}}
|
|
}
|
|
|
|
func (filterRow) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
if db == nil || name != "filterByGroup" {
|
|
return db
|
|
}
|
|
return db.Where("group_id = ?", value)
|
|
}
|
|
|
|
type filterController struct {
|
|
schemaController
|
|
rec any
|
|
}
|
|
|
|
func (c filterController) NewRecord() any { return c.rec }
|
|
|
|
func filterFS(config, columns, filters string) fstest.MapFS {
|
|
return fstest.MapFS{
|
|
"controllers/widgets/config_list.yaml": &fstest.MapFile{Data: []byte(config)},
|
|
"models/widget/columns.yaml": &fstest.MapFile{Data: []byte(columns)},
|
|
"controllers/widgets/config_filter.yaml": &fstest.MapFile{Data: []byte(filters)},
|
|
}
|
|
}
|
|
|
|
func compileFilterJSON(t *testing.T, ctl pact.AdminController, config, columns, filters string) string {
|
|
t.Helper()
|
|
schema, err := CompileList("acme.demo", ctl, filterFS(config, columns, filters))
|
|
if err != nil {
|
|
t.Fatalf("compile: %v", err)
|
|
}
|
|
raw, err := json.Marshal(schema)
|
|
if err != nil {
|
|
t.Fatalf("json: %v", err)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
func filterCatalog(t *testing.T) *phrasebook.Catalog {
|
|
t.Helper()
|
|
cat := phrasebook.NewCatalog()
|
|
err := cat.Load("acme.demo", fstest.MapFS{
|
|
"lang/en/lang.yaml": &fstest.MapFile{Data: []byte("activated: Activated\nyes: Yes\nno: No\ngrouped: Grouped\n")},
|
|
"lang/pl/lang.yaml": &fstest.MapFile{Data: []byte("activated: Aktywny\nyes: Tak\nno: Nie\ngrouped: Grupa\n")},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("catalog: %v", err)
|
|
}
|
|
return cat
|
|
}
|