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

1336 lines
34 KiB
Go

package core_test
import (
"encoding/json"
"fmt"
"slices"
"strings"
"testing"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/dbutils"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestNewCollection(t *testing.T) {
t.Parallel()
scenarios := []struct {
typ string
name string
expected []string
}{
{
"",
"",
[]string{
`"id":""`,
`"name":""`,
`"type":"base"`,
`"system":false`,
`"indexes":[]`,
`"fields":[{`,
`"name":"id"`,
`"type":"text"`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"unknown",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
`"indexes":[]`,
`"fields":[{`,
`"name":"id"`,
`"type":"text"`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"base",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
`"indexes":[]`,
`"fields":[{`,
`"name":"id"`,
`"type":"text"`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"view",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"view"`,
`"indexes":[]`,
`"fields":[]`,
`"system":false`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"auth",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"auth"`,
`"fields":[{`,
`"system":false`,
`"type":"text"`,
`"type":"email"`,
`"name":"id"`,
`"name":"email"`,
`"name":"password"`,
`"name":"tokenKey"`,
`"name":"emailVisibility"`,
`"name":"verified"`,
`idx_email`,
`idx_tokenKey`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
`"identityFields":["email"]`,
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s_%s", i, s.typ, s.name), func(t *testing.T) {
result := core.NewCollection(s.typ, s.name).String()
for _, part := range s.expected {
if !strings.Contains(result, part) {
t.Fatalf("Missing part %q in\n%v", part, result)
}
}
})
}
}
func TestNewBaseCollection(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
expected []string
}{
{
"",
[]string{
`"id":""`,
`"name":""`,
`"type":"base"`,
`"system":false`,
`"indexes":[]`,
`"fields":[{`,
`"name":"id"`,
`"type":"text"`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
`"indexes":[]`,
`"fields":[{`,
`"name":"id"`,
`"type":"text"`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) {
result := core.NewBaseCollection(s.name).String()
for _, part := range s.expected {
if !strings.Contains(result, part) {
t.Fatalf("Missing part %q in\n%v", part, result)
}
}
})
}
}
func TestNewViewCollection(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
expected []string
}{
{
"",
[]string{
`"id":""`,
`"name":""`,
`"type":"view"`,
`"indexes":[]`,
`"fields":[]`,
`"system":false`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"view"`,
`"indexes":[]`,
`"fields":[]`,
`"system":false`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) {
result := core.NewViewCollection(s.name).String()
for _, part := range s.expected {
if !strings.Contains(result, part) {
t.Fatalf("Missing part %q in\n%v", part, result)
}
}
})
}
}
func TestNewAuthCollection(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
expected []string
}{
{
"",
[]string{
`"id":""`,
`"name":""`,
`"type":"auth"`,
`"fields":[{`,
`"system":false`,
`"type":"text"`,
`"type":"email"`,
`"name":"id"`,
`"name":"email"`,
`"name":"password"`,
`"name":"tokenKey"`,
`"name":"emailVisibility"`,
`"name":"verified"`,
`idx_email`,
`idx_tokenKey`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
`"identityFields":["email"]`,
},
},
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"auth"`,
`"fields":[{`,
`"system":false`,
`"type":"text"`,
`"type":"email"`,
`"name":"id"`,
`"name":"email"`,
`"name":"password"`,
`"name":"tokenKey"`,
`"name":"emailVisibility"`,
`"name":"verified"`,
`idx_email`,
`idx_tokenKey`,
`"listRule":null`,
`"viewRule":null`,
`"createRule":null`,
`"updateRule":null`,
`"deleteRule":null`,
`"identityFields":["email"]`,
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) {
result := core.NewAuthCollection(s.name).String()
for _, part := range s.expected {
if !strings.Contains(result, part) {
t.Fatalf("Missing part %q in\n%v", part, result)
}
}
})
}
}
func TestCollectionTableName(t *testing.T) {
t.Parallel()
c := core.NewBaseCollection("test")
if c.TableName() != "_collections" {
t.Fatalf("Expected tableName %q, got %q", "_collections", c.TableName())
}
}
func TestCollectionBaseFilesPath(t *testing.T) {
t.Parallel()
c := core.Collection{}
if c.BaseFilesPath() != "" {
t.Fatalf("Expected empty string, got %q", c.BaseFilesPath())
}
c.Id = "test"
if c.BaseFilesPath() != c.Id {
t.Fatalf("Expected %q, got %q", c.Id, c.BaseFilesPath())
}
}
func TestCollectionIsBase(t *testing.T) {
t.Parallel()
scenarios := []struct {
typ string
expected bool
}{
{"unknown", false},
{core.CollectionTypeBase, true},
{core.CollectionTypeView, false},
{core.CollectionTypeAuth, false},
}
for _, s := range scenarios {
t.Run(s.typ, func(t *testing.T) {
c := core.Collection{}
c.Type = s.typ
if v := c.IsBase(); v != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, v)
}
})
}
}
func TestCollectionIsView(t *testing.T) {
t.Parallel()
scenarios := []struct {
typ string
expected bool
}{
{"unknown", false},
{core.CollectionTypeBase, false},
{core.CollectionTypeView, true},
{core.CollectionTypeAuth, false},
}
for _, s := range scenarios {
t.Run(s.typ, func(t *testing.T) {
c := core.Collection{}
c.Type = s.typ
if v := c.IsView(); v != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, v)
}
})
}
}
func TestCollectionIsAuth(t *testing.T) {
t.Parallel()
scenarios := []struct {
typ string
expected bool
}{
{"unknown", false},
{core.CollectionTypeBase, false},
{core.CollectionTypeView, false},
{core.CollectionTypeAuth, true},
}
for _, s := range scenarios {
t.Run(s.typ, func(t *testing.T) {
c := core.Collection{}
c.Type = s.typ
if v := c.IsAuth(); v != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, v)
}
})
}
}
func TestCollectionPostScan(t *testing.T) {
t.Parallel()
rawOptions := types.JSONRaw(`{
"viewQuery":"select 1",
"authRule":"1=2"
}`)
scenarios := []struct {
typ string
rawOptions types.JSONRaw
expected []string
}{
{
core.CollectionTypeBase,
rawOptions,
[]string{
`lastSavedPK:"test"`,
`ViewQuery:""`,
`AuthRule:(*string)(nil)`,
},
},
{
core.CollectionTypeView,
rawOptions,
[]string{
`lastSavedPK:"test"`,
`ViewQuery:"select 1"`,
`AuthRule:(*string)(nil)`,
},
},
{
core.CollectionTypeAuth,
rawOptions,
[]string{
`lastSavedPK:"test"`,
`ViewQuery:""`,
`AuthRule:(*string)(0x`,
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) {
c := core.Collection{}
c.Id = "test"
c.Type = s.typ
c.RawOptions = s.rawOptions
err := c.PostScan()
if err != nil {
t.Fatal(err)
}
if c.IsNew() {
t.Fatal("Expected the collection to be marked as not new")
}
rawModel := fmt.Sprintf("%#v", c)
for _, part := range s.expected {
if !strings.Contains(rawModel, part) {
t.Fatalf("Missing part %q in\n%v", part, rawModel)
}
}
})
}
}
func TestCollectionUnmarshalJSON(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
name string
raw string
collection func() *core.Collection
expectedCollection func() *core.Collection
}{
{
"base new empty",
`{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewBaseCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
},
},
{
"view new empty",
`{"type":"view","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewViewCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
},
},
{
"auth new empty",
`{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewAuthCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
},
},
{
"new but with set type (no default fields load)",
`{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
func() *core.Collection {
c := &core.Collection{}
c.Type = core.CollectionTypeBase
return c
},
func() *core.Collection {
c := &core.Collection{}
c.Type = core.CollectionTypeBase
c.Name = "test"
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
},
},
{
"existing (no default fields load)",
`{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
func() *core.Collection {
c, _ := app.FindCollectionByNameOrId("demo1")
return c
},
func() *core.Collection {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Type = core.CollectionTypeAuth
c.Name = "test"
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
collection := s.collection()
err := json.Unmarshal([]byte(s.raw), collection)
if err != nil {
t.Fatal(err)
}
rawResult, err := json.Marshal(collection)
if err != nil {
t.Fatal(err)
}
rawResultStr := string(rawResult)
rawExpected, err := json.Marshal(s.expectedCollection())
if err != nil {
t.Fatal(err)
}
rawExpectedStr := string(rawExpected)
if rawResultStr != rawExpectedStr {
t.Fatalf("Expected collection\n%s\ngot\n%s", rawExpectedStr, rawResultStr)
}
})
}
}
func TestCollectionSerialize(t *testing.T) {
scenarios := []struct {
name string
collection func() *core.Collection
expected []string
notExpected []string
}{
{
"base",
func() *core.Collection {
c := core.NewCollection(core.CollectionTypeBase, "test")
c.ViewQuery = "1=1"
c.OAuth2.Providers = []core.OAuth2ProviderConfig{
{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
}
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"base"`,
},
[]string{
"verificationTemplate",
"manageRule",
"authRule",
"secret",
"oauth2",
"clientId",
"clientSecret",
"viewQuery",
},
},
{
"view",
func() *core.Collection {
c := core.NewCollection(core.CollectionTypeView, "test")
c.ViewQuery = "1=1"
c.OAuth2.Providers = []core.OAuth2ProviderConfig{
{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
}
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"view"`,
`"viewQuery":"1=1"`,
},
[]string{
"verificationTemplate",
"manageRule",
"authRule",
"secret",
"oauth2",
"clientId",
"clientSecret",
},
},
{
"auth",
func() *core.Collection {
c := core.NewCollection(core.CollectionTypeAuth, "test")
c.ViewQuery = "1=1"
c.OAuth2.Providers = []core.OAuth2ProviderConfig{
{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
}
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"name":"test"`,
`"type":"auth"`,
`"oauth2":{`,
`"providers":[{`,
`"clientId":"test_client_id1"`,
`"clientId":"test_client_id2"`,
},
[]string{
"viewQuery",
"secret",
"clientSecret",
},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
collection := s.collection()
raw, err := collection.MarshalJSON()
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
if rawStr != collection.String() {
t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", collection.String(), rawStr)
}
for _, part := range s.expected {
if !strings.Contains(rawStr, part) {
t.Fatalf("Missing part %q in\n%v", part, rawStr)
}
}
for _, part := range s.notExpected {
if strings.Contains(rawStr, part) {
t.Fatalf("Didn't expect part %q in\n%v", part, rawStr)
}
}
})
}
}
func TestCollectionDBExport(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
date, err := types.ParseDateTime("2024-07-01 01:02:03.456Z")
if err != nil {
t.Fatal(err)
}
scenarios := []struct {
typ string
expected string
}{
{
"unknown",
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeBase,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeView,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeAuth,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) {
c := core.Collection{}
c.Type = s.typ
c.Id = "test_id"
c.Name = "test_name"
c.System = true
c.ListRule = types.Pointer("1=1")
c.ViewRule = types.Pointer("1=2")
c.CreateRule = types.Pointer("1=3")
c.UpdateRule = types.Pointer("1=4")
c.DeleteRule = types.Pointer("1=5")
c.ManageRule = types.Pointer("1=6")
c.ViewRule = types.Pointer("1=7")
c.Created = date
c.Updated = date
c.Indexes = types.JSONArray[string]{"CREATE INDEX idx1 on test_name(id)", "CREATE INDEX idx2 on test_name(id)"}
c.ViewQuery = "select 1"
c.Fields.Add(&core.BoolField{Name: "f1", System: true})
c.Fields.Add(&core.BoolField{Name: "f2", Required: true})
c.RawOptions = types.JSONRaw(`{"viewQuery": "select 2"}`) // should be ignored
result, err := c.DBExport(app)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
if str := string(raw); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
}
})
}
}
func TestCollectionIndexHelpers(t *testing.T) {
t.Parallel()
checkIndexes := func(t *testing.T, indexes, expectedIndexes []string) {
if len(indexes) != len(expectedIndexes) {
t.Fatalf("Expected %d indexes, got %d\n%v", len(expectedIndexes), len(indexes), indexes)
}
for _, idx := range expectedIndexes {
if !slices.Contains(indexes, idx) {
t.Fatalf("Missing index\n%v\nin\n%v", idx, indexes)
}
}
}
c := core.NewBaseCollection("test")
checkIndexes(t, c.Indexes, nil)
c.AddIndex("idx1", false, "colA,colB", "colA != 1")
c.AddIndex("idx2", true, "colA", "")
c.AddIndex("idx3", false, "colA", "")
c.AddIndex("idx3", false, "colB", "") // should overwrite the previous one
idx1 := "CREATE INDEX `idx1` ON `test` (colA,colB) WHERE colA != 1"
idx2 := "CREATE UNIQUE INDEX `idx2` ON `test` (colA)"
idx3 := "CREATE INDEX `idx3` ON `test` (colB)"
checkIndexes(t, c.Indexes, []string{idx1, idx2, idx3})
c.RemoveIndex("iDx2") // case-insensitive
c.RemoveIndex("missing") // noop
checkIndexes(t, c.Indexes, []string{idx1, idx3})
expectedIndexes := map[string]string{
"missing": "",
"idx1": idx1,
// the name is case insensitive
"iDX3": idx3,
}
for key, expectedIdx := range expectedIndexes {
idx := c.GetIndex(key)
if idx != expectedIdx {
t.Errorf("Expected index %q to be\n%v\ngot\n%v", key, expectedIdx, idx)
}
}
}
// -------------------------------------------------------------------
func TestCollectionDelete(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
collection string
disableIntegrityChecks bool
expectError bool
}{
{
name: "unsaved",
collection: "",
expectError: true,
},
{
name: "system",
collection: core.CollectionNameSuperusers,
expectError: true,
},
{
name: "base with references",
collection: "demo1",
expectError: true,
},
{
name: "base with references with disabled integrity checks",
collection: "demo1",
disableIntegrityChecks: true,
expectError: false,
},
{
name: "base without references",
collection: "demo1",
expectError: true,
},
{
name: "view with reference",
collection: "view1",
expectError: true,
},
{
name: "view with references with disabled integrity checks",
collection: "view1",
disableIntegrityChecks: true,
expectError: false,
},
{
name: "view without references",
collection: "view2",
disableIntegrityChecks: true,
expectError: false,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
var col *core.Collection
if s.collection == "" {
col = core.NewBaseCollection("test")
} else {
var err error
col, err = app.FindCollectionByNameOrId(s.collection)
if err != nil {
t.Fatal(err)
}
}
if s.disableIntegrityChecks {
col.IntegrityChecks(!s.disableIntegrityChecks)
}
err := app.Delete(col)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
exists := app.HasTable(col.Name)
if !col.IsNew() && exists != hasErr {
t.Fatalf("Expected HasTable %v, got %v", hasErr, exists)
}
if !hasErr {
cache, _ := app.FindCachedCollectionByNameOrId(col.Id)
if cache != nil {
t.Fatal("Expected the collection to be removed from the cache.")
}
}
})
}
}
func TestCollectionSaveModel(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
collection func(app core.App) (*core.Collection, error)
expectError bool
expectColumns []string
}{
// trigger validators
{
name: "create - trigger validators",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("!invalid")
c.Fields.Add(&core.TextField{Name: "example"})
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: true,
},
{
name: "update - trigger validators",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo5")
c.Name = "demo1"
c.Fields.Add(&core.TextField{Name: "example"})
c.Fields.RemoveByName("file")
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: true,
},
// create
{
name: "create base collection",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new")
c.Type = "" // should be auto set to "base"
c.Fields.RemoveByName("id") // ensure that the default fields will be loaded
c.Fields.Add(&core.TextField{Name: "example"})
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "example",
},
},
{
name: "create auth collection",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new")
c.Fields.RemoveByName("id") // ensure that the default fields will be loaded
c.Fields.RemoveByName("email") // ensure that the default fields will be loaded
c.Fields.Add(&core.TextField{Name: "example"})
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "email", "tokenKey", "password",
"verified", "emailVisibility", "example",
},
},
{
name: "create view collection",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewViewCollection("new")
c.Fields.Add(&core.TextField{Name: "ignored"}) // should be ignored
c.ViewQuery = "select 1 as id, 2 as example"
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "example",
},
},
// update
{
name: "update base collection",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo5")
c.Fields.Add(&core.TextField{Name: "example"})
c.Fields.RemoveByName("file")
c.Fields.GetByName("total").SetName("total_updated")
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "select_one", "select_many", "rel_one", "rel_many",
"total_updated", "created", "updated", "example",
},
},
{
name: "update auth collection",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("clients")
c.Fields.Add(&core.TextField{Name: "example"})
c.Fields.RemoveByName("file")
c.Fields.GetByName("name").SetName("name_updated")
c.AddIndex("test_save_idx", false, "example", "")
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "email", "emailVisibility", "password", "tokenKey",
"verified", "username", "name_updated", "created", "updated", "example",
},
},
{
name: "update view collection",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("view2")
c.Fields.Add(&core.TextField{Name: "example"}) // should be ignored
c.ViewQuery = "select 1 as id, 2 as example"
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "example",
},
},
// auth normalization
{
name: "unset missing oauth2 mapped fields",
collection: func(app core.App) (*core.Collection, error) {
c := core.NewAuthCollection("new")
c.OAuth2.Enabled = true
// shouldn't fail
c.OAuth2.MappedFields = core.OAuth2KnownFields{
Id: "missing",
Name: "missing",
Username: "missing",
AvatarURL: "missing",
}
return c, nil
},
expectError: false,
expectColumns: []string{
"id", "email", "emailVisibility", "password", "tokenKey", "verified",
},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := s.collection(app)
if err != nil {
t.Fatalf("Failed to retrieve test collection: %v", err)
}
saveErr := app.Save(collection)
hasErr := saveErr != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", hasErr, s.expectError, saveErr)
}
if hasErr {
return
}
// the collection should always have an id after successful Save
if collection.Id == "" {
t.Fatal("Expected collection id to be set")
}
// the timestamp fields should be non-empty after successful Save
if collection.Created.String() == "" {
t.Fatal("Expected collection created to be set")
}
if collection.Updated.String() == "" {
t.Fatal("Expected collection updated to be set")
}
// check if the records table was synced
hasTable := app.HasTable(collection.Name)
if !hasTable {
t.Fatalf("Expected records table %s to be created", collection.Name)
}
// check if the records table has the fields fields
columns, err := app.TableColumns(collection.Name)
if err != nil {
t.Fatal(err)
}
if len(columns) != len(s.expectColumns) {
t.Fatalf("Expected columns\n%v\ngot\n%v", s.expectColumns, columns)
}
for i, c := range columns {
if !slices.Contains(s.expectColumns, c) {
t.Fatalf("[%d] Didn't expect record column %q", i, c)
}
}
// make sure that all collection indexes exists
indexes, err := app.TableIndexes(collection.Name)
if err != nil {
t.Fatal(err)
}
if len(indexes) != len(collection.Indexes) {
t.Fatalf("Expected %d indexes, got %d", len(collection.Indexes), len(indexes))
}
for _, idx := range collection.Indexes {
parsed := dbutils.ParseIndex(idx)
if _, ok := indexes[parsed.IndexName]; !ok {
t.Fatalf("Missing index %q in\n%v", idx, indexes)
}
}
})
}
}
// indirect update of a field used in view should cause view(s) update
func TestCollectionSaveIndirectViewsUpdate(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
// update MaxSelect fields
{
relMany := collection.Fields.GetByName("rel_many").(*core.RelationField)
relMany.MaxSelect = 1
fileOne := collection.Fields.GetByName("file_one").(*core.FileField)
fileOne.MaxSelect = 10
if err := app.Save(collection); err != nil {
t.Fatal(err)
}
}
// check view1 fields
{
view1, err := app.FindCollectionByNameOrId("view1")
if err != nil {
t.Fatal(err)
}
relMany := view1.Fields.GetByName("rel_many").(*core.RelationField)
if relMany.MaxSelect != 1 {
t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect)
}
fileOne := view1.Fields.GetByName("file_one").(*core.FileField)
if fileOne.MaxSelect != 10 {
t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOne.MaxSelect)
}
}
// check view2 fields
{
view2, err := app.FindCollectionByNameOrId("view2")
if err != nil {
t.Fatal(err)
}
relMany := view2.Fields.GetByName("rel_many").(*core.RelationField)
if relMany.MaxSelect != 1 {
t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect)
}
}
}
func TestCollectionSaveViewWrapping(t *testing.T) {
t.Parallel()
viewName := "test_wrapping"
scenarios := []struct {
name string
query string
expected string
}{
{
"no wrapping - text field",
"select text as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
},
{
"no wrapping - id field",
"select text as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
},
{
"no wrapping - relation field",
"select rel_one as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1)",
},
{
"no wrapping - select field",
"select select_many as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1)",
},
{
"no wrapping - email field",
"select email as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1)",
},
{
"no wrapping - datetime field",
"select datetime as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1)",
},
{
"no wrapping - url field",
"select url as id, bool from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1)",
},
{
"wrapping - bool field",
"select bool as id, text as txt, url from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1))",
},
{
"wrapping - bool field (different order)",
"select text as txt, url, bool as id from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,CAST(`id` as TEXT) `id` FROM (select text as txt, url, bool as id from demo1))",
},
{
"wrapping - json field",
"select json as id, text, url from demo1",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`text`,`url` FROM (select json as id, text, url from demo1))",
},
{
"wrapping - numeric id",
"select 1 as id",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select 1 as id))",
},
{
"wrapping - expresion",
"select ('test') as id",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select ('test') as id))",
},
{
"no wrapping - cast as text",
"select cast('test' as text) as id",
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select cast('test' as text) as id)",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection := core.NewViewCollection(viewName)
collection.ViewQuery = s.query
err := app.Save(collection)
if err != nil {
t.Fatal(err)
}
var sql string
rowErr := app.DB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}").
Bind(dbx.Params{"name": viewName}).
Row(&sql)
if rowErr != nil {
t.Fatalf("Failed to retrieve view sql: %v", rowErr)
}
if sql != s.expected {
t.Fatalf("Expected query \n%v, \ngot \n%v", s.expected, sql)
}
})
}
}