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) }) } }