Files
summercms/modules/cabana/relation_test.go

282 lines
9.9 KiB
Go

package cabana
import (
"encoding/json"
"strings"
"testing"
"testing/fstest"
)
func TestRelationSchema(t *testing.T) {
ctl := relationTestController{}
fsys := relationTestFS(validRelationYAML)
form, err := CompileForm("acme.demo", ctl, fsys)
if err != nil {
t.Fatal(err)
}
relations, err := compileRelations("acme.demo", ctl, fsys, form)
if err != nil {
t.Fatal(err)
}
compiled := relations["editors"]
if compiled == nil || compiled.Contract.ParentForeignKey != "owner_id" || compiled.Contract.RelatedForeignKey != "target_id" {
t.Fatalf("compiled=%#v", compiled)
}
raw, err := json.Marshal(compiled.Schema)
if err != nil {
t.Fatal(err)
}
got := string(raw)
for _, want := range []string{`"name":"editors"`, `"key":"username"`, `"key":"email"`, `"toolbarButtons":["link","unlink"]`, `"showSearch":true`} {
if !strings.Contains(got, want) {
t.Fatalf("schema missing %s: %s", want, got)
}
}
if strings.Index(got, `"key":"username"`) >= strings.Index(got, `"key":"email"`) {
t.Fatalf("column order drifted: %s", got)
}
for _, tc := range []struct {
name string
yaml string
provider relationTestController
want string
}{
{name: "unknown key", yaml: strings.Replace(validRelationYAML, "showSearch: true", "showSearch: true\n conditions: unsafe", 1), want: "conditions"},
{name: "unsupported action", yaml: strings.Replace(validRelationYAML, "link|unlink", "link|execute", 1), want: "unsupported relation action execute"},
{name: "missing relation", yaml: "other:\n label: other\n view:\n list:\n columns: {}\n manage:\n list:\n columns: {}\n", want: "has no relation-manager field"},
{name: "bad target column", yaml: validRelationYAML, provider: relationTestController{badColumn: true}, want: "target is missing column missing"},
} {
t.Run(tc.name, func(t *testing.T) {
probe := tc.provider
form, err := CompileForm("acme.demo", probe, relationTestFS(tc.yaml))
if err != nil {
t.Fatal(err)
}
_, err = compileRelations("acme.demo", probe, relationTestFS(tc.yaml), form)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("err=%v want=%q", err, tc.want)
}
})
}
}
func TestRelationEmpty(t *testing.T) {
raw, err := json.Marshal(RelationSchema{Name: "empty"})
if err != nil {
t.Fatal(err)
}
got := string(raw)
if strings.Count(got, `"columns":[]`) != 2 || strings.Count(got, `"toolbarButtons":[]`) != 2 || strings.Contains(got, "null") {
t.Fatalf("empty relation collections=%s", got)
}
}
func TestRelationSingle(t *testing.T) {
schema := RelationSchema{Name: "editors", View: RelationPanel{List: RelationList{Columns: []RelationColumn{{Key: "email", Label: "email", Searchable: true, Sortable: true}}}}}
raw, err := json.Marshal(schema)
if err != nil {
t.Fatal(err)
}
if strings.Count(string(raw), `"key":"email"`) != 1 {
t.Fatalf("single column=%s", raw)
}
}
func TestRelationStablePages(t *testing.T) {
schema := &RelationSchema{View: RelationPanel{List: RelationList{Columns: []RelationColumn{{Key: "username", Sortable: true}, {Key: "email", Sortable: true}}}}}
page, per, key, desc, err := normalizeRelationQuery(schema, false, RelationQuery{Page: "2", PerPage: "20", Sort: "username", Dir: "asc"})
if err != nil || page != 2 || per != 20 || key != "username" || desc {
t.Fatalf("normalized=%d/%d %q desc=%v err=%v", page, per, key, desc, err)
}
if _, _, _, _, err := normalizeRelationQuery(schema, false, RelationQuery{Sort: "Username"}); err == nil {
t.Fatal("case-changed sort survived")
}
}
// TestRelationDefaultSort pins WR-06: with no sort in the request the panel
// orders by its first sortable column, or by the primary key when none is
// sortable, instead of rejecting the request.
func TestRelationDefaultSort(t *testing.T) {
schema := &RelationSchema{
View: RelationPanel{List: RelationList{Columns: []RelationColumn{{Key: "label"}, {Key: "email", Sortable: true}}}},
Manage: RelationPanel{List: RelationList{Columns: []RelationColumn{{Key: "label"}}}},
}
_, _, key, _, err := normalizeRelationQuery(schema, false, RelationQuery{})
if err != nil || key != "email" {
t.Fatalf("view default sort = %q err=%v, want the first sortable column", key, err)
}
_, _, key, _, err = normalizeRelationQuery(schema, true, RelationQuery{})
if err != nil || key != "" {
t.Fatalf("manage default sort = %q err=%v, want none (primary key order)", key, err)
}
if _, _, _, _, err := normalizeRelationQuery(schema, false, RelationQuery{Sort: "label"}); err == nil {
t.Fatal("an explicit sort on a non-sortable column was accepted")
}
}
// TestRelationColumnOrderIsIndependentOfIndentation pins WR-07: column order
// is the file's order in each panel, whatever the indentation, and the view and
// manage panels do not share one order.
func TestRelationColumnOrderIsIndependentOfIndentation(t *testing.T) {
const twoSpace = `editors:
label: demo.editors
view:
list:
columns:
username:
label: demo.username
email:
label: demo.email
toolbarButtons: link|unlink
manage:
list:
columns:
username:
label: demo.username
email:
label: demo.email
`
for name, yamlText := range map[string]string{"two-space": twoSpace, "four-space": validRelationYAML} {
t.Run(name, func(t *testing.T) {
ctl := relationTestController{}
fsys := relationTestFS(yamlText)
form, err := CompileForm("acme.demo", ctl, fsys)
if err != nil {
t.Fatal(err)
}
relations, err := compileRelations("acme.demo", ctl, fsys, form)
if err != nil {
t.Fatal(err)
}
schema := relations["editors"].Schema
for panel, cols := range map[string][]RelationColumn{"view": schema.View.List.Columns, "manage": schema.Manage.List.Columns} {
if len(cols) != 2 || cols[0].Key != "username" || cols[1].Key != "email" {
t.Fatalf("%s columns = %+v, want username then email (file order, not alphabetical)", panel, cols)
}
}
})
}
const reversed = `editors:
view:
list:
columns:
username: {label: demo.username}
email: {label: demo.email}
manage:
list:
columns:
email: {label: demo.email}
username: {label: demo.username}
`
ctl := relationTestController{}
fsys := relationTestFS(reversed)
form, err := CompileForm("acme.demo", ctl, fsys)
if err != nil {
t.Fatal(err)
}
relations, err := compileRelations("acme.demo", ctl, fsys, form)
if err != nil {
t.Fatal(err)
}
schema := relations["editors"].Schema
if v, m := schema.View.List.Columns, schema.Manage.List.Columns; v[0].Key != "username" || m[0].Key != "email" {
t.Fatalf("view=%+v manage=%+v, want each panel in its own file order", v, m)
}
}
func TestRelationCandidateExclusions(t *testing.T) {
contract := relationTestController{}.AdminRelationContracts()[0]
ids, err := contract.ExcludedRelatedIDs(&relationTestOwner{OwnerID: 41})
if err != nil || len(ids) != 1 || ids[0] != 41 {
t.Fatalf("excluded=%v err=%v", ids, err)
}
if _, err := contract.ExcludedRelatedIDs(&relationTestTarget{}); err == nil {
t.Fatal("wrong parent type survived")
}
}
const validRelationYAML = `editors:
label: demo.editors
view:
list:
columns:
username:
label: demo.username
email:
label: demo.email
toolbarButtons: link|unlink
showSearch: true
manage:
list:
columns:
username:
label: demo.username
email:
label: demo.email
showSearch: true
`
func relationTestFS(relation string) fstest.MapFS {
return fstest.MapFS{
"controllers/owners/config_form.yaml": {Data: []byte("name: owners\nform: ~/plugins/acme/demo/models/owner/fields.yaml\nmodelClass: Owner\n")},
"controllers/owners/config_relation.yaml": {Data: []byte(relation)},
"models/owner/fields.yaml": {Data: []byte("fields:\n editors:\n type: relation-manager\n relation: editors\n")},
}
}
type relationTestController struct{ badColumn bool }
func (relationTestController) ID() string { return "acme.demo.owners" }
func (relationTestController) ModelName() string { return "Owner" }
func (relationTestController) ConfigDir() string { return "controllers/owners" }
func (relationTestController) NewRecord() any { return &relationTestOwner{} }
func (c relationTestController) AdminRelationContracts() []RelationContract {
columns := map[string]string{"username": "username", "email": "email"}
if c.badColumn {
columns["email"] = "missing"
}
return []RelationContract{{
Name: "editors", NewRelated: func() any { return &relationTestTarget{} }, NewPivot: func() any { return &relationTestPivot{} },
ParentForeignKey: "owner_id", RelatedForeignKey: "target_id", Columns: columns,
HookPivotColumns: []string{"role"},
ExcludedRelatedIDs: func(parent any) ([]uint, error) {
owner, ok := parent.(*relationTestOwner)
if !ok {
return nil, &relationTestError{}
}
return []uint{owner.OwnerID}, nil
},
}}
}
type relationTestOwner struct {
ID uint `gorm:"column:id;primaryKey"`
OwnerID uint `gorm:"column:owner_id"`
Editors []relationTestTarget `gorm:"many2many:relation_test_pivots"`
}
func (*relationTestOwner) TableName() string { return "relation_test_owners" }
type relationTestTarget struct {
ID uint `gorm:"column:id;primaryKey"`
Username string `gorm:"column:username"`
Email string `gorm:"column:email"`
}
func (*relationTestTarget) TableName() string { return "relation_test_targets" }
type relationTestPivot struct {
ID uint `gorm:"column:id;primaryKey"`
OwnerID uint `gorm:"column:owner_id"`
TargetID uint `gorm:"column:target_id"`
Role string `gorm:"column:role"`
}
func (*relationTestPivot) TableName() string { return "relation_test_pivots" }
type relationTestError struct{}
func (*relationTestError) Error() string { return "wrong relation parent" }