1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-19 14:17:48 +02:00
pocketbase/core/record_model_test.go

2093 lines
54 KiB
Go
Raw Normal View History

2024-09-29 19:23:19 +03:00
package core_test
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"regexp"
"slices"
"strings"
"testing"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
func TestNewRecord(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(&core.BoolField{Name: "status"})
m := core.NewRecord(collection)
rawData, err := json.Marshal(m.FieldsData()) // should be initialized with the defaults
if err != nil {
t.Fatal(err)
}
expected := `{"id":"","status":false}`
if str := string(rawData); str != expected {
t.Fatalf("Expected schema data\n%v\ngot\n%v", expected, str)
}
}
func TestRecordCollection(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
m := core.NewRecord(collection)
if m.Collection().Name != collection.Name {
t.Fatalf("Expected collection with name %q, got %q", collection.Name, m.Collection().Name)
}
}
func TestRecordTableName(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
m := core.NewRecord(collection)
if m.TableName() != collection.Name {
t.Fatalf("Expected table %q, got %q", collection.Name, m.TableName())
}
}
func TestRecordPostScan(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test_collection")
collection.Fields.Add(&core.TextField{Name: "test"})
m := core.NewRecord(collection)
// calling PostScan without id
err := m.PostScan()
if err == nil {
t.Fatal("Expected PostScan id error, got nil")
}
m.Id = "test_id"
m.Set("test", "abc")
if v := m.IsNew(); v != true {
t.Fatalf("[before PostScan] Expected IsNew %v, got %v", true, v)
}
if v := m.Original().PK(); v != "" {
t.Fatalf("[before PostScan] Expected the original PK to be empty string, got %v", v)
}
if v := m.Original().Get("test"); v != "" {
t.Fatalf("[before PostScan] Expected the original 'test' field to be empty string, got %v", v)
}
err = m.PostScan()
if err != nil {
t.Fatalf("Expected PostScan nil error, got %v", err)
}
if v := m.IsNew(); v != false {
t.Fatalf("[after PostScan] Expected IsNew %v, got %v", false, v)
}
if v := m.Original().PK(); v != "test_id" {
t.Fatalf("[after PostScan] Expected the original PK to be %q, got %v", "test_id", v)
}
if v := m.Original().Get("test"); v != "abc" {
t.Fatalf("[after PostScan] Expected the original 'test' field to be %q, got %v", "abc", v)
}
}
func TestRecordHookTags(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
m := core.NewRecord(collection)
tags := m.HookTags()
expectedTags := []string{collection.Id, collection.Name}
if len(tags) != len(expectedTags) {
t.Fatalf("Expected tags\n%v\ngot\n%v", expectedTags, tags)
}
for _, tag := range tags {
if !slices.Contains(expectedTags, tag) {
t.Errorf("Missing expected tag %q", tag)
}
}
}
func TestRecordBaseFilesPath(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
m := core.NewRecord(collection)
m.Id = "abc"
result := m.BaseFilesPath()
expected := collection.BaseFilesPath() + "/" + m.Id
if result != expected {
t.Fatalf("Expected %q, got %q", expected, result)
}
}
func TestRecordOriginal(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
record, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
originalId := record.Id
originalName := record.GetString("name")
extraFieldsCheck := []string{`"email":`, `"custom":`}
// change the fields
record.Id = "changed"
record.Set("name", "name_new")
record.Set("custom", "test_custom")
record.SetExpand(map[string]any{"test": 123})
record.IgnoreEmailVisibility(true)
record.IgnoreUnchangedFields(true)
record.WithCustomData(true)
record.Unhide(record.Collection().Fields.FieldNames()...)
// ensure that the email visibility and the custom data toggles are active
raw, err := record.MarshalJSON()
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
for _, f := range extraFieldsCheck {
if !strings.Contains(rawStr, f) {
t.Fatalf("Expected %s in\n%s", f, rawStr)
}
}
// check changes
if v := record.GetString("name"); v != "name_new" {
t.Fatalf("Expected name to be %q, got %q", "name_new", v)
}
if v := record.GetString("custom"); v != "test_custom" {
t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
}
// check original
if v := record.Original().PK(); v != originalId {
t.Fatalf("Expected the original PK to be %q, got %q", originalId, v)
}
if v := record.Original().Id; v != originalId {
t.Fatalf("Expected the original id to be %q, got %q", originalId, v)
}
if v := record.Original().GetString("name"); v != originalName {
t.Fatalf("Expected the original name to be %q, got %q", originalName, v)
}
if v := record.Original().GetString("custom"); v != "" {
t.Fatalf("Expected the original custom to be %q, got %q", "", v)
}
if v := record.Original().Expand(); len(v) != 0 {
t.Fatalf("Expected empty original expand, got\n%v", v)
}
// ensure that the email visibility and the custom flag toggles weren't copied
originalRaw, err := record.Original().MarshalJSON()
if err != nil {
t.Fatal(err)
}
originalRawStr := string(originalRaw)
for _, f := range extraFieldsCheck {
if strings.Contains(originalRawStr, f) {
t.Fatalf("Didn't expected %s in original\n%s", f, originalRawStr)
}
}
// loading new data shouldn't affect the original state
record.Load(map[string]any{"name": "name_new2"})
if v := record.GetString("name"); v != "name_new2" {
t.Fatalf("Expected name to be %q, got %q", "name_new2", v)
}
if v := record.Original().GetString("name"); v != originalName {
t.Fatalf("Expected the original name still to be %q, got %q", originalName, v)
}
}
func TestRecordFresh(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
record, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
originalId := record.Id
extraFieldsCheck := []string{`"email":`, `"custom":`}
// change the fields
record.Id = "changed"
record.Set("name", "name_new")
record.Set("custom", "test_custom")
record.SetExpand(map[string]any{"test": 123})
record.IgnoreEmailVisibility(true)
record.IgnoreUnchangedFields(true)
record.WithCustomData(true)
record.Unhide(record.Collection().Fields.FieldNames()...)
// ensure that the email visibility and the custom data toggles are active
raw, err := record.MarshalJSON()
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
for _, f := range extraFieldsCheck {
if !strings.Contains(rawStr, f) {
t.Fatalf("Expected %s in\n%s", f, rawStr)
}
}
// check changes
if v := record.GetString("name"); v != "name_new" {
t.Fatalf("Expected name to be %q, got %q", "name_new", v)
}
if v := record.GetString("custom"); v != "test_custom" {
t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
}
// check fresh
if v := record.Fresh().LastSavedPK(); v != originalId {
t.Fatalf("Expected the fresh LastSavedPK to be %q, got %q", originalId, v)
}
if v := record.Fresh().PK(); v != record.Id {
t.Fatalf("Expected the fresh PK to be %q, got %q", record.Id, v)
}
if v := record.Fresh().Id; v != record.Id {
t.Fatalf("Expected the fresh id to be %q, got %q", record.Id, v)
}
if v := record.Fresh().GetString("name"); v != record.GetString("name") {
t.Fatalf("Expected the fresh name to be %q, got %q", record.GetString("name"), v)
}
if v := record.Fresh().GetString("custom"); v != "" {
t.Fatalf("Expected the fresh custom to be %q, got %q", "", v)
}
if v := record.Fresh().Expand(); len(v) != 0 {
t.Fatalf("Expected empty fresh expand, got\n%v", v)
}
// ensure that the email visibility and the custom flag toggles weren't copied
freshRaw, err := record.Fresh().MarshalJSON()
if err != nil {
t.Fatal(err)
}
freshRawStr := string(freshRaw)
for _, f := range extraFieldsCheck {
if strings.Contains(freshRawStr, f) {
t.Fatalf("Didn't expected %s in fresh\n%s", f, freshRawStr)
}
}
}
func TestRecordClone(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
record, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
originalId := record.Id
extraFieldsCheck := []string{`"email":`, `"custom":`}
// change the fields
record.Id = "changed"
record.Set("name", "name_new")
record.Set("custom", "test_custom")
record.SetExpand(map[string]any{"test": 123})
record.IgnoreEmailVisibility(true)
record.WithCustomData(true)
record.Unhide(record.Collection().Fields.FieldNames()...)
// ensure that the email visibility and the custom data toggles are active
raw, err := record.MarshalJSON()
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
for _, f := range extraFieldsCheck {
if !strings.Contains(rawStr, f) {
t.Fatalf("Expected %s in\n%s", f, rawStr)
}
}
// check changes
if v := record.GetString("name"); v != "name_new" {
t.Fatalf("Expected name to be %q, got %q", "name_new", v)
}
if v := record.GetString("custom"); v != "test_custom" {
t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
}
// check clone
if v := record.Clone().LastSavedPK(); v != originalId {
t.Fatalf("Expected the clone LastSavedPK to be %q, got %q", originalId, v)
}
if v := record.Clone().PK(); v != record.Id {
t.Fatalf("Expected the clone PK to be %q, got %q", record.Id, v)
}
if v := record.Clone().Id; v != record.Id {
t.Fatalf("Expected the clone id to be %q, got %q", record.Id, v)
}
if v := record.Clone().GetString("name"); v != record.GetString("name") {
t.Fatalf("Expected the clone name to be %q, got %q", record.GetString("name"), v)
}
if v := record.Clone().GetString("custom"); v != "test_custom" {
t.Fatalf("Expected the clone custom to be %q, got %q", "test_custom", v)
}
if _, ok := record.Clone().Expand()["test"]; !ok {
t.Fatalf("Expected non-empty clone expand")
}
// ensure that the email visibility and the custom data toggles state were copied
cloneRaw, err := record.Clone().MarshalJSON()
if err != nil {
t.Fatal(err)
}
cloneRawStr := string(cloneRaw)
for _, f := range extraFieldsCheck {
if !strings.Contains(cloneRawStr, f) {
t.Fatalf("Expected %s in clone\n%s", f, cloneRawStr)
}
}
}
func TestRecordExpand(t *testing.T) {
t.Parallel()
record := core.NewRecord(core.NewBaseCollection("test"))
expand := record.Expand()
if expand == nil || len(expand) != 0 {
t.Fatalf("Expected empty map expand, got %v", expand)
}
data1 := map[string]any{"a": 123, "b": 456}
data2 := map[string]any{"c": 123}
record.SetExpand(data1)
record.SetExpand(data2) // should overwrite the previous call
// modify the expand map to check for shallow copy
data2["d"] = 456
expand = record.Expand()
if len(expand) != 1 {
t.Fatalf("Expected empty map expand, got %v", expand)
}
if v := expand["c"]; v != 123 {
t.Fatalf("Expected to find expand.c %v, got %v", 123, v)
}
}
func TestRecordMergeExpand(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
m := core.NewRecord(collection)
m.Id = "m"
// a
a := core.NewRecord(collection)
a.Id = "a"
a1 := core.NewRecord(collection)
a1.Id = "a1"
a2 := core.NewRecord(collection)
a2.Id = "a2"
a3 := core.NewRecord(collection)
a3.Id = "a3"
a31 := core.NewRecord(collection)
a31.Id = "a31"
a32 := core.NewRecord(collection)
a32.Id = "a32"
a.SetExpand(map[string]any{
"a1": a1,
"a23": []*core.Record{a2, a3},
})
a3.SetExpand(map[string]any{
"a31": a31,
"a32": []*core.Record{a32},
})
// b
b := core.NewRecord(collection)
b.Id = "b"
b1 := core.NewRecord(collection)
b1.Id = "b1"
b.SetExpand(map[string]any{
"b1": b1,
})
// c
c := core.NewRecord(collection)
c.Id = "c"
// load initial expand
m.SetExpand(map[string]any{
"a": a,
"b": b,
"c": []*core.Record{c},
})
// a (new)
aNew := core.NewRecord(collection)
aNew.Id = a.Id
a3New := core.NewRecord(collection)
a3New.Id = a3.Id
a32New := core.NewRecord(collection)
a32New.Id = "a32New"
a33New := core.NewRecord(collection)
a33New.Id = "a33New"
a3New.SetExpand(map[string]any{
"a32": []*core.Record{a32New},
"a33New": a33New,
})
aNew.SetExpand(map[string]any{
"a23": []*core.Record{a2, a3New},
})
// b (new)
bNew := core.NewRecord(collection)
bNew.Id = "bNew"
dNew := core.NewRecord(collection)
dNew.Id = "dNew"
// merge expands
m.MergeExpand(map[string]any{
"a": aNew,
"b": []*core.Record{bNew},
"dNew": dNew,
})
result := m.Expand()
raw, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
expected := `{"a":{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"a1":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a1"},"a23":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a2"},{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"a31":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a31"},"a32":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a32"},{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a32New"}],"a33New":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a33New"}},"id":"a3"}]},"id":"a"},"b":[{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"b1":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"b1"}},"id":"b"},{"collectionId":"_pbc_3632233996","collectionName":"test","id":"bNew"}],"c":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"c"}],"dNew":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"dNew"}}`
if expected != rawStr {
t.Fatalf("Expected \n%v, \ngot \n%v", expected, rawStr)
}
}
func TestRecordMergeExpandNilCheck(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
scenarios := []struct {
name string
expand map[string]any
expected string
}{
{
"nil expand",
nil,
`{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}`,
},
{
"empty expand",
map[string]any{},
`{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}`,
},
{
"non-empty expand",
map[string]any{"test": core.NewRecord(collection)},
`{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"test":{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}},"id":""}`,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
m := core.NewRecord(collection)
m.MergeExpand(s.expand)
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
if rawStr != s.expected {
t.Fatalf("Expected \n%v, \ngot \n%v", s.expected, rawStr)
}
})
}
}
func TestRecordExpandedOne(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
main := core.NewRecord(collection)
single := core.NewRecord(collection)
single.Id = "single"
multiple1 := core.NewRecord(collection)
multiple1.Id = "multiple1"
multiple2 := core.NewRecord(collection)
multiple2.Id = "multiple2"
main.SetExpand(map[string]any{
"single": single,
"multiple": []*core.Record{multiple1, multiple2},
})
if v := main.ExpandedOne("missing"); v != nil {
t.Fatalf("Expected nil, got %v", v)
}
if v := main.ExpandedOne("single"); v == nil || v.Id != "single" {
t.Fatalf("Expected record with id %q, got %v", "single", v)
}
if v := main.ExpandedOne("multiple"); v == nil || v.Id != "multiple1" {
t.Fatalf("Expected record with id %q, got %v", "multiple1", v)
}
}
func TestRecordExpandedAll(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
main := core.NewRecord(collection)
single := core.NewRecord(collection)
single.Id = "single"
multiple1 := core.NewRecord(collection)
multiple1.Id = "multiple1"
multiple2 := core.NewRecord(collection)
multiple2.Id = "multiple2"
main.SetExpand(map[string]any{
"single": single,
"multiple": []*core.Record{multiple1, multiple2},
})
if v := main.ExpandedAll("missing"); v != nil {
t.Fatalf("Expected nil, got %v", v)
}
if v := main.ExpandedAll("single"); len(v) != 1 || v[0].Id != "single" {
t.Fatalf("Expected [single] slice, got %v", v)
}
if v := main.ExpandedAll("multiple"); len(v) != 2 || v[0].Id != "multiple1" || v[1].Id != "multiple2" {
t.Fatalf("Expected [multiple1, multiple2] slice, got %v", v)
}
}
func TestRecordFieldsData(t *testing.T) {
t.Parallel()
collection := core.NewAuthCollection("test")
collection.Fields.Add(&core.TextField{Name: "field1"})
collection.Fields.Add(&core.TextField{Name: "field2"})
m := core.NewRecord(collection)
m.Id = "test_id" // direct id assignment
m.Set("email", "test@example.com")
m.Set("password", "123") // hidden fields should be also returned
m.Set("tokenKey", "789")
m.Set("field1", 123)
m.Set("field2", 456)
m.Set("unknown", 789)
raw, err := json.Marshal(m.FieldsData())
if err != nil {
t.Fatal(err)
}
expected := `{"email":"test@example.com","emailVisibility":false,"field1":"123","field2":"456","id":"test_id","password":"123","tokenKey":"789","verified":false}`
if v := string(raw); v != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, v)
}
}
func TestRecordCustomData(t *testing.T) {
t.Parallel()
collection := core.NewAuthCollection("test")
collection.Fields.Add(&core.TextField{Name: "field1"})
collection.Fields.Add(&core.TextField{Name: "field2"})
m := core.NewRecord(collection)
m.Id = "test_id" // direct id assignment
m.Set("email", "test@example.com")
m.Set("password", "123") // hidden fields should be also returned
m.Set("tokenKey", "789")
m.Set("field1", 123)
m.Set("field2", 456)
m.Set("unknown", 789)
raw, err := json.Marshal(m.CustomData())
if err != nil {
t.Fatal(err)
}
expected := `{"unknown":789}`
if v := string(raw); v != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, v)
}
}
func TestRecordSetGet(t *testing.T) {
t.Parallel()
f1 := &mockField{}
f1.Name = "mock1"
f2 := &mockField{}
f2.Name = "mock2"
f3 := &mockField{}
f3.Name = "mock3"
collection := core.NewBaseCollection("test")
collection.Fields.Add(&core.TextField{Name: "text1"})
collection.Fields.Add(&core.TextField{Name: "text2"})
collection.Fields.Add(f1)
collection.Fields.Add(f2)
collection.Fields.Add(f3)
record := core.NewRecord(collection)
record.Set("text1", 123) // should be converted to string using the ScanValue fallback
record.SetRaw("text2", 456)
record.Set("mock1", 1) // should be converted to string using the setter
record.SetRaw("mock2", 1)
record.Set("mock3:test", "abc")
record.Set("unknown", 789)
t.Run("GetRaw", func(t *testing.T) {
expected := map[string]any{
"text1": "123",
"text2": 456,
"mock1": "1",
"mock2": 1,
"mock3": "modifier_set",
"mock3:test": nil,
"unknown": 789,
}
for k, v := range expected {
raw := record.GetRaw(k)
if raw != v {
t.Errorf("Expected %q to be %v, got %v", k, v, raw)
}
}
})
t.Run("Get", func(t *testing.T) {
expected := map[string]any{
"text1": "123",
"text2": 456,
"mock1": "1",
"mock2": 1,
"mock3": "modifier_set",
"mock3:test": "modifier_get",
"unknown": 789,
}
for k, v := range expected {
get := record.Get(k)
if get != v {
t.Errorf("Expected %q to be %v, got %v", k, v, get)
}
}
})
}
func TestRecordLoad(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(&core.TextField{Name: "text"})
record := core.NewRecord(collection)
record.Load(map[string]any{
"text": 123,
"custom": 456,
})
expected := map[string]any{
"text": "123",
"custom": 456,
}
for k, v := range expected {
get := record.Get(k)
if get != v {
t.Errorf("Expected %q to be %#v, got %#v", k, v, get)
}
}
}
func TestRecordGetBool(t *testing.T) {
t.Parallel()
scenarios := []struct {
value any
expected bool
}{
{nil, false},
{"", false},
{0, false},
{1, true},
{[]string{"true"}, false},
{time.Now(), false},
{"test", false},
{"false", false},
{"true", true},
{false, false},
{true, true},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetBool("test")
if result != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, result)
}
})
}
}
func TestRecordGetString(t *testing.T) {
t.Parallel()
scenarios := []struct {
value any
expected string
}{
{nil, ""},
{"", ""},
{0, "0"},
{1.4, "1.4"},
{[]string{"true"}, ""},
{map[string]int{"test": 1}, ""},
{[]byte("abc"), "abc"},
{"test", "test"},
{false, "false"},
{true, "true"},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetString("test")
if result != s.expected {
t.Fatalf("Expected %q, got %q", s.expected, result)
}
})
}
}
func TestRecordGetInt(t *testing.T) {
t.Parallel()
scenarios := []struct {
value any
expected int
}{
{nil, 0},
{"", 0},
{[]string{"true"}, 0},
{map[string]int{"test": 1}, 0},
{time.Now(), 0},
{"test", 0},
{123, 123},
{2.4, 2},
{"123", 123},
{"123.5", 0},
{false, 0},
{true, 1},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetInt("test")
if result != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, result)
}
})
}
}
func TestRecordGetFloat(t *testing.T) {
t.Parallel()
scenarios := []struct {
value any
expected float64
}{
{nil, 0},
{"", 0},
{[]string{"true"}, 0},
{map[string]int{"test": 1}, 0},
{time.Now(), 0},
{"test", 0},
{123, 123},
{2.4, 2.4},
{"123", 123},
{"123.5", 123.5},
{false, 0},
{true, 1},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetFloat("test")
if result != s.expected {
t.Fatalf("Expected %v, got %v", s.expected, result)
}
})
}
}
func TestRecordGetDateTime(t *testing.T) {
t.Parallel()
nowTime := time.Now()
testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000Z")
scenarios := []struct {
value any
expected time.Time
}{
{nil, time.Time{}},
{"", time.Time{}},
{false, time.Time{}},
{true, time.Time{}},
{"test", time.Time{}},
{[]string{"true"}, time.Time{}},
{map[string]int{"test": 1}, time.Time{}},
{1641024040, testTime},
{"2022-01-01 08:00:40.000", testTime},
{nowTime, nowTime},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetDateTime("test")
if !result.Time().Equal(s.expected) {
t.Fatalf("Expected %v, got %v", s.expected, result)
}
})
}
}
func TestRecordGetStringSlice(t *testing.T) {
t.Parallel()
nowTime := time.Now()
scenarios := []struct {
value any
expected []string
}{
{nil, []string{}},
{"", []string{}},
{false, []string{"false"}},
{true, []string{"true"}},
{nowTime, []string{}},
{123, []string{"123"}},
{"test", []string{"test"}},
{map[string]int{"test": 1}, []string{}},
{`["test1", "test2"]`, []string{"test1", "test2"}},
{[]int{123, 123, 456}, []string{"123", "456"}},
{[]string{"test", "test", "123"}, []string{"test", "123"}},
}
collection := core.NewBaseCollection("test")
record := core.NewRecord(collection)
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("test", s.value)
result := record.GetStringSlice("test")
if len(result) != len(s.expected) {
t.Fatalf("Expected %d elements, got %d: %v", len(s.expected), len(result), result)
}
for _, v := range result {
if !slices.Contains(s.expected, v) {
t.Fatalf("Cannot find %v in %v", v, s.expected)
}
}
})
}
}
func TestRecordGetUploadedFiles(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
f1, err := filesystem.NewFileFromBytes([]byte("test"), "f1")
if err != nil {
t.Fatal(err)
}
f1.Name = "f1"
f2, err := filesystem.NewFileFromBytes([]byte("test"), "f2")
if err != nil {
t.Fatal(err)
}
f2.Name = "f2"
record, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy")
if err != nil {
t.Fatal(err)
}
record.Set("files+", []any{f1, f2})
scenarios := []struct {
key string
expected string
}{
{
"",
"null",
},
{
"title",
"null",
},
{
"files",
`[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`,
},
{
"files:uploaded",
`[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`,
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.key), func(t *testing.T) {
v := record.GetUploadedFiles(s.key)
raw, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
if rawStr != s.expected {
t.Fatalf("Expected\n%s\ngot\n%s", s.expected, rawStr)
}
})
}
}
func TestRecordUnmarshalJSONField(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(&core.JSONField{Name: "field"})
record := core.NewRecord(collection)
var testPointer *string
var testStr string
var testInt int
var testBool bool
var testSlice []int
var testMap map[string]any
scenarios := []struct {
value any
destination any
expectError bool
expectedJSON string
}{
{nil, testPointer, false, `null`},
{nil, testStr, false, `""`},
{"", testStr, false, `""`},
{1, testInt, false, `1`},
{true, testBool, false, `true`},
{[]int{1, 2, 3}, testSlice, false, `[1,2,3]`},
{map[string]any{"test": 123}, testMap, false, `{"test":123}`},
// json encoded values
{`null`, testPointer, false, `null`},
{`true`, testBool, false, `true`},
{`456`, testInt, false, `456`},
{`"test"`, testStr, false, `"test"`},
{`[4,5,6]`, testSlice, false, `[4,5,6]`},
{`{"test":456}`, testMap, false, `{"test":456}`},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
record.Set("field", s.value)
err := record.UnmarshalJSONField("field", &s.destination)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr)
}
raw, _ := json.Marshal(s.destination)
if v := string(raw); v != s.expectedJSON {
t.Fatalf("Expected %q, got %q", s.expectedJSON, v)
}
})
}
}
func TestRecordFindFileFieldByFile(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(
&core.TextField{Name: "field1"},
&core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1},
&core.FileField{Name: "field3", MaxSelect: 2, MaxSize: 1},
)
m := core.NewRecord(collection)
m.Set("field1", "test")
m.Set("field2", "test.png")
m.Set("field3", []string{"test1.png", "test2.png"})
scenarios := []struct {
filename string
expectField string
}{
{"", ""},
{"test", ""},
{"test2", ""},
{"test.png", "field2"},
{"test2.png", "field3"},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%#v", i, s.filename), func(t *testing.T) {
result := m.FindFileFieldByFile(s.filename)
var fieldName string
if result != nil {
fieldName = result.Name
}
if s.expectField != fieldName {
t.Fatalf("Expected field %v, got %v", s.expectField, result)
}
})
}
}
func TestRecordDBExport(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
f1 := &core.TextField{Name: "field1"}
f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1}
f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}}
f4 := &core.RelationField{Name: "field4", MaxSelect: 2}
colBase := core.NewBaseCollection("test_base")
colBase.Fields.Add(f1, f2, f3, f4)
colAuth := core.NewAuthCollection("test_auth")
colAuth.Fields.Add(f1, f2, f3, f4)
scenarios := []struct {
collection *core.Collection
expected string
}{
{
colBase,
`{"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id"}`,
},
{
colAuth,
`{"email":"test_email","emailVisibility":true,"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id","password":"_TEST_","tokenKey":"test_tokenKey","verified":false}`,
},
}
data := map[string]any{
"id": "test_id",
"field1": "test",
"field2": "test.png",
"field3": []string{"test1", "test2"},
"field4": []string{"test11", "test12", "test11"}, // strip duplicate,
"unknown": "test_unknown",
"password": "test_passwordHash",
"username": "test_username",
"emailVisibility": true,
"email": "test_email",
"verified": "invalid", // should be casted
"tokenKey": "test_tokenKey",
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s_%s", i, s.collection.Type, s.collection.Name), func(t *testing.T) {
record := core.NewRecord(s.collection)
record.Load(data)
result, err := record.DBExport(app)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
// replace _TEST_ placeholder with .+ regex pattern
pattern := regexp.MustCompile(strings.ReplaceAll(
"^"+regexp.QuoteMeta(s.expected)+"$",
"_TEST_",
`.+`,
))
if !pattern.MatchString(rawStr) {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, rawStr)
}
})
}
}
func TestRecordIgnoreUnchangedFields(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
new := core.NewRecord(col)
existing, err := app.FindRecordById(col, "mk5fmymtx4wsprk")
if err != nil {
t.Fatal(err)
}
existing.Set("title", "test_new")
existing.Set("files", existing.Get("files")) // no change
scenarios := []struct {
ignoreUnchangedFields bool
record *core.Record
expected []string
}{
{
false,
new,
[]string{"id", "created", "updated", "title", "files"},
},
{
true,
new,
[]string{"id", "created", "updated", "title", "files"},
},
{
false,
existing,
[]string{"id", "created", "updated", "title", "files"},
},
{
true,
existing,
[]string{"id", "title"},
},
}
for i, s := range scenarios {
action := "create"
if !s.record.IsNew() {
action = "update"
}
t.Run(fmt.Sprintf("%d_%s_%v", i, action, s.ignoreUnchangedFields), func(t *testing.T) {
s.record.IgnoreUnchangedFields(s.ignoreUnchangedFields)
result, err := s.record.DBExport(app)
if err != nil {
t.Fatal(err)
}
if len(result) != len(s.expected) {
t.Fatalf("Expected %d keys, got %d:\n%v", len(s.expected), len(result), result)
}
for _, key := range s.expected {
if _, ok := result[key]; !ok {
t.Fatalf("Missing expected key %q in\n%v", key, result)
}
}
})
}
}
func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
t.Parallel()
f1 := &core.TextField{Name: "field1"}
f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1}
f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}}
f4 := &core.TextField{Name: "field4", Hidden: true}
f5 := &core.TextField{Name: "field5", Hidden: true}
colBase := core.NewBaseCollection("test_base")
colBase.Fields.Add(f1, f2, f3, f4, f5)
colAuth := core.NewAuthCollection("test_auth")
colAuth.Fields.Add(f1, f2, f3, f4, f5)
scenarios := []struct {
name string
collection *core.Collection
ignoreEmailVisibility bool
withCustomData bool
hideFields []string
unhideFields []string
expectedJSON string
}{
// base
{
"[base] no extra flags",
colBase,
false,
false,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
},
{
"[base] with email visibility",
colBase,
true, // should have no effect
false,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
},
{
"[base] with custom data",
colBase,
true, // should have no effect
true,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
{
"[base] with explicit hide and unhide fields",
colBase,
false,
true,
[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "tokenKey", "unknown"},
[]string{"field4", "@pbInternalAbc"},
`{"emailVisibility":"test_invalid","field2":"field_2.png","field4":"field_4","id":"test_id","password":"test_passwordHash","verified":true}`,
},
{
"[base] trying to unhide custom fields without explicit WithCustomData",
colBase,
false,
true,
nil,
[]string{"field5", "@pbInternalAbc", "email", "tokenKey", "unknown"},
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
// auth
{
"[auth] no extra flags",
colAuth,
false,
false,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
},
{
"[auth] with email visibility",
colAuth,
true,
false,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
},
{
"[auth] with custom data",
colAuth,
false,
true,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","verified":true}`,
},
{
"[auth] with explicit hide and unhide fields",
colAuth,
true,
true,
[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "unknown"},
[]string{"field4", "@pbInternalAbc"},
`{"emailVisibility":false,"field2":"field_2.png","field4":"field_4","id":"test_id","verified":true}`,
},
{
"[auth] trying to unhide custom fields without explicit WithCustomData",
colAuth,
false,
true,
nil,
[]string{"field5", "@pbInternalAbc", "tokenKey", "unknown", "email"}, // emailVisibility:false has higher priority
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","unknown":"test_unknown","verified":true}`,
},
}
data := map[string]any{
"id": "test_id",
"field1": "field_1",
"field2": "field_2.png",
"field3": []string{"test1", "test2"},
"field4": "field_4",
"field5": "field_5",
"expand": map[string]any{"test": 123},
"collectionId": "m_id", // should be always ignored
"collectionName": "m_name", // should be always ignored
"unknown": "test_unknown",
"password": "test_passwordHash",
"emailVisibility": "test_invalid", // for auth collections should be casted to bool
"email": "test_email",
"verified": true,
"tokenKey": "test_tokenKey",
"@pbInternalAbc": "test_custom_inter", // always hidden
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
m := core.NewRecord(s.collection)
m.Load(data)
m.IgnoreEmailVisibility(s.ignoreEmailVisibility)
m.WithCustomData(s.withCustomData)
m.Unhide(s.unhideFields...)
m.Hide(s.hideFields...)
exportResult, err := json.Marshal(m.PublicExport())
if err != nil {
t.Fatal(err)
}
exportResultStr := string(exportResult)
// MarshalJSON and PublicExport should return the same
marshalResult, err := m.MarshalJSON()
if err != nil {
t.Fatal(err)
}
marshalResultStr := string(marshalResult)
if exportResultStr != marshalResultStr {
t.Fatalf("Expected the PublicExport to be the same as MarshalJSON, but got \n%v \nvs \n%v", exportResultStr, marshalResultStr)
}
if exportResultStr != s.expectedJSON {
t.Fatalf("Expected json \n%v \ngot \n%v", s.expectedJSON, exportResultStr)
}
})
}
}
func TestRecordUnmarshalJSON(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(&core.TextField{Name: "text"})
record := core.NewRecord(collection)
data := map[string]any{
"text": 123,
"custom": 456.789,
}
rawData, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
err = record.UnmarshalJSON(rawData)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
expected := map[string]any{
"text": "123",
"custom": 456.789,
}
for k, v := range expected {
get := record.Get(k)
if get != v {
t.Errorf("Expected %q to be %#v, got %#v", k, v, get)
}
}
}
func TestRecordReplaceModifiers(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Fields.Add(
&mockField{core.TextField{Name: "mock"}},
&core.NumberField{Name: "number"},
)
originalData := map[string]any{
"mock": "a",
"number": 2.1,
}
record := core.NewRecord(collection)
for k, v := range originalData {
record.Set(k, v)
}
result := record.ReplaceModifiers(map[string]any{
"mock:test": "b",
"number+": 3,
})
expected := map[string]any{
"mock": "modifier_set",
"number": 5.1,
}
if len(result) != len(expected) {
t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
}
for k, v := range expected {
if result[k] != v {
t.Errorf("Expected %q %#v, got %#v", k, v, result[k])
}
}
// ensure that the original data hasn't changed
for k, v := range originalData {
rv := record.Get(k)
if rv != v {
t.Errorf("Expected original %q %#v, got %#v", k, v, rv)
}
}
}
func TestRecordValidate(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection := core.NewBaseCollection("test")
collection.Fields.Add(
// dummy fields to ensure that its validators are triggered
&core.TextField{Name: "f1", Min: 3},
&core.NumberField{Name: "f2", Required: true},
)
record := core.NewRecord(collection)
record.Id = "!invalid"
t.Run("no data set", func(t *testing.T) {
tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f2"})
})
t.Run("failing the text field min requirement", func(t *testing.T) {
record.Set("f1", "a")
tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f1", "f2"})
})
t.Run("satisfying the fields validations", func(t *testing.T) {
record.Id = strings.Repeat("a", 15)
record.Set("f1", "abc")
record.Set("f2", 1)
tests.TestValidationErrors(t, app.Validate(record), nil)
})
}
func TestRecordSave(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
record func(app core.App) (*core.Record, error)
expectError bool
}{
// trigger validators
{
name: "create - trigger validators",
record: func(app core.App) (*core.Record, error) {
c, _ := app.FindCollectionByNameOrId("demo2")
record := core.NewRecord(c)
return record, nil
},
expectError: true,
},
{
name: "update - trigger validators",
record: func(app core.App) (*core.Record, error) {
record, _ := app.FindFirstRecordByData("demo2", "title", "test1")
record.Set("title", "")
return record, nil
},
expectError: true,
},
// create
{
name: "create base record",
record: func(app core.App) (*core.Record, error) {
c, _ := app.FindCollectionByNameOrId("demo2")
record := core.NewRecord(c)
record.Set("title", "new_test")
return record, nil
},
expectError: false,
},
{
name: "create auth record",
record: func(app core.App) (*core.Record, error) {
c, _ := app.FindCollectionByNameOrId("nologin")
record := core.NewRecord(c)
record.Set("email", "test_new@example.com")
record.Set("password", "1234567890")
return record, nil
},
expectError: false,
},
{
name: "create view record",
record: func(app core.App) (*core.Record, error) {
c, _ := app.FindCollectionByNameOrId("view2")
record := core.NewRecord(c)
record.Set("state", true)
return record, nil
},
expectError: true, // view records are read-only
},
// update
{
name: "update base record",
record: func(app core.App) (*core.Record, error) {
record, _ := app.FindFirstRecordByData("demo2", "title", "test1")
record.Set("title", "test_new")
return record, nil
},
expectError: false,
},
{
name: "update auth record",
record: func(app core.App) (*core.Record, error) {
record, _ := app.FindAuthRecordByEmail("nologin", "test@example.com")
record.Set("name", "test_new")
record.Set("email", "test_new@example.com")
return record, nil
},
expectError: false,
},
{
name: "update view record",
record: func(app core.App) (*core.Record, error) {
record, _ := app.FindFirstRecordByData("view2", "state", true)
record.Set("state", false)
return record, nil
},
expectError: true, // view records are read-only
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
record, err := s.record(app)
if err != nil {
t.Fatalf("Failed to retrieve test record: %v", err)
}
saveErr := app.Save(record)
hasErr := saveErr != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", hasErr, s.expectError, saveErr)
}
if hasErr {
return
}
// the record should always have an id after successful Save
if record.Id == "" {
t.Fatal("Expected record id to be set")
}
if record.IsNew() {
t.Fatal("Expected the record to be marked as not new")
}
// refetch and compare the serialization
refreshed, err := app.FindRecordById(record.Collection(), record.Id)
if err != nil {
t.Fatal(err)
}
rawRefreshed, err := refreshed.MarshalJSON()
if err != nil {
t.Fatal(err)
}
raw, err := record.MarshalJSON()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(raw, rawRefreshed) {
t.Fatalf("Expected the refreshed record to be the same as the saved one, got\n%s\nVS\n%s", raw, rawRefreshed)
}
})
}
}
func TestRecordSaveIdFromOtherCollection(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
baseCollection, _ := app.FindCollectionByNameOrId("demo2")
authCollection, _ := app.FindCollectionByNameOrId("nologin")
// base collection test
r1 := core.NewRecord(baseCollection)
r1.Set("title", "test_new")
r1.Set("id", "mk5fmymtx4wsprk") // existing id of demo3 record
if err := app.Save(r1); err != nil {
t.Fatalf("Expected nil, got error %v", err)
}
// auth collection test
r2 := core.NewRecord(authCollection)
r2.SetEmail("test_new@example.com")
r2.SetPassword("1234567890")
r2.Set("id", "gk390qegs4y47wn") // existing id of "clients" record
if err := app.Save(r2); err == nil {
t.Fatal("Expected error, got nil")
}
// try again with unique id
r2.Set("id", strings.Repeat("a", 15))
if err := app.Save(r2); err != nil {
t.Fatalf("Expected nil, got error %v", err)
}
}
func TestRecordSaveIdUpdateNoValidation(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
rec, err := app.FindRecordById("demo3", "7nwo8tuiatetxdm")
if err != nil {
t.Fatal(err)
}
rec.Id = strings.Repeat("a", 15)
err = app.SaveNoValidate(rec)
if err == nil {
t.Fatal("Expected save to fail, got nil")
}
// no changes
rec.Load(rec.Original().FieldsData())
err = app.SaveNoValidate(rec)
if err != nil {
t.Fatalf("Expected save to succeed, got error %v", err)
}
}
func TestRecordSaveWithChangedPassword(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
record, err := app.FindAuthRecordByEmail("nologin", "test@example.com")
if err != nil {
t.Fatal(err)
}
originalTokenKey := record.TokenKey()
t.Run("no password change shouldn't change the tokenKey", func(t *testing.T) {
record.Set("name", "example")
if err := app.Save(record); err != nil {
t.Fatal(err)
}
tokenKey := record.TokenKey()
if tokenKey == "" || originalTokenKey != tokenKey {
t.Fatalf("Expected tokenKey to not change, got %q VS %q", originalTokenKey, tokenKey)
}
})
t.Run("password change should change the tokenKey", func(t *testing.T) {
record.Set("password", "1234567890")
if err := app.Save(record); err != nil {
t.Fatal(err)
}
tokenKey := record.TokenKey()
if tokenKey == "" || originalTokenKey == tokenKey {
t.Fatalf("Expected tokenKey to change, got %q VS %q", originalTokenKey, tokenKey)
}
})
}
func TestRecordDelete(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
demoCollection, _ := app.FindCollectionByNameOrId("demo2")
// delete unsaved record
// ---
newRec := core.NewRecord(demoCollection)
if err := app.Delete(newRec); err == nil {
t.Fatal("(newRec) Didn't expect to succeed deleting unsaved record")
}
// delete view record
// ---
viewRec, _ := app.FindRecordById("view2", "84nmscqy84lsi1t")
if err := app.Delete(viewRec); err == nil {
t.Fatal("(viewRec) Didn't expect to succeed deleting view record")
}
// check if it still exists
viewRec, _ = app.FindRecordById(viewRec.Collection().Id, viewRec.Id)
if viewRec == nil {
t.Fatal("(viewRec) Expected view record to still exists")
2024-09-29 19:23:19 +03:00
}
// delete existing record + external auths
// ---
rec1, _ := app.FindRecordById("users", "4q1xlclmfloku33")
if err := app.Delete(rec1); err != nil {
t.Fatalf("(rec1) Expected nil, got error %v", err)
}
// check if it was really deleted
if refreshed, _ := app.FindRecordById(rec1.Collection().Id, rec1.Id); refreshed != nil {
t.Fatalf("(rec1) Expected record to be deleted, got %v", refreshed)
}
// check if the external auths were deleted
if auths, _ := app.FindAllExternalAuthsByRecord(rec1); len(auths) > 0 {
t.Fatalf("(rec1) Expected external auths to be deleted, got %v", auths)
}
// delete existing record while being part of a non-cascade required relation
// ---
rec2, _ := app.FindRecordById("demo3", "7nwo8tuiatetxdm")
if err := app.Delete(rec2); err == nil {
t.Fatalf("(rec2) Expected error, got nil")
}
// delete existing record + cascade
// ---
calledQueries := []string{}
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
calledQueries = append(calledQueries, sql)
}
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
calledQueries = append(calledQueries, sql)
}
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
calledQueries = append(calledQueries, sql)
}
app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
calledQueries = append(calledQueries, sql)
}
rec3, _ := app.FindRecordById("users", "oap640cot4yru2s")
// delete
if err := app.Delete(rec3); err != nil {
t.Fatalf("(rec3) Expected nil, got error %v", err)
}
// check if it was really deleted
rec3, _ = app.FindRecordById(rec3.Collection().Id, rec3.Id)
if rec3 != nil {
t.Fatalf("(rec3) Expected record to be deleted, got %v", rec3)
}
// check if the operation cascaded
rel, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t")
if rel != nil {
t.Fatalf("(rec3) Expected the delete to cascade, found relation %v", rel)
}
// ensure that the json rel fields were prefixed
joinedQueries := strings.Join(calledQueries, " ")
expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
if !strings.Contains(joinedQueries, expectedRelManyPart) {
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries)
}
expectedRelOnePart := "SELECT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`='"
if !strings.Contains(joinedQueries, expectedRelOnePart) {
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries)
}
}
func TestRecordDeleteBatchProcessing(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
if err := createMockBatchProcessingData(app); err != nil {
t.Fatal(err)
}
// find and delete the first c1 record to trigger cascade
mainRecord, _ := app.FindRecordById("c1", "a")
if err := app.Delete(mainRecord); err != nil {
t.Fatal(err)
}
// check if the main record was deleted
_, err := app.FindRecordById(mainRecord.Collection().Id, mainRecord.Id)
if err == nil {
t.Fatal("The main record wasn't deleted")
}
// check if the c1 b rel field were updated
c1RecordB, err := app.FindRecordById("c1", "b")
if err != nil || c1RecordB.GetString("rel") != "" {
t.Fatalf("Expected c1RecordB.rel to be nil, got %v", c1RecordB.GetString("rel"))
}
// check if the c2 rel fields were updated
c2Records, err := app.FindAllRecords("c2", nil)
if err != nil || len(c2Records) == 0 {
t.Fatalf("Failed to fetch c2 records: %v", err)
}
for _, r := range c2Records {
ids := r.GetStringSlice("rel")
if len(ids) != 1 || ids[0] != "b" {
t.Fatalf("Expected only 'b' rel id, got %v", ids)
}
}
// check if all c3 relations were deleted
c3Records, err := app.FindAllRecords("c3", nil)
if err != nil {
t.Fatalf("Failed to fetch c3 records: %v", err)
}
if total := len(c3Records); total != 0 {
t.Fatalf("Expected c3 records to be deleted, found %d", total)
}
}
func createMockBatchProcessingData(app core.App) error {
// create mock collection without relation
c1 := core.NewBaseCollection("c1")
c1.Id = "c1"
c1.Fields.Add(
&core.TextField{Name: "text"},
&core.RelationField{
Name: "rel",
MaxSelect: 1,
CollectionId: "c1",
CascadeDelete: false, // should unset all rel fields
},
)
if err := app.SaveNoValidate(c1); err != nil {
return err
}
// create mock collection with a multi-rel field
c2 := core.NewBaseCollection("c2")
c2.Id = "c2"
c2.Fields.Add(
&core.TextField{Name: "text"},
&core.RelationField{
Name: "rel",
MaxSelect: 10,
CollectionId: "c1",
CascadeDelete: false, // should unset all rel fields
},
)
if err := app.SaveNoValidate(c2); err != nil {
return err
}
// create mock collection with a single-rel field
c3 := core.NewBaseCollection("c3")
c3.Id = "c3"
c3.Fields.Add(
&core.RelationField{
Name: "rel",
MaxSelect: 1,
CollectionId: "c1",
CascadeDelete: true, // should delete all c3 records
},
)
if err := app.SaveNoValidate(c3); err != nil {
return err
}
// insert mock records
c1RecordA := core.NewRecord(c1)
c1RecordA.Id = "a"
c1RecordA.Set("rel", c1RecordA.Id) // self reference
if err := app.SaveNoValidate(c1RecordA); err != nil {
return err
}
c1RecordB := core.NewRecord(c1)
c1RecordB.Id = "b"
c1RecordB.Set("rel", c1RecordA.Id) // rel to another record from the same collection
if err := app.SaveNoValidate(c1RecordB); err != nil {
return err
}
for i := 0; i < 4500; i++ {
c2Record := core.NewRecord(c2)
c2Record.Set("rel", []string{c1RecordA.Id, c1RecordB.Id})
if err := app.SaveNoValidate(c2Record); err != nil {
return err
}
c3Record := core.NewRecord(c3)
c3Record.Set("rel", c1RecordA.Id)
if err := app.SaveNoValidate(c3Record); err != nil {
return err
}
}
// set the same id as the relation for at least 1 record
// to check whether the correct condition will be added
c3Record := core.NewRecord(c3)
c3Record.Set("rel", c1RecordA.Id)
c3Record.Id = c1RecordA.Id
if err := app.SaveNoValidate(c3Record); err != nil {
return err
}
return nil
}
// -------------------------------------------------------------------
type mockField struct {
core.TextField
}
func (f *mockField) FindGetter(key string) core.GetterFunc {
switch key {
case f.Name + ":test":
return func(record *core.Record) any {
return "modifier_get"
}
default:
return nil
}
}
func (f *mockField) FindSetter(key string) core.SetterFunc {
switch key {
case f.Name:
return func(record *core.Record, raw any) {
record.SetRaw(f.Name, cast.ToString(raw))
}
case f.Name + ":test":
return func(record *core.Record, raw any) {
record.SetRaw(f.Name, "modifier_set")
}
default:
return nil
}
}