package core_test import ( "context" "database/sql" "fmt" "os" "path/filepath" "slices" "strings" "testing" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" ) func TestCollectionQuery(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() expected := "SELECT {{_collections}}.* FROM `_collections`" sql := app.CollectionQuery().Build().SQL() if sql != expected { t.Errorf("Expected sql %s, got %s", expected, sql) } } func TestReloadCachedCollections(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() err := app.ReloadCachedCollections() if err != nil { t.Fatal(err) } cached := app.Store().Get(core.StoreKeyCachedCollections) cachedCollections, ok := cached.([]*core.Collection) if !ok { t.Fatalf("Expected []*core.Collection, got %T", cached) } collections, err := app.FindAllCollections() if err != nil { t.Fatalf("Failed to retrieve all collections: %v", err) } if len(cachedCollections) != len(collections) { t.Fatalf("Expected %d collections, got %d", len(collections), len(cachedCollections)) } for _, c := range collections { var exists bool for _, cc := range cachedCollections { if cc.Id == c.Id { exists = true break } } if !exists { t.Fatalf("The collections cache is missing collection %q", c.Name) } } } func TestFindAllCollections(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionTypes []string expectTotal int }{ {nil, 16}, {[]string{}, 16}, {[]string{""}, 16}, {[]string{"unknown"}, 0}, {[]string{"unknown", core.CollectionTypeAuth}, 4}, {[]string{core.CollectionTypeAuth, core.CollectionTypeView}, 7}, } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s", i, strings.Join(s.collectionTypes, "_")), func(t *testing.T) { collections, err := app.FindAllCollections(s.collectionTypes...) if err != nil { t.Fatal(err) } if len(collections) != s.expectTotal { t.Fatalf("Expected %d collections, got %d", s.expectTotal, len(collections)) } expectedTypes := list.NonzeroUniques(s.collectionTypes) if len(expectedTypes) > 0 { for _, c := range collections { if !slices.Contains(expectedTypes, c.Type) { t.Fatalf("Unexpected collection type %s\n%v", c.Type, c) } } } }) } } func TestFindCollectionByNameOrId(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { nameOrId string expectError bool }{ {"", true}, {"missing", true}, {"wsmn24bux7wo113", false}, {"demo1", false}, {"DEMO1", false}, // case insensitive } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s", i, s.nameOrId), func(t *testing.T) { model, err := app.FindCollectionByNameOrId(s.nameOrId) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if model != nil && model.Id != s.nameOrId && !strings.EqualFold(model.Name, s.nameOrId) { t.Fatalf("Expected model with identifier %s, got %v", s.nameOrId, model) } }) } } func TestFindCachedCollectionByNameOrId(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() totalQueries := 0 app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { totalQueries++ } run := func(withCache bool) { scenarios := []struct { nameOrId string expectError bool }{ {"", true}, {"missing", true}, {"wsmn24bux7wo113", false}, {"demo1", false}, {"DEMO1", false}, // case insensitive } var expectedTotalQueries int if withCache { err := app.ReloadCachedCollections() if err != nil { t.Fatal(err) } } else { app.Store().Reset(nil) expectedTotalQueries = len(scenarios) } totalQueries = 0 for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s", i, s.nameOrId), func(t *testing.T) { model, err := app.FindCachedCollectionByNameOrId(s.nameOrId) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if model != nil && model.Id != s.nameOrId && !strings.EqualFold(model.Name, s.nameOrId) { t.Fatalf("Expected model with identifier %s, got %v", s.nameOrId, model) } }) } if totalQueries != expectedTotalQueries { t.Fatalf("Expected %d totalQueries, got %d", expectedTotalQueries, totalQueries) } } run(true) run(false) } func TestFindCollectionReferences(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatal(err) } result, err := app.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_one_unique", "rel_many_no_cascade", "rel_many_no_cascade_required", "rel_many_cascade", "rel_many_unique", } 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 !slices.Contains(expectedFields, f.GetName()) { t.Fatalf("[%d] Didn't expect field %v", i, f) } } } } func TestFindCachedCollectionReferences(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatal(err) } totalQueries := 0 app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { totalQueries++ } run := func(withCache bool) { var expectedTotalQueries int if withCache { err := app.ReloadCachedCollections() if err != nil { t.Fatal(err) } } else { app.Store().Reset(nil) expectedTotalQueries = 1 } totalQueries = 0 result, err := app.FindCachedCollectionReferences( 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_one_unique", "rel_many_no_cascade", "rel_many_no_cascade_required", "rel_many_cascade", "rel_many_unique", } 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 !slices.Contains(expectedFields, f.GetName()) { t.Fatalf("[%d] Didn't expect field %v", i, f) } } } if totalQueries != expectedTotalQueries { t.Fatalf("Expected %d totalQueries, got %d", expectedTotalQueries, totalQueries) } } run(true) run(false) } func TestIsCollectionNameUnique(t *testing.T) { t.Parallel() 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, s := range scenarios { t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) { result := app.IsCollectionNameUnique(s.name, s.excludeId) if result != s.expected { t.Errorf("Expected %v, got %v", s.expected, result) } }) } } func TestFindCollectionTruncate(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() countFiles := func(collectionId string) (int, error) { entries, err := os.ReadDir(filepath.Join(app.DataDir(), "storage", collectionId)) return len(entries), err } t.Run("truncate view", func(t *testing.T) { view2, err := app.FindCollectionByNameOrId("view2") if err != nil { t.Fatal(err) } err = app.TruncateCollection(view2) if err == nil { t.Fatalf("Expected truncate to fail because view collections can't be truncated") } }) t.Run("truncate failure", func(t *testing.T) { demo3, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatal(err) } originalTotalRecords, err := app.CountRecords(demo3) if err != nil { t.Fatal(err) } originalTotalFiles, err := countFiles(demo3.Id) if err != nil { t.Fatal(err) } err = app.TruncateCollection(demo3) if err == nil { t.Fatalf("Expected truncate to fail due to cascade delete failed required constraint") } // short delay to ensure that the file delete goroutine has been executed time.Sleep(100 * time.Millisecond) totalRecords, err := app.CountRecords(demo3) if err != nil { t.Fatal(err) } if totalRecords != originalTotalRecords { t.Fatalf("Expected %d records, got %d", originalTotalRecords, totalRecords) } totalFiles, err := countFiles(demo3.Id) if err != nil { t.Fatal(err) } if totalFiles != originalTotalFiles { t.Fatalf("Expected %d files, got %d", originalTotalFiles, totalFiles) } }) t.Run("truncate success", func(t *testing.T) { demo5, err := app.FindCollectionByNameOrId("demo5") if err != nil { t.Fatal(err) } err = app.TruncateCollection(demo5) if err != nil { t.Fatal(err) } // short delay to ensure that the file delete goroutine has been executed time.Sleep(100 * time.Millisecond) total, err := app.CountRecords(demo5) if err != nil { t.Fatal(err) } if total != 0 { t.Fatalf("Expected all records to be deleted, got %v", total) } totalFiles, err := countFiles(demo5.Id) if err != nil { t.Fatal(err) } if totalFiles != 0 { t.Fatalf("Expected truncated record files to be deleted, got %d", totalFiles) } // try to truncate again (shouldn't return an error) err = app.TruncateCollection(demo5) if err != nil { t.Fatal(err) } }) }