package core_test import ( "encoding/json" "errors" "fmt" "slices" "strings" "testing" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/types" ) func TestRecordQueryWithDifferentCollectionValues(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } scenarios := []struct { name string collection any expectedTotal int expectError bool }{ {"with nil value", nil, 0, true}, {"with invalid or missing collection id/name", "missing", 0, true}, {"with pointer model", collection, 3, false}, {"with value model", *collection, 3, false}, {"with name", "demo1", 3, false}, {"with id", "wsmn24bux7wo113", 3, false}, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { var records []*core.Record err := app.RecordQuery(s.collection).All(&records) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasError %v, got %v", s.expectError, hasErr) } if total := len(records); total != s.expectedTotal { t.Fatalf("Expected %d records, got %d", s.expectedTotal, total) } }) } } func TestRecordQueryOne(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string collection string recordId string model core.Model }{ { "record model", "demo1", "84nmscqy84lsi1t", &core.Record{}, }, { "record proxy", "demo1", "84nmscqy84lsi1t", &struct { core.BaseRecordProxy }{}, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { collection, err := app.FindCollectionByNameOrId(s.collection) if err != nil { t.Fatal(err) } q := app.RecordQuery(collection). Where(dbx.HashExp{"id": s.recordId}) if err := q.One(s.model); err != nil { t.Fatal(err) } if s.model.PK() != s.recordId { t.Fatalf("Expected record with id %q, got %q", s.recordId, s.model.PK()) } }) } } func TestRecordQueryAll(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() type mockRecordProxy struct { core.BaseRecordProxy } scenarios := []struct { name string collection string recordIds []any result any }{ { "slice of Record models", "demo1", []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, &[]core.Record{}, }, { "slice of pointer Record models", "demo1", []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, &[]*core.Record{}, }, { "slice of Record proxies", "demo1", []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, &[]mockRecordProxy{}, }, { "slice of pointer Record proxies", "demo1", []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, &[]mockRecordProxy{}, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { collection, err := app.FindCollectionByNameOrId(s.collection) if err != nil { t.Fatal(err) } q := app.RecordQuery(collection). Where(dbx.HashExp{"id": s.recordIds}) if err := q.All(s.result); err != nil { t.Fatal(err) } raw, err := json.Marshal(s.result) if err != nil { t.Fatal(err) } rawStr := string(raw) sliceOfMaps := []any{} if err := json.Unmarshal(raw, &sliceOfMaps); err != nil { t.Fatal(err) } if len(sliceOfMaps) != len(s.recordIds) { t.Fatalf("Expected %d items, got %d", len(s.recordIds), len(sliceOfMaps)) } for _, id := range s.recordIds { if !strings.Contains(rawStr, fmt.Sprintf(`"id":%q`, id)) { t.Fatalf("Missing id %q in\n%s", id, rawStr) } } }) } } func TestFindRecordById(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionIdOrName string id string filters []func(q *dbx.SelectQuery) error expectError bool }{ {"demo2", "missing", nil, true}, {"missing", "0yxhwia2amd8gec", nil, true}, {"demo2", "0yxhwia2amd8gec", nil, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{}, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{nil, nil}, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ nil, func(q *dbx.SelectQuery) error { return nil }, }, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"title": "missing"}) return nil }, }, true}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { return errors.New("test error") }, }, true}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"title": "test3"}) return nil }, }, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"title": "test3"}) return nil }, nil, }, false}, {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"title": "test3"}) return nil }, func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"active": false}) return nil }, }, true}, {"sz5l5z67tg7gku0", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"title": "test3"}) return nil }, func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"active": true}) return nil }, }, false}, } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s_%s_%d", i, s.collectionIdOrName, s.id, len(s.filters)), func(t *testing.T) { record, err := app.FindRecordById( s.collectionIdOrName, s.id, s.filters..., ) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if record != nil && record.Id != s.id { t.Fatalf("Expected record with id %s, got %s", s.id, record.Id) } }) } } func TestFindRecordsByIds(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionIdOrName string ids []string filters []func(q *dbx.SelectQuery) error expectTotal int expectError bool }{ {"demo2", []string{}, nil, 0, false}, {"demo2", []string{""}, nil, 0, false}, {"demo2", []string{"missing"}, nil, 0, false}, {"missing", []string{"0yxhwia2amd8gec"}, nil, 0, true}, {"demo2", []string{"0yxhwia2amd8gec"}, nil, 1, false}, {"sz5l5z67tg7gku0", []string{"0yxhwia2amd8gec"}, nil, 1, false}, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, nil, 2, false, }, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{}, 2, false, }, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{nil, nil}, 2, false, }, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { return nil // empty filter }, }, 2, false, }, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { return nil // empty filter }, func(q *dbx.SelectQuery) error { return errors.New("test error") }, }, 0, true, }, { "demo2", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"active": true}) return nil }, nil, }, 1, false, }, { "sz5l5z67tg7gku0", []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, []func(q *dbx.SelectQuery) error{ func(q *dbx.SelectQuery) error { q.AndWhere(dbx.HashExp{"active": true}) return nil }, func(q *dbx.SelectQuery) error { q.AndWhere(dbx.Not(dbx.HashExp{"title": ""})) return nil }, }, 1, false, }, } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s_%v_%d", i, s.collectionIdOrName, s.ids, len(s.filters)), func(t *testing.T) { records, err := app.FindRecordsByIds( s.collectionIdOrName, s.ids, s.filters..., ) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if len(records) != s.expectTotal { t.Fatalf("Expected %d records, got %d", s.expectTotal, len(records)) } for _, r := range records { if !slices.Contains(s.ids, r.Id) { t.Fatalf("Couldn't find id %s in %v", r.Id, s.ids) } } }) } } func TestFindAllRecords(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionIdOrName string expressions []dbx.Expression expectIds []string expectError bool }{ { "missing", nil, []string{}, true, }, { "demo2", nil, []string{ "achvryl401bhse3", "llvuca81nly1qls", "0yxhwia2amd8gec", }, false, }, { "demo2", []dbx.Expression{ nil, dbx.HashExp{"id": "123"}, }, []string{}, false, }, { "sz5l5z67tg7gku0", []dbx.Expression{ dbx.Like("title", "test").Match(true, true), dbx.HashExp{"active": true}, }, []string{ "achvryl401bhse3", "0yxhwia2amd8gec", }, false, }, } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s", i, s.collectionIdOrName), func(t *testing.T) { records, err := app.FindAllRecords(s.collectionIdOrName, s.expressions...) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if len(records) != len(s.expectIds) { t.Fatalf("Expected %d records, got %d", len(s.expectIds), len(records)) } for _, r := range records { if !slices.Contains(s.expectIds, r.Id) { t.Fatalf("Couldn't find id %s in %v", r.Id, s.expectIds) } } }) } } func TestFindFirstRecordByData(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionIdOrName string key string value any expectId string expectError bool }{ { "missing", "id", "llvuca81nly1qls", "llvuca81nly1qls", true, }, { "demo2", "", "llvuca81nly1qls", "", true, }, { "demo2", "id", "invalid", "", true, }, { "demo2", "id", "llvuca81nly1qls", "llvuca81nly1qls", false, }, { "sz5l5z67tg7gku0", "title", "test3", "0yxhwia2amd8gec", false, }, } for i, s := range scenarios { t.Run(fmt.Sprintf("%d_%s_%s_%v", i, s.collectionIdOrName, s.key, s.value), func(t *testing.T) { record, err := app.FindFirstRecordByData(s.collectionIdOrName, s.key, s.value) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if !s.expectError && record.Id != s.expectId { t.Fatalf("Expected record with id %s, got %v", s.expectId, record.Id) } }) } } func TestFindRecordsByFilter(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string collectionIdOrName string filter string sort string limit int offset int params []dbx.Params expectError bool expectRecordIds []string }{ { "missing collection", "missing", "id != ''", "", 0, 0, nil, true, nil, }, { "invalid filter", "demo2", "someMissingField > 1", "", 0, 0, nil, true, nil, }, { "empty filter", "demo2", "", "", 0, 0, nil, false, []string{ "llvuca81nly1qls", "achvryl401bhse3", "0yxhwia2amd8gec", }, }, { "simple filter", "demo2", "id != ''", "", 0, 0, nil, false, []string{ "llvuca81nly1qls", "achvryl401bhse3", "0yxhwia2amd8gec", }, }, { "multi-condition filter with sort", "demo2", "id != '' && active=true", "-created,title", -1, // should behave the same as 0 0, nil, false, []string{ "0yxhwia2amd8gec", "achvryl401bhse3", }, }, { "with limit and offset", "sz5l5z67tg7gku0", "id != ''", "title", 2, 1, nil, false, []string{ "achvryl401bhse3", "0yxhwia2amd8gec", }, }, { "with placeholder params", "demo2", "active = {:active}", "", 10, 0, []dbx.Params{{"active": false}}, false, []string{ "llvuca81nly1qls", }, }, { "with json filter and sort", "demo4", "json_object != null && json_object.a.b = 'test'", "-json_object.a", 10, 0, []dbx.Params{{"active": false}}, false, []string{ "i9naidtvr6qsgb4", }, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { records, err := app.FindRecordsByFilter( s.collectionIdOrName, s.filter, s.sort, s.limit, s.offset, s.params..., ) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if hasErr { return } if len(records) != len(s.expectRecordIds) { t.Fatalf("Expected %d records, got %d", len(s.expectRecordIds), len(records)) } for i, id := range s.expectRecordIds { if id != records[i].Id { t.Fatalf("Expected record with id %q, got %q at index %d", id, records[i].Id, i) } } }) } } func TestFindFirstRecordByFilter(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string collectionIdOrName string filter string params []dbx.Params expectError bool expectRecordId string }{ { "missing collection", "missing", "id != ''", nil, true, "", }, { "invalid filter", "demo2", "someMissingField > 1", nil, true, "", }, { "empty filter", "demo2", "", nil, false, "llvuca81nly1qls", }, { "valid filter but no matches", "demo2", "id = 'test'", nil, true, "", }, { "valid filter and multiple matches", "sz5l5z67tg7gku0", "id != ''", nil, false, "llvuca81nly1qls", }, { "with placeholder params", "demo2", "active = {:active}", []dbx.Params{{"active": false}}, false, "llvuca81nly1qls", }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { record, err := app.FindFirstRecordByFilter(s.collectionIdOrName, s.filter, s.params...) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if hasErr { return } if record.Id != s.expectRecordId { t.Fatalf("Expected record with id %q, got %q", s.expectRecordId, record.Id) } }) } } func TestCountRecords(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string collectionIdOrName string expressions []dbx.Expression expectTotal int64 expectError bool }{ { "missing collection", "missing", nil, 0, true, }, { "valid collection name", "demo2", nil, 3, false, }, { "valid collection id", "sz5l5z67tg7gku0", nil, 3, false, }, { "nil expression", "demo2", []dbx.Expression{nil}, 3, false, }, { "no matches", "demo2", []dbx.Expression{ nil, dbx.Like("title", "missing"), dbx.HashExp{"active": true}, }, 0, false, }, { "with matches", "demo2", []dbx.Expression{ nil, dbx.Like("title", "test"), dbx.HashExp{"active": true}, }, 2, false, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { total, err := app.CountRecords(s.collectionIdOrName, s.expressions...) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if total != s.expectTotal { t.Fatalf("Expected total %d, got %d", s.expectTotal, total) } }) } } func TestFindAuthRecordByToken(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { name string token string types []string expectedId string }{ { "empty token", "", nil, "", }, { "invalid token", "invalid", nil, "", }, { "expired token", "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.2D3tmqPn3vc5LoqqCz8V-iCDVXo9soYiH0d32G7FQT4", nil, "", }, { "valid auth token", "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", nil, "4q1xlclmfloku33", }, { "valid verification token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.5GmuZr4vmwk3Cb_3ZZWNxwbE75KZC-j71xxIPR9AsVw", nil, "dc49k6jgejn40h3", }, { "auth token with file type only check", "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", []string{core.TokenTypeFile}, "", }, { "auth token with file and auth type check", "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", []string{core.TokenTypeFile, core.TokenTypeAuth}, "4q1xlclmfloku33", }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { record, err := app.FindAuthRecordByToken(s.token, s.types...) hasErr := err != nil expectErr := s.expectedId == "" if hasErr != expectErr { t.Fatalf("Expected hasErr to be %v, got %v (%v)", expectErr, hasErr, err) } if hasErr { return } if record.Id != s.expectedId { t.Fatalf("Expected record with id %q, got %q", s.expectedId, record.Id) } }) } } func TestFindAuthRecordByEmail(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { collectionIdOrName string email string expectError bool }{ {"missing", "test@example.com", true}, {"demo2", "test@example.com", true}, {"users", "missing@example.com", true}, {"users", "test@example.com", false}, {"clients", "test2@example.com", false}, } for _, s := range scenarios { t.Run(fmt.Sprintf("%s_%s", s.collectionIdOrName, s.email), func(t *testing.T) { record, err := app.FindAuthRecordByEmail(s.collectionIdOrName, s.email) hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) } if hasErr { return } if record.Email() != s.email { t.Fatalf("Expected record with email %s, got %s", s.email, record.Email()) } }) } } func TestCanAccessRecord(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") if err != nil { t.Fatal(err) } user, err := app.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } record, err := app.FindRecordById("demo1", "imy661ixudk5izi") if err != nil { t.Fatal(err) } scenarios := []struct { name string record *core.Record requestInfo *core.RequestInfo rule *string expected bool expectError bool }{ { "as superuser with nil rule", record, &core.RequestInfo{ Auth: superuser, }, nil, true, false, }, { "as superuser with non-empty rule", record, &core.RequestInfo{ Auth: superuser, }, types.Pointer("id = ''"), // the filter rule should be ignored true, false, }, { "as superuser with invalid rule", record, &core.RequestInfo{ Auth: superuser, }, types.Pointer("id ?!@ 1"), // the filter rule should be ignored true, false, }, { "as guest with nil rule", record, &core.RequestInfo{}, nil, false, false, }, { "as guest with empty rule", record, &core.RequestInfo{}, types.Pointer(""), true, false, }, { "as guest with invalid rule", record, &core.RequestInfo{}, types.Pointer("id ?!@ 1"), false, true, }, { "as guest with mismatched rule", record, &core.RequestInfo{}, types.Pointer("@request.auth.id != ''"), false, false, }, { "as guest with matched rule", record, &core.RequestInfo{ Body: map[string]any{"test": 1}, }, types.Pointer("@request.auth.id != '' || @request.body.test = 1"), true, false, }, { "as auth record with nil rule", record, &core.RequestInfo{ Auth: user, }, nil, false, false, }, { "as auth record with empty rule", record, &core.RequestInfo{ Auth: user, }, types.Pointer(""), true, false, }, { "as auth record with invalid rule", record, &core.RequestInfo{ Auth: user, }, types.Pointer("id ?!@ 1"), false, true, }, { "as auth record with mismatched rule", record, &core.RequestInfo{ Auth: user, Body: map[string]any{"test": 1}, }, types.Pointer("@request.auth.id != '' && @request.body.test > 1"), false, false, }, { "as auth record with matched rule", record, &core.RequestInfo{ Auth: user, Body: map[string]any{"test": 2}, }, types.Pointer("@request.auth.id != '' && @request.body.test > 1"), true, false, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { result, err := app.CanAccessRecord(s.record, s.requestInfo, s.rule) if result != s.expected { t.Fatalf("Expected %v, got %v", s.expected, result) } hasErr := err != nil if hasErr != s.expectError { t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } }) } }