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" )
2024-10-29 20:08:16 +02:00
collection . Id = "_pbc_123"
2024-09-29 19:23:19 +03:00
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 )
2024-10-29 20:08:16 +02:00
expected := ` { "a": { "collectionId":"_pbc_123","collectionName":"test","expand": { "a1": { "collectionId":"_pbc_123","collectionName":"test","id":"a1"},"a23":[ { "collectionId":"_pbc_123","collectionName":"test","id":"a2"}, { "collectionId":"_pbc_123","collectionName":"test","expand": { "a31": { "collectionId":"_pbc_123","collectionName":"test","id":"a31"},"a32":[ { "collectionId":"_pbc_123","collectionName":"test","id":"a32"}, { "collectionId":"_pbc_123","collectionName":"test","id":"a32New"}],"a33New": { "collectionId":"_pbc_123","collectionName":"test","id":"a33New"}},"id":"a3"}]},"id":"a"},"b":[ { "collectionId":"_pbc_123","collectionName":"test","expand": { "b1": { "collectionId":"_pbc_123","collectionName":"test","id":"b1"}},"id":"b"}, { "collectionId":"_pbc_123","collectionName":"test","id":"bNew"}],"c":[ { "collectionId":"_pbc_123","collectionName":"test","id":"c"}],"dNew": { "collectionId":"_pbc_123","collectionName":"test","id":"dNew"}} `
2024-09-29 19:23:19 +03:00
if expected != rawStr {
t . Fatalf ( "Expected \n%v, \ngot \n%v" , expected , rawStr )
}
}
func TestRecordMergeExpandNilCheck ( t * testing . T ) {
t . Parallel ( )
collection := core . NewBaseCollection ( "test" )
2024-10-29 20:08:16 +02:00
collection . Id = "_pbc_123"
2024-09-29 19:23:19 +03:00
scenarios := [ ] struct {
name string
expand map [ string ] any
expected string
} {
{
"nil expand" ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_123","collectionName":"test","id":""} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"empty expand" ,
map [ string ] any { } ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_123","collectionName":"test","id":""} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"non-empty expand" ,
map [ string ] any { "test" : core . NewRecord ( collection ) } ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_123","collectionName":"test","expand": { "test": { "collectionId":"_pbc_123","collectionName":"test","id":""}},"id":""} ` ,
2024-09-29 19:23:19 +03:00
} ,
}
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" )
2024-10-29 20:08:16 +02:00
colBase . Id = "_pbc_base_123"
2024-09-29 19:23:19 +03:00
colBase . Fields . Add ( f1 , f2 , f3 , f4 , f5 )
colAuth := core . NewAuthCollection ( "test_auth" )
2024-10-29 20:08:16 +02:00
colAuth . Id = "_pbc_auth_123"
2024-09-29 19:23:19 +03:00
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 ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_base_123","collectionName":"test_base","expand": { "test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[base] with email visibility" ,
colBase ,
true , // should have no effect
false ,
nil ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_base_123","collectionName":"test_base","expand": { "test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[base] with custom data" ,
colBase ,
true , // should have no effect
true ,
nil ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_base_123","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} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[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" } ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_base_123","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} ` ,
2024-09-29 19:23:19 +03:00
} ,
// auth
{
"[auth] no extra flags" ,
colAuth ,
false ,
false ,
nil ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand": { "test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[auth] with email visibility" ,
colAuth ,
true ,
false ,
nil ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_auth_123","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} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[auth] with custom data" ,
colAuth ,
false ,
true ,
nil ,
nil ,
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_auth_123","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} ` ,
2024-09-29 19:23:19 +03:00
} ,
{
"[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
2024-10-29 20:08:16 +02:00
` { "collectionId":"_pbc_auth_123","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} ` ,
2024-09-29 19:23:19 +03:00
} ,
}
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
// ---
2024-10-09 11:51:03 +03:00
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
}
}