package daos_test import ( "encoding/json" "errors" "strings" "testing" "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" ) func TestCollectionQuery(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() expected := "SELECT {{_collections}}.* FROM `_collections`" sql := app.Dao().CollectionQuery().Build().SQL() if sql != expected { t.Errorf("Expected sql %s, got %s", expected, sql) } } func TestFindCollectionsByType(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionType string expectError bool expectTotal int }{ {"", false, 0}, {"unknown", false, 0}, {models.CollectionTypeAuth, false, 3}, {models.CollectionTypeBase, false, 5}, } for i, scenario := range scenarios { collections, err := app.Dao().FindCollectionsByType(scenario.collectionType) hasErr := err != nil if hasErr != scenario.expectError { t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) } if len(collections) != scenario.expectTotal { t.Errorf("(%d) Expected %d collections, got %d", i, scenario.expectTotal, len(collections)) } for _, c := range collections { if c.Type != scenario.collectionType { t.Errorf("(%d) Expected collection with type %s, got %s: \n%v", i, scenario.collectionType, c.Type, c) } } } } func TestFindCollectionByNameOrId(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { nameOrId string expectError bool }{ {"", true}, {"missing", true}, {"wsmn24bux7wo113", false}, {"demo1", false}, {"DEMO1", false}, // case insensitive check } for i, scenario := range scenarios { model, err := app.Dao().FindCollectionByNameOrId(scenario.nameOrId) hasErr := err != nil if hasErr != scenario.expectError { t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) } if model != nil && model.Id != scenario.nameOrId && !strings.EqualFold(model.Name, scenario.nameOrId) { t.Errorf("(%d) Expected model with identifier %s, got %v", i, scenario.nameOrId, model) } } } func TestIsCollectionNameUnique(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string excludeId string expected bool }{ {"", "", false}, {"demo1", "", false}, {"Demo1", "", false}, {"new", "", true}, {"demo1", "wsmn24bux7wo113", true}, } for i, scenario := range scenarios { result := app.Dao().IsCollectionNameUnique(scenario.name, scenario.excludeId) if result != scenario.expected { t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) } } } func TestFindCollectionReferences(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.Dao().FindCollectionByNameOrId("demo3") if err != nil { t.Fatal(err) } result, err := app.Dao().FindCollectionReferences( collection, collection.Id, // test whether "nonempty" exclude ids condition will be skipped "", "", ) if err != nil { t.Fatal(err) } if len(result) != 1 { t.Fatalf("Expected 1 collection, got %d: %v", len(result), result) } expectedFields := []string{ "rel_one_no_cascade", "rel_one_no_cascade_required", "rel_one_cascade", "rel_many_no_cascade", "rel_many_no_cascade_required", "rel_many_cascade", } for col, fields := range result { if col.Name != "demo4" { t.Fatalf("Expected collection demo4, got %s", col.Name) } if len(fields) != len(expectedFields) { t.Fatalf("Expected fields %v, got %v", expectedFields, fields) } for i, f := range fields { if !list.ExistInSlice(f.Name, expectedFields) { t.Fatalf("(%d) Didn't expect field %v", i, f) } } } } func TestDeleteCollection(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() c0 := &models.Collection{} c1, err := app.Dao().FindCollectionByNameOrId("clients") if err != nil { t.Fatal(err) } c2, err := app.Dao().FindCollectionByNameOrId("demo2") if err != nil { t.Fatal(err) } c3, err := app.Dao().FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } c3.System = true if err := app.Dao().Save(c3); err != nil { t.Fatal(err) } scenarios := []struct { model *models.Collection expectError bool }{ {c0, true}, {c1, false}, {c2, true}, // is part of a reference {c3, true}, // system } for i, scenario := range scenarios { err := app.Dao().DeleteCollection(scenario.model) hasErr := err != nil if hasErr != scenario.expectError { t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr) } } } func TestSaveCollectionCreate(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() collection := &models.Collection{ Name: "new_test", Type: models.CollectionTypeBase, Schema: schema.NewSchema( &schema.SchemaField{ Type: schema.FieldTypeText, Name: "test", }, ), } err := app.Dao().SaveCollection(collection) if err != nil { t.Fatal(err) } if collection.Id == "" { t.Fatal("Expected collection id to be set") } // check if the records table was created hasTable := app.Dao().HasTable(collection.Name) if !hasTable { t.Fatalf("Expected records table %s to be created", collection.Name) } // check if the records table has the schema fields columns, err := app.Dao().GetTableColumns(collection.Name) if err != nil { t.Fatal(err) } expectedColumns := []string{"id", "created", "updated", "test"} if len(columns) != len(expectedColumns) { t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) } for i, c := range columns { if !list.ExistInSlice(c, expectedColumns) { t.Fatalf("(%d) Didn't expect record column %s", i, c) } } } func TestSaveCollectionUpdate(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.Dao().FindCollectionByNameOrId("demo3") if err != nil { t.Fatal(err) } // rename an existing schema field and add a new one oldField := collection.Schema.GetFieldByName("title") oldField.Name = "title_update" collection.Schema.AddField(&schema.SchemaField{ Type: schema.FieldTypeText, Name: "test", }) saveErr := app.Dao().SaveCollection(collection) if saveErr != nil { t.Fatal(saveErr) } // check if the records table has the schema fields expectedColumns := []string{"id", "created", "updated", "title_update", "test", "files"} columns, err := app.Dao().GetTableColumns(collection.Name) if err != nil { t.Fatal(err) } if len(columns) != len(expectedColumns) { t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) } for i, c := range columns { if !list.ExistInSlice(c, expectedColumns) { t.Fatalf("(%d) Didn't expect record column %s", i, c) } } } func TestImportCollections(t *testing.T) { scenarios := []struct { name string jsonData string deleteMissing bool beforeRecordsSync func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error expectError bool expectCollectionsCount int beforeTestFunc func(testApp *tests.TestApp, resultCollections []*models.Collection) afterTestFunc func(testApp *tests.TestApp, resultCollections []*models.Collection) }{ { name: "empty collections", jsonData: `[]`, expectError: true, expectCollectionsCount: 8, }, { name: "minimal collection import", jsonData: `[ {"name": "import_test1", "schema": [{"name":"test", "type": "text"}]}, {"name": "import_test2", "type": "auth"} ]`, deleteMissing: false, expectError: false, expectCollectionsCount: 10, }, { name: "minimal collection import + failed beforeRecordsSync", jsonData: `[ {"name": "import_test", "schema": [{"name":"test", "type": "text"}]} ]`, beforeRecordsSync: func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error { return errors.New("test_error") }, deleteMissing: false, expectError: true, expectCollectionsCount: 8, }, { name: "minimal collection import + successful beforeRecordsSync", jsonData: `[ {"name": "import_test", "schema": [{"name":"test", "type": "text"}]} ]`, beforeRecordsSync: func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error { return nil }, deleteMissing: false, expectError: false, expectCollectionsCount: 9, }, { name: "new + update + delete system collection", jsonData: `[ { "id":"wsmn24bux7wo113", "name":"demo", "schema":[ { "id":"_2hlxbmp", "name":"title", "type":"text", "system":false, "required":true, "unique":false, "options":{ "min":3, "max":null, "pattern":"" } } ] }, { "name": "import1", "schema": [ { "name":"active", "type":"bool" } ] } ]`, deleteMissing: true, expectError: true, expectCollectionsCount: 8, }, { name: "new + update + delete non-system collection", jsonData: `[ { "id": "kpv709sk2lqbqk8", "system": true, "name": "nologin", "type": "auth", "options": { "allowEmailAuth": false, "allowOAuth2Auth": false, "allowUsernameAuth": false, "exceptEmailDomains": [], "manageRule": "@request.auth.collectionName = 'users'", "minPasswordLength": 8, "onlyEmailDomains": [], "requireEmail": true }, "listRule": "", "viewRule": "", "createRule": "", "updateRule": "", "deleteRule": "", "schema": [ { "id": "x8zzktwe", "name": "name", "type": "text", "system": false, "required": false, "unique": false, "options": { "min": null, "max": null, "pattern": "" } } ] }, { "id":"wsmn24bux7wo113", "name":"demo", "schema":[ { "id":"_2hlxbmp", "name":"title", "type":"text", "system":false, "required":true, "unique":false, "options":{ "min":3, "max":null, "pattern":"" } } ] }, { "id": "test_deleted_collection_name_reuse", "name": "demo2", "schema": [ { "id":"fz6iql2m", "name":"active", "type":"bool" } ] } ]`, deleteMissing: true, expectError: false, expectCollectionsCount: 3, }, { name: "test with deleteMissing: false", jsonData: `[ { "id":"wsmn24bux7wo113", "name":"demo1", "schema":[ { "id":"_2hlxbmp", "name":"title", "type":"text", "system":false, "required":true, "unique":false, "options":{ "min":3, "max":null, "pattern":"" } }, { "id":"_2hlxbmp", "name":"field_with_duplicate_id", "type":"text", "system":false, "required":true, "unique":false, "options":{ "min":3, "max":null, "pattern":"" } }, { "id":"abcd_import", "name":"new_field", "type":"text" } ] }, { "name": "new_import", "schema": [ { "id":"abcd_import", "name":"active", "type":"bool" } ] } ]`, deleteMissing: false, expectError: false, expectCollectionsCount: 9, afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) { expectedCollectionFields := map[string]int{ "nologin": 1, "demo1": 15, "demo2": 2, "demo3": 2, "demo4": 11, "demo5": 5, "new_import": 1, } for name, expectedCount := range expectedCollectionFields { collection, err := testApp.Dao().FindCollectionByNameOrId(name) if err != nil { t.Fatal(err) } if totalFields := len(collection.Schema.Fields()); totalFields != expectedCount { t.Errorf("Expected %d %q fields, got %d", expectedCount, collection.Name, totalFields) } } }, }, } for _, scenario := range scenarios { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() importedCollections := []*models.Collection{} // load data loadErr := json.Unmarshal([]byte(scenario.jsonData), &importedCollections) if loadErr != nil { t.Fatalf("[%s] Failed to load data: %v", scenario.name, loadErr) continue } err := testApp.Dao().ImportCollections(importedCollections, scenario.deleteMissing, scenario.beforeRecordsSync) hasErr := err != nil if hasErr != scenario.expectError { t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasErr, err) } // check collections count collections := []*models.Collection{} if err := testApp.Dao().CollectionQuery().All(&collections); err != nil { t.Fatal(err) } if len(collections) != scenario.expectCollectionsCount { t.Errorf("[%s] Expected %d collections, got %d", scenario.name, scenario.expectCollectionsCount, len(collections)) } if scenario.afterTestFunc != nil { scenario.afterTestFunc(testApp, collections) } } }