mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-02-05 10:45:09 +02:00
1336 lines
34 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|