1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-01-27 23:46:18 +02:00
pocketbase/core/collection_validate_test.go
2024-09-29 21:09:46 +03:00

814 lines
23 KiB
Go

package core_test
import (
"testing"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestCollectionValidate(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
collection func(app core.App) (*core.Collection, error)
expectedErrors []string
}{
{
name: "empty collection",
collection: func(app core.App) (*core.Collection, error) {
return &core.Collection{}, nil
},
expectedErrors: []string{
"id", "name", "type", "fields", // no default fields because the type is unknown
},
},
{
name: "unknown type with all invalid fields",
collection: func(app core.App) (*core.Collection, error) {
c := &core.Collection{}
c.Id = "invalid_id ?!@#$"
c.Name = "invalid_name ?!@#$"
c.Type = "invalid_type"
c.ListRule = types.Pointer("missing = '123'")
c.ViewRule = types.Pointer("missing = '123'")
c.CreateRule = types.Pointer("missing = '123'")
c.UpdateRule = types.Pointer("missing = '123'")
c.DeleteRule = types.Pointer("missing = '123'")
c.Indexes = []string{"create index '' on '' ()"}
// type specific fields
c.ViewQuery = "invalid" // should be ignored
c.AuthRule = types.Pointer("missing = '123'") // should be ignored
return c, nil
},
expectedErrors: []string{
"id", "name", "type", "indexes",
"listRule", "viewRule", "createRule", "updateRule", "deleteRule",
"fields", // no default fields because the type is unknown
},
},
{
name: "base with invalid fields",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("invalid_name ?!@#$")
c.Indexes = []string{"create index '' on '' ()"}
// type specific fields
c.ViewQuery = "invalid" // should be ignored
c.AuthRule = types.Pointer("missing = '123'") // should be ignored
return c, nil
},
expectedErrors: []string{"name", "indexes"},
},
{
name: "view with invalid fields",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewViewCollection("invalid_name ?!@#$")
c.Indexes = []string{"create index '' on '' ()"}
// type specific fields
c.ViewQuery = "invalid"
c.AuthRule = types.Pointer("missing = '123'") // should be ignored
return c, nil
},
expectedErrors: []string{"indexes", "name", "fields", "viewQuery"},
},
{
name: "auth with invalid fields",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("invalid_name ?!@#$")
c.Indexes = []string{"create index '' on '' ()"}
// type specific fields
c.ViewQuery = "invalid" // should be ignored
c.AuthRule = types.Pointer("missing = '123'")
return c, nil
},
expectedErrors: []string{"indexes", "name", "authRule"},
},
// type checks
{
name: "empty type",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Type = ""
return c, nil
},
expectedErrors: []string{"type"},
},
{
name: "unknown type",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Type = "unknown"
return c, nil
},
expectedErrors: []string{"type"},
},
{
name: "base type",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
return c, nil
},
expectedErrors: []string{},
},
{
name: "view type",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewViewCollection("test")
c.ViewQuery = "select 1 as id"
return c, nil
},
expectedErrors: []string{},
},
{
name: "auth type",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("test")
return c, nil
},
expectedErrors: []string{},
},
{
name: "changing type",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("users")
c.Type = core.CollectionTypeBase
return c, nil
},
expectedErrors: []string{"type"},
},
// system checks
{
name: "change from system to regular",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
c.System = false
return c, nil
},
expectedErrors: []string{"system"},
},
{
name: "change from regular to system",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.System = true
return c, nil
},
expectedErrors: []string{"system"},
},
{
name: "create system",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_system")
c.System = true
return c, nil
},
expectedErrors: []string{},
},
// id checks
{
name: "empty id",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Id = ""
return c, nil
},
expectedErrors: []string{"id"},
},
{
name: "invalid id",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Id = "!invalid"
return c, nil
},
expectedErrors: []string{"id"},
},
{
name: "existing id",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Id = "_pb_users_auth_"
return c, nil
},
expectedErrors: []string{"id"},
},
{
name: "changing id",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo3")
c.Id = "anything"
return c, nil
},
expectedErrors: []string{"id"},
},
{
name: "valid id",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test")
c.Id = "anything"
return c, nil
},
expectedErrors: []string{},
},
// name checks
{
name: "empty name",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("")
c.Id = "test"
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "invalid name",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("!invalid")
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "name with _via_",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("a_via_b")
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "create with existing collection name",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("demo1")
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "create with existing internal table name",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("_collections")
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "update with existing collection name",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("users")
c.Name = "demo1"
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "update with existing internal table name",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("users")
c.Name = "_collections"
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "system collection name change",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
c.Name = "superusers_new"
return c, nil
},
expectedErrors: []string{"name"},
},
{
name: "create with valid name",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_col")
return c, nil
},
expectedErrors: []string{},
},
{
name: "update with valid name",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Name = "demo1_new"
return c, nil
},
expectedErrors: []string{},
},
// rule checks
{
name: "invalid base rules",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new")
c.ListRule = types.Pointer("!invalid")
c.ViewRule = types.Pointer("missing = 123")
c.CreateRule = types.Pointer("id = 123 && missing = 456")
c.UpdateRule = types.Pointer("(id = 123")
c.DeleteRule = types.Pointer("missing = 123")
return c, nil
},
expectedErrors: []string{"listRule", "viewRule", "createRule", "updateRule", "deleteRule"},
},
{
name: "valid base rules",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new")
c.Fields.Add(&core.TextField{Name: "f1"}) // dummy field to ensure that new fields can be referenced
c.ListRule = types.Pointer("")
c.ViewRule = types.Pointer("f1 = 123")
c.CreateRule = types.Pointer("id = 123 && f1 = 456")
c.UpdateRule = types.Pointer("(id = 123)")
c.DeleteRule = types.Pointer("f1 = 123")
return c, nil
},
expectedErrors: []string{},
},
{
name: "view with non-nil create/update/delete rules",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewViewCollection("new")
c.ViewQuery = "select 1 as id, 'text' as f1"
c.ListRule = types.Pointer("id = 123")
c.ViewRule = types.Pointer("f1 = 456")
c.CreateRule = types.Pointer("")
c.UpdateRule = types.Pointer("")
c.DeleteRule = types.Pointer("")
return c, nil
},
expectedErrors: []string{"createRule", "updateRule", "deleteRule"},
},
{
name: "view with nil create/update/delete rules",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewViewCollection("new")
c.ViewQuery = "select 1 as id, 'text' as f1"
c.ListRule = types.Pointer("id = 1")
c.ViewRule = types.Pointer("f1 = 456")
return c, nil
},
expectedErrors: []string{},
},
{
name: "changing api rules",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("users")
c.Fields.Add(&core.TextField{Name: "f1"}) // dummy field to ensure that new fields can be referenced
c.ListRule = types.Pointer("id = 1")
c.ViewRule = types.Pointer("f1 = 456")
c.CreateRule = types.Pointer("id = 123 && f1 = 456")
c.UpdateRule = types.Pointer("(id = 123)")
c.DeleteRule = types.Pointer("f1 = 123")
return c, nil
},
expectedErrors: []string{},
},
{
name: "changing system collection api rules",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
c.ListRule = types.Pointer("1 = 1")
c.ViewRule = types.Pointer("1 = 1")
c.CreateRule = types.Pointer("1 = 1")
c.UpdateRule = types.Pointer("1 = 1")
c.DeleteRule = types.Pointer("1 = 1")
c.ManageRule = types.Pointer("1 = 1")
c.AuthRule = types.Pointer("1 = 1")
return c, nil
},
expectedErrors: []string{
"listRule", "viewRule", "createRule", "updateRule",
"deleteRule", "manageRule", "authRule",
},
},
// indexes checks
{
name: "invalid index expression",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index invalid",
"create index idx_test_demo2 on anything (text)", // the name of table shouldn't matter
}
return c, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "index name used in other table",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index `idx_test_demo1` on demo1 (id)",
"create index `__pb_USERS_auth__username_idx` on anything (text)", // should be case-insensitive
}
return c, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "duplicated index names",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index idx_test_demo1 on demo1 (id)",
"create index idx_test_demo1 on anything (text)",
}
return c, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "try to add index to a view collection",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("view1")
c.Indexes = []string{"create index idx_test_view1 on view1 (id)"}
return c, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "replace old with new indexes",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index idx_test_demo1 on demo1 (id)",
"create index idx_test_demo2 on anything (text)", // the name of table shouldn't matter
}
return c, nil
},
expectedErrors: []string{},
},
{
name: "old + new indexes",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"CREATE INDEX `_wsmn24bux7wo113_created_idx` ON `demo1` (`created`)",
"create index idx_test_demo1 on anything (id)",
}
return c, nil
},
expectedErrors: []string{},
},
{
name: "index for missing field",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index idx_test_demo1 on anything (missing)", // still valid because it is checked on db persist
}
return c, nil
},
expectedErrors: []string{},
},
{
name: "auth collection with missing required unique indexes",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Indexes = []string{}
return c, nil
},
expectedErrors: []string{"indexes", "passwordAuth"},
},
{
name: "auth collection with non-unique required indexes",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Indexes = []string{
"create index test_idx1 on new_auth (tokenKey)",
"create index test_idx2 on new_auth (email)",
}
return c, nil
},
expectedErrors: []string{"indexes", "passwordAuth"},
},
{
name: "auth collection with unique required indexes",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Indexes = []string{
"create unique index test_idx1 on new_auth (tokenKey)",
"create unique index test_idx2 on new_auth (email)",
}
return c, nil
},
expectedErrors: []string{},
},
{
name: "removing index on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// mark the title field as system
demo2.Fields.GetByName("title").SetSystem(true)
if err = app.Save(demo2); err != nil {
return nil, err
}
// refresh
demo2, err = app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
demo2.RemoveIndex("idx_unique_demo2_title")
return demo2, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "changing index on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// mark the title field as system
demo2.Fields.GetByName("title").SetSystem(true)
if err = app.Save(demo2); err != nil {
return nil, err
}
// refresh
demo2, err = app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// replace the index with a partial one
demo2.RemoveIndex("idx_unique_demo2_title")
demo2.AddIndex("idx_unique_demo2_title", true, "title", "1 = 1")
return demo2, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "changing index on non-system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// replace the index with a partial one
demo2.RemoveIndex("idx_demo2_active")
demo2.AddIndex("idx_demo2_active", true, "active", "1 = 1")
return demo2, nil
},
expectedErrors: []string{},
},
// fields list checks
{
name: "empty fields",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = nil // the minimum fields should auto added
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "no id primay key field",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = core.NewFieldsList(
&core.TextField{Name: "id"},
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with id primay key field",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = core.NewFieldsList(
&core.TextField{Name: "id", PrimaryKey: true, Required: true},
)
return c, nil
},
expectedErrors: []string{},
},
{
name: "duplicated field names",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = core.NewFieldsList(
&core.TextField{Name: "id", PrimaryKey: true, Required: true},
&core.TextField{Id: "f1", Name: "Test"}, // case-insensitive
&core.BoolField{Id: "f2", Name: "test"},
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "changing field type",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
f := c.Fields.GetByName("text")
c.Fields.Add(&core.BoolField{Id: f.GetId(), Name: f.GetName()})
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "renaming system field",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId(core.CollectionNameAuthOrigins)
f := c.Fields.GetByName("fingerprint")
f.SetName("fingerprint_new")
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "deleting system field",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId(core.CollectionNameAuthOrigins)
c.Fields.RemoveByName("fingerprint")
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "invalid field setting",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test_new")
c.Fields.Add(&core.TextField{Name: "f1", Min: -10})
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "valid field setting",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("test_new")
c.Fields.Add(&core.TextField{Name: "f1", Min: 10})
return c, nil
},
expectedErrors: []string{},
},
{
name: "fields view changes should be ignored",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("view1")
c.Fields = nil
return c, nil
},
expectedErrors: []string{},
},
{
name: "with reserved auth only field name (passwordConfirm)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "passwordConfirm"},
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with reserved auth only field name (oldPassword)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "oldPassword"},
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with invalid password auth field options (1)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "password", System: true, Hidden: true}, // should be PasswordField
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with valid password auth field options (2)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.PasswordField{Name: "password", System: true, Hidden: true},
)
return c, nil
},
expectedErrors: []string{},
},
{
name: "with invalid tokenKey auth field options (1)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "tokenKey", System: true}, // should be also hidden
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with valid tokenKey auth field options (2)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "tokenKey", System: true, Hidden: true},
)
return c, nil
},
expectedErrors: []string{},
},
{
name: "with invalid email auth field options (1)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "email", System: true}, // should be EmailField
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with valid email auth field options (2)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.EmailField{Name: "email", System: true},
)
return c, nil
},
expectedErrors: []string{},
},
{
name: "with invalid verified auth field options (1)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.TextField{Name: "verified", System: true}, // should be BoolField
)
return c, nil
},
expectedErrors: []string{"fields"},
},
{
name: "with valid verified auth field options (2)",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new_auth")
c.Fields.Add(
&core.BoolField{Name: "verified", System: true},
)
return c, nil
},
expectedErrors: []string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := s.collection(app)
if err != nil {
t.Fatalf("Failed to retrieve test collection: %v", err)
}
result := app.Validate(collection)
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}