mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-03-21 06:36:27 +02:00
451 lines
14 KiB
Go
451 lines
14 KiB
Go
package router_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/router"
|
|
)
|
|
|
|
func pointer[T any](val T) *T {
|
|
return &val
|
|
}
|
|
|
|
func TestUnmarshalRequestData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mapData := map[string][]string{
|
|
"number1": {"1"},
|
|
"number2": {"2", "3"},
|
|
"number3": {"2.1", "-3.4"},
|
|
"string0": {""},
|
|
"string1": {"a"},
|
|
"string2": {"b", "c"},
|
|
"bool1": {"true"},
|
|
"bool2": {"true", "false"},
|
|
"mixed": {"true", "123", "test"},
|
|
"@jsonPayload": {`{"json_a":null,"json_b":123}`, `{"json_c":[1,2,3]}`},
|
|
}
|
|
|
|
structData := map[string][]string{
|
|
"stringTag": {"a", "b"},
|
|
"StringPtr": {"b"},
|
|
"StringSlice": {"a", "b", "c", ""},
|
|
"stringSlicePtrTag": {"d", "e"},
|
|
"StringSliceOfPtr": {"f", "g"},
|
|
|
|
"boolTag": {"true"},
|
|
"BoolPtr": {"true"},
|
|
"BoolSlice": {"true", "false", ""},
|
|
"boolSlicePtrTag": {"false", "false", "true"},
|
|
"BoolSliceOfPtr": {"false", "true", "false"},
|
|
|
|
"int8Tag": {"-1", "2"},
|
|
"Int8Ptr": {"3"},
|
|
"Int8Slice": {"4", "5", ""},
|
|
"int8SlicePtrTag": {"5", "6"},
|
|
"Int8SliceOfPtr": {"7", "8"},
|
|
|
|
"int16Tag": {"-1", "2"},
|
|
"Int16Ptr": {"3"},
|
|
"Int16Slice": {"4", "5", ""},
|
|
"int16SlicePtrTag": {"5", "6"},
|
|
"Int16SliceOfPtr": {"7", "8"},
|
|
|
|
"int32Tag": {"-1", "2"},
|
|
"Int32Ptr": {"3"},
|
|
"Int32Slice": {"4", "5", ""},
|
|
"int32SlicePtrTag": {"5", "6"},
|
|
"Int32SliceOfPtr": {"7", "8"},
|
|
|
|
"int64Tag": {"-1", "2"},
|
|
"Int64Ptr": {"3"},
|
|
"Int64Slice": {"4", "5", ""},
|
|
"int64SlicePtrTag": {"5", "6"},
|
|
"Int64SliceOfPtr": {"7", "8"},
|
|
|
|
"intTag": {"-1", "2"},
|
|
"IntPtr": {"3"},
|
|
"IntSlice": {"4", "5", ""},
|
|
"intSlicePtrTag": {"5", "6"},
|
|
"IntSliceOfPtr": {"7", "8"},
|
|
|
|
"uint8Tag": {"1", "2"},
|
|
"Uint8Ptr": {"3"},
|
|
"Uint8Slice": {"4", "5", ""},
|
|
"uint8SlicePtrTag": {"5", "6"},
|
|
"Uint8SliceOfPtr": {"7", "8"},
|
|
|
|
"uint16Tag": {"1", "2"},
|
|
"Uint16Ptr": {"3"},
|
|
"Uint16Slice": {"4", "5", ""},
|
|
"uint16SlicePtrTag": {"5", "6"},
|
|
"Uint16SliceOfPtr": {"7", "8"},
|
|
|
|
"uint32Tag": {"1", "2"},
|
|
"Uint32Ptr": {"3"},
|
|
"Uint32Slice": {"4", "5", ""},
|
|
"uint32SlicePtrTag": {"5", "6"},
|
|
"Uint32SliceOfPtr": {"7", "8"},
|
|
|
|
"uint64Tag": {"1", "2"},
|
|
"Uint64Ptr": {"3"},
|
|
"Uint64Slice": {"4", "5", ""},
|
|
"uint64SlicePtrTag": {"5", "6"},
|
|
"Uint64SliceOfPtr": {"7", "8"},
|
|
|
|
"uintTag": {"1", "2"},
|
|
"UintPtr": {"3"},
|
|
"UintSlice": {"4", "5", ""},
|
|
"uintSlicePtrTag": {"5", "6"},
|
|
"UintSliceOfPtr": {"7", "8"},
|
|
|
|
"float32Tag": {"-1.2"},
|
|
"Float32Ptr": {"1.5", "2.0"},
|
|
"Float32Slice": {"1", "2.3", "-0.3", ""},
|
|
"float32SlicePtrTag": {"-1.3", "3"},
|
|
"Float32SliceOfPtr": {"0", "1.2"},
|
|
|
|
"float64Tag": {"-1.2"},
|
|
"Float64Ptr": {"1.5", "2.0"},
|
|
"Float64Slice": {"1", "2.3", "-0.3", ""},
|
|
"float64SlicePtrTag": {"-1.3", "3"},
|
|
"Float64SliceOfPtr": {"0", "1.2"},
|
|
|
|
"timeTag": {"2009-11-10T15:00:00Z"},
|
|
"TimePtr": {"2009-11-10T14:00:00Z", "2009-11-10T15:00:00Z"},
|
|
"TimeSlice": {"2009-11-10T14:00:00Z", "2009-11-10T15:00:00Z"},
|
|
"timeSlicePtrTag": {"2009-11-10T15:00:00Z", "2009-11-10T16:00:00Z"},
|
|
"TimeSliceOfPtr": {"2009-11-10T17:00:00Z", "2009-11-10T18:00:00Z"},
|
|
|
|
// @jsonPayload fields
|
|
"@jsonPayload": {
|
|
`{"payloadA":"test", "shouldBeIgnored": "abc"}`,
|
|
`{"payloadB":[1,2,3], "payloadC":true}`,
|
|
},
|
|
|
|
// unexported fields or `-` tags
|
|
"unexperted": {"test"},
|
|
"SkipExported": {"test"},
|
|
"unexportedStructFieldWithoutTag.Name": {"test"},
|
|
"unexportedStruct.Name": {"test"},
|
|
|
|
// structs
|
|
"StructWithoutTag.Name": {"test1"},
|
|
"exportedStruct.Name": {"test2"},
|
|
|
|
// embedded
|
|
"embed_name": {"test3"},
|
|
"embed2.embed_name2": {"test4"},
|
|
}
|
|
|
|
type embed1 struct {
|
|
Name string `form:"embed_name" json:"embed_name"`
|
|
}
|
|
|
|
type embed2 struct {
|
|
Name string `form:"embed_name2" json:"embed_name2"`
|
|
}
|
|
|
|
//nolint
|
|
type TestStruct struct {
|
|
String string `form:"stringTag" query:"stringTag2"`
|
|
StringPtr *string
|
|
StringSlice []string
|
|
StringSlicePtr *[]string `form:"stringSlicePtrTag"`
|
|
StringSliceOfPtr []*string
|
|
|
|
Bool bool `form:"boolTag" query:"boolTag2"`
|
|
BoolPtr *bool
|
|
BoolSlice []bool
|
|
BoolSlicePtr *[]bool `form:"boolSlicePtrTag"`
|
|
BoolSliceOfPtr []*bool
|
|
|
|
Int8 int8 `form:"int8Tag" query:"int8Tag2"`
|
|
Int8Ptr *int8
|
|
Int8Slice []int8
|
|
Int8SlicePtr *[]int8 `form:"int8SlicePtrTag"`
|
|
Int8SliceOfPtr []*int8
|
|
|
|
Int16 int16 `form:"int16Tag" query:"int16Tag2"`
|
|
Int16Ptr *int16
|
|
Int16Slice []int16
|
|
Int16SlicePtr *[]int16 `form:"int16SlicePtrTag"`
|
|
Int16SliceOfPtr []*int16
|
|
|
|
Int32 int32 `form:"int32Tag" query:"int32Tag2"`
|
|
Int32Ptr *int32
|
|
Int32Slice []int32
|
|
Int32SlicePtr *[]int32 `form:"int32SlicePtrTag"`
|
|
Int32SliceOfPtr []*int32
|
|
|
|
Int64 int64 `form:"int64Tag" query:"int64Tag2"`
|
|
Int64Ptr *int64
|
|
Int64Slice []int64
|
|
Int64SlicePtr *[]int64 `form:"int64SlicePtrTag"`
|
|
Int64SliceOfPtr []*int64
|
|
|
|
Int int `form:"intTag" query:"intTag2"`
|
|
IntPtr *int
|
|
IntSlice []int
|
|
IntSlicePtr *[]int `form:"intSlicePtrTag"`
|
|
IntSliceOfPtr []*int
|
|
|
|
Uint8 uint8 `form:"uint8Tag" query:"uint8Tag2"`
|
|
Uint8Ptr *uint8
|
|
Uint8Slice []uint8
|
|
Uint8SlicePtr *[]uint8 `form:"uint8SlicePtrTag"`
|
|
Uint8SliceOfPtr []*uint8
|
|
|
|
Uint16 uint16 `form:"uint16Tag" query:"uint16Tag2"`
|
|
Uint16Ptr *uint16
|
|
Uint16Slice []uint16
|
|
Uint16SlicePtr *[]uint16 `form:"uint16SlicePtrTag"`
|
|
Uint16SliceOfPtr []*uint16
|
|
|
|
Uint32 uint32 `form:"uint32Tag" query:"uint32Tag2"`
|
|
Uint32Ptr *uint32
|
|
Uint32Slice []uint32
|
|
Uint32SlicePtr *[]uint32 `form:"uint32SlicePtrTag"`
|
|
Uint32SliceOfPtr []*uint32
|
|
|
|
Uint64 uint64 `form:"uint64Tag" query:"uint64Tag2"`
|
|
Uint64Ptr *uint64
|
|
Uint64Slice []uint64
|
|
Uint64SlicePtr *[]uint64 `form:"uint64SlicePtrTag"`
|
|
Uint64SliceOfPtr []*uint64
|
|
|
|
Uint uint `form:"uintTag" query:"uintTag2"`
|
|
UintPtr *uint
|
|
UintSlice []uint
|
|
UintSlicePtr *[]uint `form:"uintSlicePtrTag"`
|
|
UintSliceOfPtr []*uint
|
|
|
|
Float32 float32 `form:"float32Tag" query:"float32Tag2"`
|
|
Float32Ptr *float32
|
|
Float32Slice []float32
|
|
Float32SlicePtr *[]float32 `form:"float32SlicePtrTag"`
|
|
Float32SliceOfPtr []*float32
|
|
|
|
Float64 float64 `form:"float64Tag" query:"float64Tag2"`
|
|
Float64Ptr *float64
|
|
Float64Slice []float64
|
|
Float64SlicePtr *[]float64 `form:"float64SlicePtrTag"`
|
|
Float64SliceOfPtr []*float64
|
|
|
|
// encoding.TextUnmarshaler
|
|
Time time.Time `form:"timeTag" query:"timeTag2"`
|
|
TimePtr *time.Time
|
|
TimeSlice []time.Time
|
|
TimeSlicePtr *[]time.Time `form:"timeSlicePtrTag"`
|
|
TimeSliceOfPtr []*time.Time
|
|
|
|
// @jsonPayload fields
|
|
JSONPayloadA string `form:"shouldBeIgnored" json:"payloadA"`
|
|
JSONPayloadB []int `json:"payloadB"`
|
|
JSONPayloadC bool `json:"-"`
|
|
|
|
// unexported fields or `-` tags
|
|
unexported string
|
|
SkipExported string `form:"-"`
|
|
unexportedStructFieldWithoutTag struct {
|
|
Name string `json:"unexportedStructFieldWithoutTag_name"`
|
|
}
|
|
unexportedStructFieldWithTag struct {
|
|
Name string `json:"unexportedStructFieldWithTag_name"`
|
|
} `form:"unexportedStruct"`
|
|
|
|
// structs
|
|
StructWithoutTag struct {
|
|
Name string `json:"StructWithoutTag_name"`
|
|
}
|
|
StructWithTag struct {
|
|
Name string `json:"StructWithTag_name"`
|
|
} `form:"exportedStruct"`
|
|
|
|
// embedded
|
|
embed1
|
|
embed2 `form:"embed2"`
|
|
}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
data map[string][]string
|
|
dst any
|
|
tag string
|
|
prefix string
|
|
error bool
|
|
result string
|
|
}{
|
|
{
|
|
name: "nil data",
|
|
data: nil,
|
|
dst: pointer(map[string]any{}),
|
|
error: false,
|
|
result: `{}`,
|
|
},
|
|
{
|
|
name: "non-pointer map[string]any",
|
|
data: mapData,
|
|
dst: map[string]any{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "unsupported *map[string]string",
|
|
data: mapData,
|
|
dst: pointer(map[string]string{}),
|
|
error: true,
|
|
},
|
|
{
|
|
name: "unsupported *map[string][]string",
|
|
data: mapData,
|
|
dst: pointer(map[string][]string{}),
|
|
error: true,
|
|
},
|
|
{
|
|
name: "*map[string]any",
|
|
data: mapData,
|
|
dst: pointer(map[string]any{}),
|
|
result: `{"bool1":true,"bool2":[true,false],"json_a":null,"json_b":123,"json_c":[1,2,3],"mixed":[true,123,"test"],"number1":1,"number2":[2,3],"number3":[2.1,-3.4],"string0":"","string1":"a","string2":["b","c"]}`,
|
|
},
|
|
{
|
|
name: "valid pointer struct (all fields)",
|
|
data: structData,
|
|
dst: &TestStruct{},
|
|
result: `{"String":"a","StringPtr":"b","StringSlice":["a","b","c",""],"StringSlicePtr":["d","e"],"StringSliceOfPtr":["f","g"],"Bool":true,"BoolPtr":true,"BoolSlice":[true,false,false],"BoolSlicePtr":[false,false,true],"BoolSliceOfPtr":[false,true,false],"Int8":-1,"Int8Ptr":3,"Int8Slice":[4,5,0],"Int8SlicePtr":[5,6],"Int8SliceOfPtr":[7,8],"Int16":-1,"Int16Ptr":3,"Int16Slice":[4,5,0],"Int16SlicePtr":[5,6],"Int16SliceOfPtr":[7,8],"Int32":-1,"Int32Ptr":3,"Int32Slice":[4,5,0],"Int32SlicePtr":[5,6],"Int32SliceOfPtr":[7,8],"Int64":-1,"Int64Ptr":3,"Int64Slice":[4,5,0],"Int64SlicePtr":[5,6],"Int64SliceOfPtr":[7,8],"Int":-1,"IntPtr":3,"IntSlice":[4,5,0],"IntSlicePtr":[5,6],"IntSliceOfPtr":[7,8],"Uint8":1,"Uint8Ptr":3,"Uint8Slice":"BAUA","Uint8SlicePtr":"BQY=","Uint8SliceOfPtr":[7,8],"Uint16":1,"Uint16Ptr":3,"Uint16Slice":[4,5,0],"Uint16SlicePtr":[5,6],"Uint16SliceOfPtr":[7,8],"Uint32":1,"Uint32Ptr":3,"Uint32Slice":[4,5,0],"Uint32SlicePtr":[5,6],"Uint32SliceOfPtr":[7,8],"Uint64":1,"Uint64Ptr":3,"Uint64Slice":[4,5,0],"Uint64SlicePtr":[5,6],"Uint64SliceOfPtr":[7,8],"Uint":1,"UintPtr":3,"UintSlice":[4,5,0],"UintSlicePtr":[5,6],"UintSliceOfPtr":[7,8],"Float32":-1.2,"Float32Ptr":1.5,"Float32Slice":[1,2.3,-0.3,0],"Float32SlicePtr":[-1.3,3],"Float32SliceOfPtr":[0,1.2],"Float64":-1.2,"Float64Ptr":1.5,"Float64Slice":[1,2.3,-0.3,0],"Float64SlicePtr":[-1.3,3],"Float64SliceOfPtr":[0,1.2],"Time":"2009-11-10T15:00:00Z","TimePtr":"2009-11-10T14:00:00Z","TimeSlice":["2009-11-10T14:00:00Z","2009-11-10T15:00:00Z"],"TimeSlicePtr":["2009-11-10T15:00:00Z","2009-11-10T16:00:00Z"],"TimeSliceOfPtr":["2009-11-10T17:00:00Z","2009-11-10T18:00:00Z"],"payloadA":"test","payloadB":[1,2,3],"SkipExported":"","StructWithoutTag":{"StructWithoutTag_name":"test1"},"StructWithTag":{"StructWithTag_name":"test2"},"embed_name":"test3","embed_name2":"test4"}`,
|
|
},
|
|
{
|
|
name: "non-pointer struct",
|
|
data: structData,
|
|
dst: TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "invalid struct uint value",
|
|
data: map[string][]string{"uintTag": {"-1"}},
|
|
dst: &TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "invalid struct int value",
|
|
data: map[string][]string{"intTag": {"abc"}},
|
|
dst: &TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "invalid struct bool value",
|
|
data: map[string][]string{"boolTag": {"abc"}},
|
|
dst: &TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "invalid struct float value",
|
|
data: map[string][]string{"float64Tag": {"abc"}},
|
|
dst: &TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "invalid struct TextUnmarshaler value",
|
|
data: map[string][]string{"timeTag": {"123"}},
|
|
dst: &TestStruct{},
|
|
error: true,
|
|
},
|
|
{
|
|
name: "custom tagKey",
|
|
data: map[string][]string{
|
|
"tag1": {"a"},
|
|
"tag2": {"b"},
|
|
"tag3": {"c"},
|
|
"Item": {"d"},
|
|
},
|
|
dst: &struct {
|
|
Item string `form:"tag1" query:"tag2" json:"tag2"`
|
|
}{},
|
|
tag: "query",
|
|
result: `{"tag2":"b"}`,
|
|
},
|
|
{
|
|
name: "custom prefix",
|
|
data: map[string][]string{
|
|
"test.A": {"1"},
|
|
"A": {"2"},
|
|
"test.alias": {"3"},
|
|
},
|
|
dst: &struct {
|
|
A string
|
|
B string `form:"alias"`
|
|
}{},
|
|
prefix: "test",
|
|
result: `{"A":"1","B":"3"}`,
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
err := router.UnmarshalRequestData(s.data, s.dst, s.tag, s.prefix)
|
|
|
|
hasErr := err != nil
|
|
if hasErr != s.error {
|
|
t.Fatalf("Expected hasErr %v, got %v (%v)", s.error, hasErr, err)
|
|
}
|
|
|
|
if hasErr {
|
|
return
|
|
}
|
|
|
|
raw, err := json.Marshal(s.dst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !bytes.Equal(raw, []byte(s.result)) {
|
|
t.Fatalf("Expected dst \n%s\ngot\n%s", s.result, raw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// note: extra unexported checks in addition to the above test as there
|
|
// is no easy way to print nested structs with all their fields.
|
|
func TestUnmarshalRequestDataUnexportedFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
//nolint:all
|
|
type TestStruct struct {
|
|
Exported string
|
|
|
|
unexported string
|
|
// to ensure that the reflection doesn't take tags with higher priority than the exported state
|
|
unexportedWithTag string `form:"unexportedWithTag" json:"unexportedWithTag"`
|
|
}
|
|
|
|
dst := &TestStruct{}
|
|
|
|
err := router.UnmarshalRequestData(map[string][]string{
|
|
"Exported": {"test"}, // just for reference
|
|
|
|
"Unexported": {"test"},
|
|
"unexported": {"test"},
|
|
"UnexportedWithTag": {"test"},
|
|
"unexportedWithTag": {"test"},
|
|
}, dst, "", "")
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if dst.Exported != "test" {
|
|
t.Fatalf("Expected the Exported field to be %q, got %q", "test", dst.Exported)
|
|
}
|
|
|
|
if dst.unexported != "" {
|
|
t.Fatalf("Expected the unexported field to remain empty, got %q", dst.unexported)
|
|
}
|
|
|
|
if dst.unexportedWithTag != "" {
|
|
t.Fatalf("Expected the unexportedWithTag field to remain empty, got %q", dst.unexportedWithTag)
|
|
}
|
|
}
|