1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-02-14 17:00:06 +02:00
pocketbase/core/collection_query_test.go
2024-12-12 22:55:55 +02:00

475 lines
10 KiB
Go

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