1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-02-05 10:45:09 +02:00
pocketbase/core/view_test.go
2024-09-29 21:09:46 +03:00

638 lines
14 KiB
Go

package core_test
import (
"encoding/json"
"fmt"
"slices"
"testing"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
)
func ensureNoTempViews(app core.App, t *testing.T) {
var total int
err := app.DB().Select("count(*)").
From("sqlite_schema").
AndWhere(dbx.HashExp{"type": "view"}).
AndWhere(dbx.NewExp(`[[name]] LIKE '%\_temp\_%' ESCAPE '\'`)).
Limit(1).
Row(&total)
if err != nil {
t.Fatalf("Failed to check for temp views: %v", err)
}
if total > 0 {
t.Fatalf("Expected all temp views to be deleted, got %d", total)
}
}
func TestDeleteView(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
viewName string
expectError bool
}{
{"", true},
{"demo1", true}, // not a view table
{"missing", false}, // missing or already deleted
{"view1", false}, // existing
{"VieW1", false}, // view names are case insensitives
}
for i, s := range scenarios {
err := app.DeleteView(s.viewName)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%d - %q] Expected hasErr %v, got %v (%v)", i, s.viewName, s.expectError, hasErr, err)
}
}
ensureNoTempViews(app, t)
}
func TestSaveView(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
scenarioName string
viewName string
query string
expectError bool
expectColumns []string
}{
{
"empty name and query",
"",
"",
true,
nil,
},
{
"empty name",
"",
"select * from " + core.CollectionNameSuperusers,
true,
nil,
},
{
"empty query",
"123Test",
"",
true,
nil,
},
{
"invalid query",
"123Test",
"123 456",
true,
nil,
},
{
"missing table",
"123Test",
"select id from missing",
true,
nil,
},
{
"non select query",
"123Test",
"drop table " + core.CollectionNameSuperusers,
true,
nil,
},
{
"multiple select queries",
"123Test",
"select *, count(id) as c from " + core.CollectionNameSuperusers + "; select * from demo1;",
true,
nil,
},
{
"try to break the parent parenthesis",
"123Test",
"select *, count(id) as c from `" + core.CollectionNameSuperusers + "`)",
true,
nil,
},
{
"simple select query (+ trimmed semicolon)",
"123Test",
";select *, count(id) as c from " + core.CollectionNameSuperusers + ";",
false,
[]string{
"id", "created", "updated",
"password", "tokenKey", "email",
"emailVisibility", "verified",
"c",
},
},
{
"update old view with new query",
"123Test",
"select 1 as test from " + core.CollectionNameSuperusers,
false,
[]string{"test"},
},
}
for _, s := range scenarios {
t.Run(s.scenarioName, func(t *testing.T) {
err := app.SaveView(s.viewName, s.query)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
if hasErr {
return
}
infoRows, err := app.TableInfo(s.viewName)
if err != nil {
t.Fatalf("Failed to fetch table info for %s: %v", s.viewName, err)
}
if len(s.expectColumns) != len(infoRows) {
t.Fatalf("Expected %d columns, got %d", len(s.expectColumns), len(infoRows))
}
for _, row := range infoRows {
if !slices.Contains(s.expectColumns, row.Name) {
t.Fatalf("Missing %q column in %v", row.Name, s.expectColumns)
}
}
})
}
ensureNoTempViews(app, t)
}
func TestCreateViewFieldsWithDiscardedNestedTransaction(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
app.RunInTransaction(func(txApp core.App) error {
_, err := txApp.CreateViewFields("select id from missing")
if err == nil {
t.Fatal("Expected error, got nil")
}
return nil
})
ensureNoTempViews(app, t)
}
func TestCreateViewFields(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
name string
query string
expectError bool
expectFields map[string]string // name-type pairs
}{
{
"empty query",
"",
true,
nil,
},
{
"invalid query",
"test 123456",
true,
nil,
},
{
"missing table",
"select id from missing",
true,
nil,
},
{
"query with wildcard column",
"select a.id, a.* from demo1 a",
true,
nil,
},
{
"query without id",
"select text, url, created, updated from demo1",
true,
nil,
},
{
"query with comments",
`
select
-- test single line
id,
text,
/* multi
line comment */
url, created, updated from demo1
`,
false,
map[string]string{
"id": core.FieldTypeText,
"text": core.FieldTypeText,
"url": core.FieldTypeURL,
"created": core.FieldTypeAutodate,
"updated": core.FieldTypeAutodate,
},
},
{
"query with all fields and quoted identifiers",
`
select
"id",
"created",
"updated",
[text],
` + "`bool`" + `,
"url",
"select_one",
"select_many",
"file_one",
"demo1"."file_many",
` + "`demo1`." + "`number`" + ` number_alias,
"email",
"datetime",
"json",
"rel_one",
"rel_many",
'single_quoted_custom_literal' as 'single_quoted_column'
from demo1
`,
false,
map[string]string{
"id": core.FieldTypeText,
"created": core.FieldTypeAutodate,
"updated": core.FieldTypeAutodate,
"text": core.FieldTypeText,
"bool": core.FieldTypeBool,
"url": core.FieldTypeURL,
"select_one": core.FieldTypeSelect,
"select_many": core.FieldTypeSelect,
"file_one": core.FieldTypeFile,
"file_many": core.FieldTypeFile,
"number_alias": core.FieldTypeNumber,
"email": core.FieldTypeEmail,
"datetime": core.FieldTypeDate,
"json": core.FieldTypeJSON,
"rel_one": core.FieldTypeRelation,
"rel_many": core.FieldTypeRelation,
"single_quoted_column": core.FieldTypeJSON,
},
},
{
"query with indirect relations fields",
"select a.id, b.id as bid, b.created from demo1 as a left join demo2 b",
false,
map[string]string{
"id": core.FieldTypeText,
"bid": core.FieldTypeRelation,
"created": core.FieldTypeAutodate,
},
},
{
"query with multiple froms, joins and style of aliasses",
`
select
a.id as id,
b.id as bid,
lj.id cid,
ij.id as did,
a.bool,
` + core.CollectionNameSuperusers + `.id as eid,
` + core.CollectionNameSuperusers + `.email
from demo1 a, demo2 as b
left join demo3 lj on lj.id = 123
inner join demo4 as ij on ij.id = 123
join ` + core.CollectionNameSuperusers + `
where 1=1
group by a.id
limit 10
`,
false,
map[string]string{
"id": core.FieldTypeText,
"bid": core.FieldTypeRelation,
"cid": core.FieldTypeRelation,
"did": core.FieldTypeRelation,
"bool": core.FieldTypeBool,
"eid": core.FieldTypeRelation,
"email": core.FieldTypeEmail,
},
},
{
"query with casts",
`select
a.id,
count(a.id) count,
cast(a.id as int) cast_int,
cast(a.id as integer) cast_integer,
cast(a.id as real) cast_real,
cast(a.id as decimal) cast_decimal,
cast(a.id as numeric) cast_numeric,
cast(a.id as text) cast_text,
cast(a.id as bool) cast_bool,
cast(a.id as boolean) cast_boolean,
avg(a.id) avg,
sum(a.id) sum,
total(a.id) total,
min(a.id) min,
max(a.id) max
from demo1 a`,
false,
map[string]string{
"id": core.FieldTypeText,
"count": core.FieldTypeNumber,
"total": core.FieldTypeNumber,
"cast_int": core.FieldTypeNumber,
"cast_integer": core.FieldTypeNumber,
"cast_real": core.FieldTypeNumber,
"cast_decimal": core.FieldTypeNumber,
"cast_numeric": core.FieldTypeNumber,
"cast_text": core.FieldTypeText,
"cast_bool": core.FieldTypeBool,
"cast_boolean": core.FieldTypeBool,
// json because they are nullable
"sum": core.FieldTypeJSON,
"avg": core.FieldTypeJSON,
"min": core.FieldTypeJSON,
"max": core.FieldTypeJSON,
},
},
{
"query with reserved auth collection fields",
`
select
a.id,
a.username,
a.email,
a.emailVisibility,
a.verified,
demo1.id relid
from users a
left join demo1
`,
false,
map[string]string{
"id": core.FieldTypeText,
"username": core.FieldTypeText,
"email": core.FieldTypeEmail,
"emailVisibility": core.FieldTypeBool,
"verified": core.FieldTypeBool,
"relid": core.FieldTypeRelation,
},
},
{
"query with unknown fields and aliases",
`select
id,
id as id2,
text as text_alias,
url as url_alias,
"demo1"."bool" as bool_alias,
number as number_alias,
created created_alias,
updated updated_alias,
123 as custom
from demo1`,
false,
map[string]string{
"id": core.FieldTypeText,
"id2": core.FieldTypeRelation,
"text_alias": core.FieldTypeText,
"url_alias": core.FieldTypeURL,
"bool_alias": core.FieldTypeBool,
"number_alias": core.FieldTypeNumber,
"created_alias": core.FieldTypeAutodate,
"updated_alias": core.FieldTypeAutodate,
"custom": core.FieldTypeJSON,
},
},
{
"query with distinct and reordered id column",
`select distinct
id as id2,
id,
123 as custom
from demo1`,
false,
map[string]string{
"id2": core.FieldTypeRelation,
"id": core.FieldTypeText,
"custom": core.FieldTypeJSON,
},
},
{
"query with aliasing the same field multiple times",
`select
a.id as id,
a.text as alias1,
a.text as alias2,
b.text as alias3,
b.text as alias4
from demo1 a
left join demo1 as b`,
false,
map[string]string{
"id": core.FieldTypeText,
"alias1": core.FieldTypeText,
"alias2": core.FieldTypeText,
"alias3": core.FieldTypeText,
"alias4": core.FieldTypeText,
},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result, err := app.CreateViewFields(s.query)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
if hasErr {
return
}
if len(s.expectFields) != len(result) {
serialized, _ := json.Marshal(result)
t.Fatalf("Expected %d fields, got %d: \n%s", len(s.expectFields), len(result), serialized)
}
for name, typ := range s.expectFields {
field := result.GetByName(name)
if field == nil {
t.Fatalf("Expected to find field %s, got nil", name)
}
if field.Type() != typ {
t.Fatalf("Expected field %s to be %q, got %q", name, typ, field.Type())
}
}
})
}
ensureNoTempViews(app, t)
}
func TestFindRecordByViewFile(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
prevCollection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
totalLevels := 6
// create collection view mocks
fileOneAlias := "file_one one0"
fileManyAlias := "file_many many0"
mockCollections := make([]*core.Collection, 0, totalLevels)
for i := 0; i <= totalLevels; i++ {
view := new(core.Collection)
view.Type = core.CollectionTypeView
view.Name = fmt.Sprintf("_test_view%d", i)
view.ViewQuery = fmt.Sprintf(
"select id, %s, %s from %s",
fileOneAlias,
fileManyAlias,
prevCollection.Name,
)
// save view
if err := app.Save(view); err != nil {
t.Fatalf("Failed to save view%d: %v", i, err)
}
mockCollections = append(mockCollections, view)
prevCollection = view
fileOneAlias = fmt.Sprintf("one%d one%d", i, i+1)
fileManyAlias = fmt.Sprintf("many%d many%d", i, i+1)
}
fileOneName := "test_d61b33QdDU.txt"
fileManyName := "test_QZFjKjXchk.txt"
expectedRecordId := "84nmscqy84lsi1t"
scenarios := []struct {
name string
collectionNameOrId string
fileFieldName string
filename string
expectError bool
expectRecordId string
}{
{
"missing collection",
"missing",
"a",
fileOneName,
true,
"",
},
{
"non-view collection",
"demo1",
"file_one",
fileOneName,
true,
"",
},
{
"view collection after the max recursion limit",
mockCollections[totalLevels-1].Name,
fmt.Sprintf("one%d", totalLevels-1),
fileOneName,
true,
"",
},
{
"first view collection (single file)",
mockCollections[0].Name,
"one0",
fileOneName,
false,
expectedRecordId,
},
{
"first view collection (many files)",
mockCollections[0].Name,
"many0",
fileManyName,
false,
expectedRecordId,
},
{
"last view collection before the recursion limit (single file)",
mockCollections[totalLevels-2].Name,
fmt.Sprintf("one%d", totalLevels-2),
fileOneName,
false,
expectedRecordId,
},
{
"last view collection before the recursion limit (many files)",
mockCollections[totalLevels-2].Name,
fmt.Sprintf("many%d", totalLevels-2),
fileManyName,
false,
expectedRecordId,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
record, err := app.FindRecordByViewFile(
s.collectionNameOrId,
s.fileFieldName,
s.filename,
)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
if hasErr {
return
}
if record.Id != s.expectRecordId {
t.Fatalf("Expected recordId %q, got %q", s.expectRecordId, record.Id)
}
})
}
}