mirror of
https://github.com/labstack/echo.git
synced 2025-11-23 22:24:54 +02:00
Revert the DefaultBinder empty body handling changes following @aldas's concerns about: - Body replacement potentially interfering with custom readers - Lack of proper reproduction case for the original issue - Potential over-engineering for an edge case The "read one byte and reconstruct body" approach could interfere with users who add custom readers with specific behavior. Waiting for better reproduction case and less invasive solution. Refs: https://github.com/labstack/echo/issues/2813#issuecomment-3294563361 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1687 lines
52 KiB
Go
1687 lines
52 KiB
Go
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
|
|
|
|
package echo
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
type bindTestStruct struct {
|
|
I int
|
|
PtrI *int
|
|
I8 int8
|
|
PtrI8 *int8
|
|
I16 int16
|
|
PtrI16 *int16
|
|
I32 int32
|
|
PtrI32 *int32
|
|
I64 int64
|
|
PtrI64 *int64
|
|
UI uint
|
|
PtrUI *uint
|
|
UI8 uint8
|
|
PtrUI8 *uint8
|
|
UI16 uint16
|
|
PtrUI16 *uint16
|
|
UI32 uint32
|
|
PtrUI32 *uint32
|
|
UI64 uint64
|
|
PtrUI64 *uint64
|
|
B bool
|
|
PtrB *bool
|
|
F32 float32
|
|
PtrF32 *float32
|
|
F64 float64
|
|
PtrF64 *float64
|
|
S string
|
|
PtrS *string
|
|
cantSet string
|
|
DoesntExist string
|
|
GoT time.Time
|
|
GoTptr *time.Time
|
|
T Timestamp
|
|
Tptr *Timestamp
|
|
SA StringArray
|
|
}
|
|
|
|
type bindTestStructWithTags struct {
|
|
I int `json:"I" form:"I"`
|
|
PtrI *int `json:"PtrI" form:"PtrI"`
|
|
I8 int8 `json:"I8" form:"I8"`
|
|
PtrI8 *int8 `json:"PtrI8" form:"PtrI8"`
|
|
I16 int16 `json:"I16" form:"I16"`
|
|
PtrI16 *int16 `json:"PtrI16" form:"PtrI16"`
|
|
I32 int32 `json:"I32" form:"I32"`
|
|
PtrI32 *int32 `json:"PtrI32" form:"PtrI32"`
|
|
I64 int64 `json:"I64" form:"I64"`
|
|
PtrI64 *int64 `json:"PtrI64" form:"PtrI64"`
|
|
UI uint `json:"UI" form:"UI"`
|
|
PtrUI *uint `json:"PtrUI" form:"PtrUI"`
|
|
UI8 uint8 `json:"UI8" form:"UI8"`
|
|
PtrUI8 *uint8 `json:"PtrUI8" form:"PtrUI8"`
|
|
UI16 uint16 `json:"UI16" form:"UI16"`
|
|
PtrUI16 *uint16 `json:"PtrUI16" form:"PtrUI16"`
|
|
UI32 uint32 `json:"UI32" form:"UI32"`
|
|
PtrUI32 *uint32 `json:"PtrUI32" form:"PtrUI32"`
|
|
UI64 uint64 `json:"UI64" form:"UI64"`
|
|
PtrUI64 *uint64 `json:"PtrUI64" form:"PtrUI64"`
|
|
B bool `json:"B" form:"B"`
|
|
PtrB *bool `json:"PtrB" form:"PtrB"`
|
|
F32 float32 `json:"F32" form:"F32"`
|
|
PtrF32 *float32 `json:"PtrF32" form:"PtrF32"`
|
|
F64 float64 `json:"F64" form:"F64"`
|
|
PtrF64 *float64 `json:"PtrF64" form:"PtrF64"`
|
|
S string `json:"S" form:"S"`
|
|
PtrS *string `json:"PtrS" form:"PtrS"`
|
|
cantSet string
|
|
DoesntExist string `json:"DoesntExist" form:"DoesntExist"`
|
|
GoT time.Time `json:"GoT" form:"GoT"`
|
|
GoTptr *time.Time `json:"GoTptr" form:"GoTptr"`
|
|
T Timestamp `json:"T" form:"T"`
|
|
Tptr *Timestamp `json:"Tptr" form:"Tptr"`
|
|
SA StringArray `json:"SA" form:"SA"`
|
|
}
|
|
|
|
type Timestamp time.Time
|
|
type TA []Timestamp
|
|
type StringArray []string
|
|
type Struct struct {
|
|
Foo string
|
|
}
|
|
type Bar struct {
|
|
Baz int `json:"baz" query:"baz"`
|
|
}
|
|
|
|
func (t *Timestamp) UnmarshalParam(src string) error {
|
|
ts, err := time.Parse(time.RFC3339, src)
|
|
*t = Timestamp(ts)
|
|
return err
|
|
}
|
|
|
|
func (a *StringArray) UnmarshalParam(src string) error {
|
|
*a = StringArray(strings.Split(src, ","))
|
|
return nil
|
|
}
|
|
|
|
func (s *Struct) UnmarshalParam(src string) error {
|
|
*s = Struct{
|
|
Foo: src,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t bindTestStruct) GetCantSet() string {
|
|
return t.cantSet
|
|
}
|
|
|
|
var values = map[string][]string{
|
|
"I": {"0"},
|
|
"PtrI": {"0"},
|
|
"I8": {"8"},
|
|
"PtrI8": {"8"},
|
|
"I16": {"16"},
|
|
"PtrI16": {"16"},
|
|
"I32": {"32"},
|
|
"PtrI32": {"32"},
|
|
"I64": {"64"},
|
|
"PtrI64": {"64"},
|
|
"UI": {"0"},
|
|
"PtrUI": {"0"},
|
|
"UI8": {"8"},
|
|
"PtrUI8": {"8"},
|
|
"UI16": {"16"},
|
|
"PtrUI16": {"16"},
|
|
"UI32": {"32"},
|
|
"PtrUI32": {"32"},
|
|
"UI64": {"64"},
|
|
"PtrUI64": {"64"},
|
|
"B": {"true"},
|
|
"PtrB": {"true"},
|
|
"F32": {"32.5"},
|
|
"PtrF32": {"32.5"},
|
|
"F64": {"64.5"},
|
|
"PtrF64": {"64.5"},
|
|
"S": {"test"},
|
|
"PtrS": {"test"},
|
|
"cantSet": {"test"},
|
|
"T": {"2016-12-06T19:09:05+01:00"},
|
|
"Tptr": {"2016-12-06T19:09:05+01:00"},
|
|
"GoT": {"2016-12-06T19:09:05+01:00"},
|
|
"GoTptr": {"2016-12-06T19:09:05+01:00"},
|
|
"ST": {"bar"},
|
|
}
|
|
|
|
// ptr return pointer to value. This is useful as `v := []*int8{&int8(1)}` will not compile
|
|
func ptr[T any](value T) *T {
|
|
return &value
|
|
}
|
|
|
|
func TestToMultipleFields(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?id=1&ID=2", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
|
|
type Root struct {
|
|
ID int64 `query:"id"`
|
|
Child2 struct {
|
|
ID int64
|
|
}
|
|
Child1 struct {
|
|
ID int64 `query:"id"`
|
|
}
|
|
}
|
|
|
|
u := new(Root)
|
|
err := c.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, int64(1), u.ID) // perfectly reasonable
|
|
assert.Equal(t, int64(1), u.Child1.ID) // untagged struct containing tagged field gets filled (by tag)
|
|
assert.Equal(t, int64(0), u.Child2.ID) // untagged struct containing untagged field should not be bind
|
|
}
|
|
}
|
|
|
|
func TestBindJSON(t *testing.T) {
|
|
testBindOkay(t, strings.NewReader(userJSON), nil, MIMEApplicationJSON)
|
|
testBindOkay(t, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON)
|
|
testBindArrayOkay(t, strings.NewReader(usersJSON), nil, MIMEApplicationJSON)
|
|
testBindArrayOkay(t, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON)
|
|
testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
|
|
testBindError(t, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})
|
|
}
|
|
|
|
func TestBindXML(t *testing.T) {
|
|
testBindOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)
|
|
testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
|
|
testBindArrayOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)
|
|
testBindArrayOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
|
|
testBindError(t, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New(""))
|
|
testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{})
|
|
testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{})
|
|
testBindOkay(t, strings.NewReader(userXML), nil, MIMETextXML)
|
|
testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMETextXML)
|
|
testBindError(t, strings.NewReader(invalidContent), MIMETextXML, errors.New(""))
|
|
testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{})
|
|
testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{})
|
|
}
|
|
|
|
func TestBindForm(t *testing.T) {
|
|
|
|
testBindOkay(t, strings.NewReader(userForm), nil, MIMEApplicationForm)
|
|
testBindOkay(t, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm)
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userForm))
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
req.Header.Set(HeaderContentType, MIMEApplicationForm)
|
|
err := c.Bind(&[]struct{ Field string }{})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestBindQueryParams(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?id=1&name=Jon+Snow", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Snow", u.Name)
|
|
}
|
|
}
|
|
|
|
func TestBindQueryParamsCaseInsensitive(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?ID=1&NAME=Jon+Snow", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Snow", u.Name)
|
|
}
|
|
}
|
|
|
|
func TestBindQueryParamsCaseSensitivePrioritized(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?id=1&ID=2&NAME=Jon+Snow&name=Jon+Doe", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Doe", u.Name)
|
|
}
|
|
}
|
|
|
|
func TestBindHeaderParam(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set("Name", "Jon Doe")
|
|
req.Header.Set("Id", "2")
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
err := (&DefaultBinder{}).BindHeaders(c, u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 2, u.ID)
|
|
assert.Equal(t, "Jon Doe", u.Name)
|
|
}
|
|
}
|
|
|
|
func TestBindHeaderParamBadType(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set("Id", "salamander")
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
err := (&DefaultBinder{}).BindHeaders(c, u)
|
|
assert.Error(t, err)
|
|
|
|
httpErr, ok := err.(*HTTPError)
|
|
if assert.True(t, ok) {
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalParam(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
T Timestamp `query:"ts"`
|
|
TA []Timestamp `query:"ta"`
|
|
SA StringArray `query:"sa"`
|
|
ST Struct
|
|
StWithTag struct {
|
|
Foo string `query:"st"`
|
|
}
|
|
}{}
|
|
err := c.Bind(&result)
|
|
ts := Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC))
|
|
|
|
if assert.NoError(t, err) {
|
|
// assert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)
|
|
assert.Equal(t, ts, result.T)
|
|
assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA)
|
|
assert.Equal(t, []Timestamp{ts, ts}, result.TA)
|
|
assert.Equal(t, Struct{""}, result.ST) // child struct does not have a field with matching tag
|
|
assert.Equal(t, "baz", result.StWithTag.Foo) // child struct has field with matching tag
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalText(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
T time.Time `query:"ts"`
|
|
TA []time.Time `query:"ta"`
|
|
SA StringArray `query:"sa"`
|
|
ST Struct
|
|
}{}
|
|
err := c.Bind(&result)
|
|
ts := time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)
|
|
if assert.NoError(t, err) {
|
|
// assert.Equal(t, Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)
|
|
assert.Equal(t, ts, result.T)
|
|
assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA)
|
|
assert.Equal(t, []time.Time{ts, ts}, result.TA)
|
|
assert.Equal(t, Struct{""}, result.ST) // field in child struct does not have tag
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalParamPtr(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
Tptr *Timestamp `query:"ts"`
|
|
}{}
|
|
err := c.Bind(&result)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), *result.Tptr)
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalParamAnonymousFieldPtr(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
*Bar
|
|
}{&Bar{}}
|
|
err := c.Bind(&result)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, result.Baz)
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalParamAnonymousFieldPtrNil(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
*Bar
|
|
}{}
|
|
err := c.Bind(&result)
|
|
if assert.NoError(t, err) {
|
|
assert.Nil(t, result.Bar)
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalParamAnonymousFieldPtrCustomTag(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, `/?bar={"baz":100}&baz=1`, nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
*Bar `json:"bar" query:"bar"`
|
|
}{&Bar{}}
|
|
err := c.Bind(&result)
|
|
assert.Contains(t, err.Error(), "query/param/form tags are not allowed with anonymous struct field")
|
|
}
|
|
|
|
func TestBindUnmarshalTextPtr(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
result := struct {
|
|
Tptr *time.Time `query:"ts"`
|
|
}{}
|
|
err := c.Bind(&result)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC), *result.Tptr)
|
|
}
|
|
}
|
|
|
|
func TestBindMultipartForm(t *testing.T) {
|
|
bodyBuffer := new(bytes.Buffer)
|
|
mw := multipart.NewWriter(bodyBuffer)
|
|
mw.WriteField("id", "1")
|
|
mw.WriteField("name", "Jon Snow")
|
|
mw.Close()
|
|
body := bodyBuffer.Bytes()
|
|
|
|
testBindOkay(t, bytes.NewReader(body), nil, mw.FormDataContentType())
|
|
testBindOkay(t, bytes.NewReader(body), dummyQuery, mw.FormDataContentType())
|
|
}
|
|
|
|
func TestBindUnsupportedMediaType(t *testing.T) {
|
|
testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
|
|
}
|
|
|
|
func TestDefaultBinder_bindDataToMap(t *testing.T) {
|
|
exampleData := map[string][]string{
|
|
"multiple": {"1", "2"},
|
|
"single": {"3"},
|
|
}
|
|
|
|
t.Run("ok, bind to map[string]string", func(t *testing.T) {
|
|
dest := map[string]string{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string]string{
|
|
"multiple": "1",
|
|
"single": "3",
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string]string with nil map", func(t *testing.T) {
|
|
var dest map[string]string
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string]string{
|
|
"multiple": "1",
|
|
"single": "3",
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string][]string", func(t *testing.T) {
|
|
dest := map[string][]string{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string][]string{
|
|
"multiple": {"1", "2"},
|
|
"single": {"3"},
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string][]string with nil map", func(t *testing.T) {
|
|
var dest map[string][]string
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string][]string{
|
|
"multiple": {"1", "2"},
|
|
"single": {"3"},
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string]interface", func(t *testing.T) {
|
|
dest := map[string]interface{}{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string]interface{}{
|
|
"multiple": "1",
|
|
"single": "3",
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string]interface with nil map", func(t *testing.T) {
|
|
var dest map[string]interface{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t,
|
|
map[string]interface{}{
|
|
"multiple": "1",
|
|
"single": "3",
|
|
},
|
|
dest,
|
|
)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string]int skips", func(t *testing.T) {
|
|
dest := map[string]int{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t, map[string]int{}, dest)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string]int skips with nil map", func(t *testing.T) {
|
|
var dest map[string]int
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t, map[string]int(nil), dest)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string][]int skips", func(t *testing.T) {
|
|
dest := map[string][]int{}
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t, map[string][]int{}, dest)
|
|
})
|
|
|
|
t.Run("ok, bind to map[string][]int skips with nil map", func(t *testing.T) {
|
|
var dest map[string][]int
|
|
assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param", nil))
|
|
assert.Equal(t, map[string][]int(nil), dest)
|
|
})
|
|
}
|
|
|
|
func TestBindbindData(t *testing.T) {
|
|
ts := new(bindTestStruct)
|
|
b := new(DefaultBinder)
|
|
err := b.bindData(ts, values, "form", nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, 0, ts.I)
|
|
assert.Equal(t, int8(0), ts.I8)
|
|
assert.Equal(t, int16(0), ts.I16)
|
|
assert.Equal(t, int32(0), ts.I32)
|
|
assert.Equal(t, int64(0), ts.I64)
|
|
assert.Equal(t, uint(0), ts.UI)
|
|
assert.Equal(t, uint8(0), ts.UI8)
|
|
assert.Equal(t, uint16(0), ts.UI16)
|
|
assert.Equal(t, uint32(0), ts.UI32)
|
|
assert.Equal(t, uint64(0), ts.UI64)
|
|
assert.Equal(t, false, ts.B)
|
|
assert.Equal(t, float32(0), ts.F32)
|
|
assert.Equal(t, float64(0), ts.F64)
|
|
assert.Equal(t, "", ts.S)
|
|
assert.Equal(t, "", ts.cantSet)
|
|
}
|
|
|
|
func TestBindParam(t *testing.T) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetPath("/users/:id/:name")
|
|
c.SetParamNames("id", "name")
|
|
c.SetParamValues("1", "Jon Snow")
|
|
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Snow", u.Name)
|
|
}
|
|
|
|
// Second test for the absence of a param
|
|
c2 := e.NewContext(req, rec)
|
|
c2.SetPath("/users/:id")
|
|
c2.SetParamNames("id")
|
|
c2.SetParamValues("1")
|
|
|
|
u = new(user)
|
|
err = c2.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "", u.Name)
|
|
}
|
|
|
|
// Bind something with param and post data payload
|
|
body := bytes.NewBufferString(`{ "name": "Jon Snow" }`)
|
|
e2 := New()
|
|
req2 := httptest.NewRequest(http.MethodPost, "/", body)
|
|
req2.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
|
|
|
rec2 := httptest.NewRecorder()
|
|
|
|
c3 := e2.NewContext(req2, rec2)
|
|
c3.SetPath("/users/:id")
|
|
c3.SetParamNames("id")
|
|
c3.SetParamValues("1")
|
|
|
|
u = new(user)
|
|
err = c3.Bind(u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Snow", u.Name)
|
|
}
|
|
}
|
|
|
|
func TestBindUnmarshalTypeError(t *testing.T) {
|
|
body := bytes.NewBufferString(`{ "id": "text" }`)
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodPost, "/", body)
|
|
req.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
|
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
u := new(user)
|
|
|
|
err := c.Bind(u)
|
|
|
|
he := &HTTPError{Code: http.StatusBadRequest, Message: "Unmarshal type error: expected=int, got=string, field=id, offset=14", Internal: err.(*HTTPError).Internal}
|
|
|
|
assert.Equal(t, he, err)
|
|
}
|
|
|
|
func TestBindSetWithProperType(t *testing.T) {
|
|
ts := new(bindTestStruct)
|
|
typ := reflect.TypeOf(ts).Elem()
|
|
val := reflect.ValueOf(ts).Elem()
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
typeField := typ.Field(i)
|
|
structField := val.Field(i)
|
|
if !structField.CanSet() {
|
|
continue
|
|
}
|
|
if len(values[typeField.Name]) == 0 {
|
|
continue
|
|
}
|
|
val := values[typeField.Name][0]
|
|
err := setWithProperType(typeField.Type.Kind(), val, structField)
|
|
assert.NoError(t, err)
|
|
}
|
|
assertBindTestStruct(t, ts)
|
|
|
|
type foo struct {
|
|
Bar bytes.Buffer
|
|
}
|
|
v := &foo{}
|
|
typ = reflect.TypeOf(v).Elem()
|
|
val = reflect.ValueOf(v).Elem()
|
|
assert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0)))
|
|
}
|
|
|
|
func BenchmarkBindbindDataWithTags(b *testing.B) {
|
|
b.ReportAllocs()
|
|
ts := new(bindTestStructWithTags)
|
|
binder := new(DefaultBinder)
|
|
var err error
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
err = binder.bindData(ts, values, "form", nil)
|
|
}
|
|
assert.NoError(b, err)
|
|
assertBindTestStruct(b, (*bindTestStruct)(ts))
|
|
}
|
|
|
|
func assertBindTestStruct(tb testing.TB, ts *bindTestStruct) {
|
|
assert.Equal(tb, 0, ts.I)
|
|
assert.Equal(tb, int8(8), ts.I8)
|
|
assert.Equal(tb, int16(16), ts.I16)
|
|
assert.Equal(tb, int32(32), ts.I32)
|
|
assert.Equal(tb, int64(64), ts.I64)
|
|
assert.Equal(tb, uint(0), ts.UI)
|
|
assert.Equal(tb, uint8(8), ts.UI8)
|
|
assert.Equal(tb, uint16(16), ts.UI16)
|
|
assert.Equal(tb, uint32(32), ts.UI32)
|
|
assert.Equal(tb, uint64(64), ts.UI64)
|
|
assert.Equal(tb, true, ts.B)
|
|
assert.Equal(tb, float32(32.5), ts.F32)
|
|
assert.Equal(tb, float64(64.5), ts.F64)
|
|
assert.Equal(tb, "test", ts.S)
|
|
assert.Equal(tb, "", ts.GetCantSet())
|
|
}
|
|
|
|
func testBindOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {
|
|
e := New()
|
|
path := "/"
|
|
if len(query) > 0 {
|
|
path += "?" + query.Encode()
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, path, r)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
req.Header.Set(HeaderContentType, ctype)
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
if assert.Equal(t, nil, err) {
|
|
assert.Equal(t, 1, u.ID)
|
|
assert.Equal(t, "Jon Snow", u.Name)
|
|
}
|
|
}
|
|
|
|
func testBindArrayOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {
|
|
e := New()
|
|
path := "/"
|
|
if len(query) > 0 {
|
|
path += "?" + query.Encode()
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, path, r)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
req.Header.Set(HeaderContentType, ctype)
|
|
u := []user{}
|
|
err := c.Bind(&u)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, 1, len(u))
|
|
assert.Equal(t, 1, u[0].ID)
|
|
assert.Equal(t, "Jon Snow", u[0].Name)
|
|
}
|
|
}
|
|
|
|
func testBindError(t *testing.T, r io.Reader, ctype string, expectedInternal error) {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodPost, "/", r)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
req.Header.Set(HeaderContentType, ctype)
|
|
u := new(user)
|
|
err := c.Bind(u)
|
|
|
|
switch {
|
|
case strings.HasPrefix(ctype, MIMEApplicationJSON), strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML),
|
|
strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
|
|
if assert.IsType(t, new(HTTPError), err) {
|
|
assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)
|
|
assert.IsType(t, expectedInternal, err.(*HTTPError).Internal)
|
|
}
|
|
default:
|
|
if assert.IsType(t, new(HTTPError), err) {
|
|
assert.Equal(t, ErrUnsupportedMediaType, err)
|
|
assert.IsType(t, expectedInternal, err.(*HTTPError).Internal)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
|
|
// tests to check binding behaviour when multiple sources (path params, query params and request body) are in use
|
|
// binding is done in steps and one source could overwrite previous source binded data
|
|
// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
|
|
|
|
type Opts struct {
|
|
ID int `json:"id" form:"id" query:"id"`
|
|
Node string `json:"node" form:"node" query:"node" param:"node"`
|
|
Lang string
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
givenURL string
|
|
givenContent io.Reader
|
|
givenMethod string
|
|
whenBindTarget interface{}
|
|
whenNoPathParams bool
|
|
expect interface{}
|
|
expectError string
|
|
}{
|
|
{
|
|
name: "ok, POST bind to struct with: path param + query param + body",
|
|
givenMethod: http.MethodPost,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Opts{ID: 1, Node: "node_from_path"}, // query params are not used, node is filled from path
|
|
},
|
|
{
|
|
name: "ok, PUT bind to struct with: path param + query param + body",
|
|
givenMethod: http.MethodPut,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Opts{ID: 1, Node: "node_from_path"}, // query params are not used
|
|
},
|
|
{
|
|
name: "ok, GET bind to struct with: path param + query param + body",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Opts{ID: 1, Node: "xxx"}, // query overwrites previous path value
|
|
},
|
|
{
|
|
name: "ok, GET bind to struct with: path param + query param + body",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Opts{ID: 1, Node: "zzz"}, // body is binded last and overwrites previous (path,query) values
|
|
},
|
|
{
|
|
name: "ok, DELETE bind to struct with: path param + query param + body",
|
|
givenMethod: http.MethodDelete,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Opts{ID: 1, Node: "zzz"}, // for DELETE body is binded after query params
|
|
},
|
|
{
|
|
name: "ok, POST bind to struct with: path param + body",
|
|
givenMethod: http.MethodPost,
|
|
givenURL: "/api/real_node/endpoint",
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Opts{ID: 1, Node: "node_from_path"},
|
|
},
|
|
{
|
|
name: "ok, POST bind to struct with path + query + body = body has priority",
|
|
givenMethod: http.MethodPost,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Opts{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
|
},
|
|
{
|
|
name: "nok, POST body bind failure",
|
|
givenMethod: http.MethodPost,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`{`),
|
|
expect: &Opts{ID: 0, Node: "node_from_path"}, // query binding has already modified bind target
|
|
expectError: "code=400, message=unexpected EOF, internal=unexpected EOF",
|
|
},
|
|
{
|
|
name: "nok, GET with body bind failure when types are not convertible",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?id=nope",
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Opts{ID: 0, Node: "node_from_path"}, // path params binding has already modified bind target
|
|
expectError: "code=400, message=strconv.ParseInt: parsing \"nope\": invalid syntax, internal=strconv.ParseInt: parsing \"nope\": invalid syntax",
|
|
},
|
|
{
|
|
name: "nok, GET body bind failure - trying to bind json array to struct",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
expect: &Opts{ID: 0, Node: "xxx"}, // query binding has already modified bind target
|
|
expectError: "code=400, message=Unmarshal type error: expected=echo.Opts, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Opts",
|
|
},
|
|
{ // query param is ignored as we do not know where exactly to bind it in slice
|
|
name: "ok, GET bind to struct slice, ignore query param",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
whenNoPathParams: true,
|
|
whenBindTarget: &[]Opts{},
|
|
expect: &[]Opts{
|
|
{ID: 1, Node: ""},
|
|
},
|
|
},
|
|
{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice
|
|
name: "ok, POST binding to slice should not be affected query params types",
|
|
givenMethod: http.MethodPost,
|
|
givenURL: "/api/real_node/endpoint?id=nope&node=xxx",
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
whenNoPathParams: true,
|
|
whenBindTarget: &[]Opts{},
|
|
expect: &[]Opts{{ID: 1}},
|
|
expectError: "",
|
|
},
|
|
{ // path param is ignored as we do not know where exactly to bind it in slice
|
|
name: "ok, GET bind to struct slice, ignore path param",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
whenBindTarget: &[]Opts{},
|
|
expect: &[]Opts{
|
|
{ID: 1, Node: ""},
|
|
},
|
|
},
|
|
{
|
|
name: "ok, GET body bind json array to slice",
|
|
givenMethod: http.MethodGet,
|
|
givenURL: "/api/real_node/endpoint",
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
whenNoPathParams: true,
|
|
whenBindTarget: &[]Opts{},
|
|
expect: &[]Opts{{ID: 1, Node: ""}},
|
|
expectError: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
e := New()
|
|
// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
|
|
req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
|
|
req.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
|
|
if !tc.whenNoPathParams {
|
|
c.SetParamNames("node")
|
|
c.SetParamValues("node_from_path")
|
|
}
|
|
|
|
var bindTarget interface{}
|
|
if tc.whenBindTarget != nil {
|
|
bindTarget = tc.whenBindTarget
|
|
} else {
|
|
bindTarget = &Opts{}
|
|
}
|
|
b := new(DefaultBinder)
|
|
|
|
err := b.Bind(bindTarget, c)
|
|
if tc.expectError != "" {
|
|
assert.EqualError(t, err, tc.expectError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
assert.Equal(t, tc.expect, bindTarget)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultBinder_BindBody(t *testing.T) {
|
|
// tests to check binding behaviour when multiple sources (path params, query params and request body) are in use
|
|
// generally when binding from request body - URL and path params are ignored - unless form is being binded.
|
|
// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
|
|
|
|
type Node struct {
|
|
ID int `json:"id" xml:"id" form:"id" query:"id"`
|
|
Node string `json:"node" xml:"node" form:"node" query:"node" param:"node"`
|
|
}
|
|
type Nodes struct {
|
|
Nodes []Node `xml:"node" form:"node"`
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
givenURL string
|
|
givenContent io.Reader
|
|
givenMethod string
|
|
givenContentType string
|
|
whenNoPathParams bool
|
|
whenChunkedBody bool
|
|
whenBindTarget interface{}
|
|
expect interface{}
|
|
expectError string
|
|
}{
|
|
{
|
|
name: "ok, JSON POST bind to struct with: path + query + empty field in body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
|
|
},
|
|
{
|
|
name: "ok, JSON POST bind to struct with: path + query + body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
|
},
|
|
{
|
|
name: "ok, JSON POST body bind json array to slice (has matching path/query params)",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
|
whenNoPathParams: true,
|
|
whenBindTarget: &[]Node{},
|
|
expect: &[]Node{{ID: 1, Node: ""}},
|
|
expectError: "",
|
|
},
|
|
{ // rare case as GET is not usually used to send request body
|
|
name: "ok, JSON GET bind to struct with: path + query + empty field in body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodGet,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`{"id": 1}`),
|
|
expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
|
|
},
|
|
{ // rare case as GET is not usually used to send request body
|
|
name: "ok, JSON GET bind to struct with: path + query + body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodGet,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
|
expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
|
},
|
|
{
|
|
name: "nok, JSON POST body bind failure",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(`{`),
|
|
expect: &Node{ID: 0, Node: ""},
|
|
expectError: "code=400, message=unexpected EOF, internal=unexpected EOF",
|
|
},
|
|
{
|
|
name: "ok, XML POST bind to struct with: path + query + empty body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationXML,
|
|
givenContent: strings.NewReader(`<node><id>1</id><node>yyy</node></node>`),
|
|
expect: &Node{ID: 1, Node: "yyy"},
|
|
},
|
|
{
|
|
name: "ok, XML POST bind array to slice with: path + query + body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationXML,
|
|
givenContent: strings.NewReader(`<nodes><node><id>1</id><node>yyy</node></node></nodes>`),
|
|
whenBindTarget: &Nodes{},
|
|
expect: &Nodes{Nodes: []Node{{ID: 1, Node: "yyy"}}},
|
|
},
|
|
{
|
|
name: "nok, XML POST bind failure",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationXML,
|
|
givenContent: strings.NewReader(`<node><`),
|
|
expect: &Node{ID: 0, Node: ""},
|
|
expectError: "code=400, message=Syntax error: line=1, error=XML syntax error on line 1: unexpected EOF, internal=XML syntax error on line 1: unexpected EOF",
|
|
},
|
|
{
|
|
name: "ok, FORM POST bind to struct with: path + query + body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationForm,
|
|
givenContent: strings.NewReader(`id=1&node=yyy`),
|
|
expect: &Node{ID: 1, Node: "yyy"},
|
|
},
|
|
{
|
|
// NB: form values are taken from BOTH body and query for POST/PUT/PATCH by standard library implementation
|
|
// See: https://golang.org/pkg/net/http/#Request.ParseForm
|
|
name: "ok, FORM POST bind to struct with: path + query + empty field in body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationForm,
|
|
givenContent: strings.NewReader(`id=1`),
|
|
expect: &Node{ID: 1, Node: "xxx"},
|
|
},
|
|
{
|
|
// NB: form values are taken from query by standard library implementation
|
|
// See: https://golang.org/pkg/net/http/#Request.ParseForm
|
|
name: "ok, FORM GET bind to struct with: path + query + empty field in body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodGet,
|
|
givenContentType: MIMEApplicationForm,
|
|
givenContent: strings.NewReader(`id=1`),
|
|
expect: &Node{ID: 0, Node: "xxx"}, // 'xxx' is taken from URL and body is not used with GET by implementation
|
|
},
|
|
{
|
|
name: "nok, unsupported content type",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMETextPlain,
|
|
givenContent: strings.NewReader(`<html></html>`),
|
|
expect: &Node{ID: 0, Node: ""},
|
|
expectError: "code=415, message=Unsupported Media Type",
|
|
},
|
|
// FIXME: REASON in Go 1.24 and earlier http.NoBody would result ContentLength=-1
|
|
// but as of Go 1.25 http.NoBody would result ContentLength=0
|
|
// I am too lazy to bother documenting this as 2 version specific tests.
|
|
//{
|
|
// name: "nok, JSON POST with http.NoBody",
|
|
// givenURL: "/api/real_node/endpoint?node=xxx",
|
|
// givenMethod: http.MethodPost,
|
|
// givenContentType: MIMEApplicationJSON,
|
|
// givenContent: http.NoBody,
|
|
// expect: &Node{ID: 0, Node: ""},
|
|
// expectError: "code=400, message=EOF, internal=EOF",
|
|
//},
|
|
{
|
|
name: "ok, JSON POST with empty body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: strings.NewReader(""),
|
|
expect: &Node{ID: 0, Node: ""},
|
|
},
|
|
{
|
|
name: "ok, JSON POST bind to struct with: path + query + chunked body",
|
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
|
givenMethod: http.MethodPost,
|
|
givenContentType: MIMEApplicationJSON,
|
|
givenContent: httputil.NewChunkedReader(strings.NewReader("18\r\n" + `{"id": 1, "node": "zzz"}` + "\r\n0\r\n")),
|
|
whenChunkedBody: true,
|
|
expect: &Node{ID: 1, Node: "zzz"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
e := New()
|
|
// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
|
|
req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
|
|
switch tc.givenContentType {
|
|
case MIMEApplicationXML:
|
|
req.Header.Set(HeaderContentType, MIMEApplicationXML)
|
|
case MIMEApplicationForm:
|
|
req.Header.Set(HeaderContentType, MIMEApplicationForm)
|
|
case MIMEApplicationJSON:
|
|
req.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
|
}
|
|
if tc.whenChunkedBody {
|
|
req.ContentLength = -1
|
|
req.TransferEncoding = append(req.TransferEncoding, "chunked")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
|
|
if !tc.whenNoPathParams {
|
|
c.SetParamNames("node")
|
|
c.SetParamValues("real_node")
|
|
}
|
|
|
|
var bindTarget interface{}
|
|
if tc.whenBindTarget != nil {
|
|
bindTarget = tc.whenBindTarget
|
|
} else {
|
|
bindTarget = &Node{}
|
|
}
|
|
b := new(DefaultBinder)
|
|
|
|
err := b.BindBody(c, bindTarget)
|
|
if tc.expectError != "" {
|
|
assert.EqualError(t, err, tc.expectError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
assert.Equal(t, tc.expect, bindTarget)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testBindURL(queryString string, target any) error {
|
|
e := New()
|
|
req := httptest.NewRequest(http.MethodGet, queryString, nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
return c.Bind(target)
|
|
}
|
|
|
|
type unixTimestamp struct {
|
|
Time time.Time
|
|
}
|
|
|
|
func (t *unixTimestamp) UnmarshalParam(param string) error {
|
|
n, err := strconv.ParseInt(param, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not an integer", param)
|
|
}
|
|
*t = unixTimestamp{Time: time.Unix(n, 0)}
|
|
return err
|
|
}
|
|
|
|
type IntArrayA []int
|
|
|
|
// UnmarshalParam converts value to *Int64Slice. This allows the API to accept
|
|
// a comma-separated list of integers as a query parameter.
|
|
func (i *IntArrayA) UnmarshalParam(value string) error {
|
|
var values = strings.Split(value, ",")
|
|
var numbers = make([]int, 0, len(values))
|
|
|
|
for _, v := range values {
|
|
n, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not an integer", v)
|
|
}
|
|
|
|
numbers = append(numbers, int(n))
|
|
}
|
|
|
|
*i = append(*i, numbers...)
|
|
return nil
|
|
}
|
|
|
|
func TestBindUnmarshalParamExtras(t *testing.T) {
|
|
// this test documents how bind handles `BindUnmarshaler` interface:
|
|
// NOTE: BindUnmarshaler chooses first input value to be bound.
|
|
|
|
t.Run("nok, unmarshalling fails", func(t *testing.T) {
|
|
result := struct {
|
|
V unixTimestamp `query:"t"`
|
|
}{}
|
|
err := testBindURL("/?t=xxxx", &result)
|
|
|
|
assert.EqualError(t, err, "code=400, message='xxxx' is not an integer, internal='xxxx' is not an integer")
|
|
})
|
|
|
|
t.Run("ok, target is struct", func(t *testing.T) {
|
|
result := struct {
|
|
V unixTimestamp `query:"t"`
|
|
}{}
|
|
err := testBindURL("/?t=1710095540&t=1710095541", &result)
|
|
|
|
assert.NoError(t, err)
|
|
expect := unixTimestamp{
|
|
Time: time.Unix(1710095540, 0),
|
|
}
|
|
assert.Equal(t, expect, result.V)
|
|
})
|
|
|
|
t.Run("ok, target is an alias to slice and is nil, append only values from first", func(t *testing.T) {
|
|
result := struct {
|
|
V IntArrayA `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1,2,3&a=4,5,6", &result)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, IntArrayA([]int{1, 2, 3}), result.V)
|
|
})
|
|
|
|
t.Run("ok, target is an alias to slice and is nil, single input", func(t *testing.T) {
|
|
result := struct {
|
|
V IntArrayA `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1,2", &result)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, IntArrayA([]int{1, 2}), result.V)
|
|
})
|
|
|
|
t.Run("ok, target is pointer an alias to slice and is nil", func(t *testing.T) {
|
|
result := struct {
|
|
V *IntArrayA `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1&a=4,5,6", &result)
|
|
|
|
assert.NoError(t, err)
|
|
var expected = IntArrayA([]int{1})
|
|
assert.Equal(t, &expected, result.V)
|
|
})
|
|
|
|
t.Run("ok, target is pointer an alias to slice and is NOT nil", func(t *testing.T) {
|
|
result := struct {
|
|
V *IntArrayA `query:"a"`
|
|
}{}
|
|
result.V = new(IntArrayA) // NOT nil
|
|
|
|
err := testBindURL("/?a=1&a=4,5,6", &result)
|
|
|
|
assert.NoError(t, err)
|
|
var expected = IntArrayA([]int{1})
|
|
assert.Equal(t, &expected, result.V)
|
|
})
|
|
}
|
|
|
|
type unixTimestampLast struct {
|
|
Time time.Time
|
|
}
|
|
|
|
// this is silly example for `bindMultipleUnmarshaler` for type that uses last input value for unmarshalling
|
|
func (t *unixTimestampLast) UnmarshalParams(params []string) error {
|
|
lastInput := params[len(params)-1]
|
|
n, err := strconv.ParseInt(lastInput, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not an integer", lastInput)
|
|
}
|
|
*t = unixTimestampLast{Time: time.Unix(n, 0)}
|
|
return err
|
|
}
|
|
|
|
type IntArrayB []int
|
|
|
|
func (i *IntArrayB) UnmarshalParams(params []string) error {
|
|
var numbers = make([]int, 0, len(params))
|
|
|
|
for _, param := range params {
|
|
var values = strings.Split(param, ",")
|
|
for _, v := range values {
|
|
n, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("'%s' is not an integer", v)
|
|
}
|
|
numbers = append(numbers, int(n))
|
|
}
|
|
}
|
|
|
|
*i = append(*i, numbers...)
|
|
return nil
|
|
}
|
|
|
|
func TestBindUnmarshalParams(t *testing.T) {
|
|
// this test documents how bind handles `bindMultipleUnmarshaler` interface:
|
|
|
|
t.Run("nok, unmarshalling fails", func(t *testing.T) {
|
|
result := struct {
|
|
V unixTimestampLast `query:"t"`
|
|
}{}
|
|
err := testBindURL("/?t=xxxx", &result)
|
|
|
|
assert.EqualError(t, err, "code=400, message='xxxx' is not an integer, internal='xxxx' is not an integer")
|
|
})
|
|
|
|
t.Run("ok, target is struct", func(t *testing.T) {
|
|
result := struct {
|
|
V unixTimestampLast `query:"t"`
|
|
}{}
|
|
err := testBindURL("/?t=1710095540&t=1710095541", &result)
|
|
|
|
assert.NoError(t, err)
|
|
expect := unixTimestampLast{
|
|
Time: time.Unix(1710095541, 0),
|
|
}
|
|
assert.Equal(t, expect, result.V)
|
|
})
|
|
|
|
t.Run("ok, target is an alias to slice and is nil, append multiple inputs", func(t *testing.T) {
|
|
result := struct {
|
|
V IntArrayB `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1,2,3&a=4,5,6", &result)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, IntArrayB([]int{1, 2, 3, 4, 5, 6}), result.V)
|
|
})
|
|
|
|
t.Run("ok, target is an alias to slice and is nil, single input", func(t *testing.T) {
|
|
result := struct {
|
|
V IntArrayB `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1,2", &result)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, IntArrayB([]int{1, 2}), result.V)
|
|
})
|
|
|
|
t.Run("ok, target is pointer an alias to slice and is nil", func(t *testing.T) {
|
|
result := struct {
|
|
V *IntArrayB `query:"a"`
|
|
}{}
|
|
err := testBindURL("/?a=1&a=4,5,6", &result)
|
|
|
|
assert.NoError(t, err)
|
|
var expected = IntArrayB([]int{1, 4, 5, 6})
|
|
assert.Equal(t, &expected, result.V)
|
|
})
|
|
|
|
t.Run("ok, target is pointer an alias to slice and is NOT nil", func(t *testing.T) {
|
|
result := struct {
|
|
V *IntArrayB `query:"a"`
|
|
}{}
|
|
result.V = new(IntArrayB) // NOT nil
|
|
|
|
err := testBindURL("/?a=1&a=4,5,6", &result)
|
|
assert.NoError(t, err)
|
|
var expected = IntArrayB([]int{1, 4, 5, 6})
|
|
assert.Equal(t, &expected, result.V)
|
|
})
|
|
}
|
|
|
|
func TestBindInt8(t *testing.T) {
|
|
t.Run("nok, binding fails", func(t *testing.T) {
|
|
type target struct {
|
|
V int8 `query:"v"`
|
|
}
|
|
p := target{}
|
|
err := testBindURL("/?v=x&v=2", &p)
|
|
assert.EqualError(t, err, "code=400, message=strconv.ParseInt: parsing \"x\": invalid syntax, internal=strconv.ParseInt: parsing \"x\": invalid syntax")
|
|
})
|
|
|
|
t.Run("nok, int8 embedded in struct", func(t *testing.T) {
|
|
type target struct {
|
|
int8 `query:"v"` // embedded field is `Anonymous`. We can only set public fields
|
|
}
|
|
p := target{}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{0}, p)
|
|
})
|
|
|
|
t.Run("nok, pointer to int8 embedded in struct", func(t *testing.T) {
|
|
type target struct {
|
|
*int8 `query:"v"` // embedded field is `Anonymous`. We can only set public fields
|
|
}
|
|
p := target{}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, target{int8: nil}, p)
|
|
})
|
|
|
|
t.Run("ok, bind int8 as struct field", func(t *testing.T) {
|
|
type target struct {
|
|
V int8 `query:"v"`
|
|
}
|
|
p := target{V: 127}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: 1}, p)
|
|
})
|
|
|
|
t.Run("ok, bind pointer to int8 as struct field, value is nil", func(t *testing.T) {
|
|
type target struct {
|
|
V *int8 `query:"v"`
|
|
}
|
|
p := target{}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: ptr(int8(1))}, p)
|
|
})
|
|
|
|
t.Run("ok, bind pointer to int8 as struct field, value is set", func(t *testing.T) {
|
|
type target struct {
|
|
V *int8 `query:"v"`
|
|
}
|
|
p := target{V: ptr(int8(127))}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: ptr(int8(1))}, p)
|
|
})
|
|
|
|
t.Run("ok, bind int8 slice as struct field, value is nil", func(t *testing.T) {
|
|
type target struct {
|
|
V []int8 `query:"v"`
|
|
}
|
|
p := target{}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: []int8{1, 2}}, p)
|
|
})
|
|
|
|
t.Run("ok, bind slice of int8 as struct field, value is set", func(t *testing.T) {
|
|
type target struct {
|
|
V []int8 `query:"v"`
|
|
}
|
|
p := target{V: []int8{111}}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: []int8{1, 2}}, p)
|
|
})
|
|
|
|
t.Run("ok, bind slice of pointer to int8 as struct field, value is set", func(t *testing.T) {
|
|
type target struct {
|
|
V []*int8 `query:"v"`
|
|
}
|
|
p := target{V: []*int8{ptr(int8(127))}}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: []*int8{ptr(int8(1)), ptr(int8(2))}}, p)
|
|
})
|
|
|
|
t.Run("ok, bind pointer to slice of int8 as struct field, value is set", func(t *testing.T) {
|
|
type target struct {
|
|
V *[]int8 `query:"v"`
|
|
}
|
|
p := target{V: &[]int8{111}}
|
|
err := testBindURL("/?v=1&v=2", &p)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, target{V: &[]int8{1, 2}}, p)
|
|
})
|
|
}
|
|
|
|
func TestBindMultipartFormFiles(t *testing.T) {
|
|
file1 := createTestFormFile("file", "file1.txt")
|
|
file11 := createTestFormFile("file", "file11.txt")
|
|
file2 := createTestFormFile("file2", "file2.txt")
|
|
filesA := createTestFormFile("files", "filesA.txt")
|
|
filesB := createTestFormFile("files", "filesB.txt")
|
|
|
|
t.Run("nok, can not bind to multipart file struct", func(t *testing.T) {
|
|
var target struct {
|
|
File multipart.FileHeader `form:"file"`
|
|
}
|
|
err := bindMultipartFiles(t, &target, file1, file2) // file2 should be ignored
|
|
|
|
assert.EqualError(t, err, "code=400, message=binding to multipart.FileHeader struct is not supported, use pointer to struct, internal=binding to multipart.FileHeader struct is not supported, use pointer to struct")
|
|
})
|
|
|
|
t.Run("ok, bind single multipart file to pointer to multipart file", func(t *testing.T) {
|
|
var target struct {
|
|
File *multipart.FileHeader `form:"file"`
|
|
}
|
|
err := bindMultipartFiles(t, &target, file1, file2) // file2 should be ignored
|
|
|
|
assert.NoError(t, err)
|
|
assertMultipartFileHeader(t, target.File, file1)
|
|
})
|
|
|
|
t.Run("ok, bind multiple multipart files to pointer to multipart file", func(t *testing.T) {
|
|
var target struct {
|
|
File *multipart.FileHeader `form:"file"`
|
|
}
|
|
err := bindMultipartFiles(t, &target, file1, file11)
|
|
|
|
assert.NoError(t, err)
|
|
assertMultipartFileHeader(t, target.File, file1) // should choose first one
|
|
})
|
|
|
|
t.Run("ok, bind multiple multipart files to slice of multipart file", func(t *testing.T) {
|
|
var target struct {
|
|
Files []multipart.FileHeader `form:"files"`
|
|
}
|
|
err := bindMultipartFiles(t, &target, filesA, filesB, file1)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, target.Files, 2)
|
|
assertMultipartFileHeader(t, &target.Files[0], filesA)
|
|
assertMultipartFileHeader(t, &target.Files[1], filesB)
|
|
})
|
|
|
|
t.Run("ok, bind multiple multipart files to slice of pointer to multipart file", func(t *testing.T) {
|
|
var target struct {
|
|
Files []*multipart.FileHeader `form:"files"`
|
|
}
|
|
err := bindMultipartFiles(t, &target, filesA, filesB, file1)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, target.Files, 2)
|
|
assertMultipartFileHeader(t, target.Files[0], filesA)
|
|
assertMultipartFileHeader(t, target.Files[1], filesB)
|
|
})
|
|
}
|
|
|
|
type testFormFile struct {
|
|
Fieldname string
|
|
Filename string
|
|
Content []byte
|
|
}
|
|
|
|
func createTestFormFile(formFieldName string, filename string) testFormFile {
|
|
return testFormFile{
|
|
Fieldname: formFieldName,
|
|
Filename: filename,
|
|
Content: []byte(strings.Repeat(filename, 10)),
|
|
}
|
|
}
|
|
|
|
func bindMultipartFiles(t *testing.T, target any, files ...testFormFile) error {
|
|
var body bytes.Buffer
|
|
mw := multipart.NewWriter(&body)
|
|
|
|
for _, file := range files {
|
|
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
|
|
assert.NoError(t, err)
|
|
|
|
n, err := fw.Write(file.Content)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, len(file.Content), n)
|
|
}
|
|
|
|
err := mw.Close()
|
|
assert.NoError(t, err)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, "/", &body)
|
|
assert.NoError(t, err)
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
e := New()
|
|
c := e.NewContext(req, rec)
|
|
return c.Bind(target)
|
|
}
|
|
|
|
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFormFile) {
|
|
assert.Equal(t, file.Filename, fh.Filename)
|
|
assert.Equal(t, int64(len(file.Content)), fh.Size)
|
|
fl, err := fh.Open()
|
|
assert.NoError(t, err)
|
|
body, err := io.ReadAll(fl)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, string(file.Content), string(body))
|
|
err = fl.Close()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestTimeFormatBinding(t *testing.T) {
|
|
type TestStruct struct {
|
|
DateTimeLocal time.Time `form:"datetime_local" format:"2006-01-02T15:04"`
|
|
Date time.Time `query:"date" format:"2006-01-02"`
|
|
CustomFormat time.Time `form:"custom" format:"01/02/2006 15:04:05"`
|
|
DefaultTime time.Time `form:"default_time"` // No format tag - should use default parsing
|
|
PtrTime *time.Time `query:"ptr_time" format:"2006-01-02"`
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
contentType string
|
|
data string
|
|
queryParams string
|
|
expect TestStruct
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "ok, datetime-local format binding",
|
|
contentType: MIMEApplicationForm,
|
|
data: "datetime_local=2023-12-25T14:30&default_time=2023-12-25T14:30:45Z",
|
|
expect: TestStruct{
|
|
DateTimeLocal: time.Date(2023, 12, 25, 14, 30, 0, 0, time.UTC),
|
|
DefaultTime: time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC),
|
|
},
|
|
},
|
|
{
|
|
name: "ok, date format binding via query params",
|
|
queryParams: "?date=2023-01-15&ptr_time=2023-02-20",
|
|
expect: TestStruct{
|
|
Date: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
PtrTime: &time.Time{},
|
|
},
|
|
},
|
|
{
|
|
name: "ok, custom format via form data",
|
|
contentType: MIMEApplicationForm,
|
|
data: "custom=12/25/2023 14:30:45",
|
|
expect: TestStruct{
|
|
CustomFormat: time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC),
|
|
},
|
|
},
|
|
{
|
|
name: "nok, invalid format should fail",
|
|
contentType: MIMEApplicationForm,
|
|
data: "datetime_local=invalid-date",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "nok, wrong format should fail",
|
|
contentType: MIMEApplicationForm,
|
|
data: "datetime_local=2023-12-25", // Missing time part
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
e := New()
|
|
var req *http.Request
|
|
|
|
if tc.contentType == MIMEApplicationJSON {
|
|
req = httptest.NewRequest(http.MethodPost, "/"+tc.queryParams, strings.NewReader(tc.data))
|
|
req.Header.Set(HeaderContentType, tc.contentType)
|
|
} else if tc.contentType == MIMEApplicationForm {
|
|
req = httptest.NewRequest(http.MethodPost, "/"+tc.queryParams, strings.NewReader(tc.data))
|
|
req.Header.Set(HeaderContentType, tc.contentType)
|
|
} else {
|
|
req = httptest.NewRequest(http.MethodGet, "/"+tc.queryParams, nil)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
|
|
var result TestStruct
|
|
err := c.Bind(&result)
|
|
|
|
if tc.expectError {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
|
|
// Check individual fields since time comparison can be tricky
|
|
if !tc.expect.DateTimeLocal.IsZero() {
|
|
assert.True(t, tc.expect.DateTimeLocal.Equal(result.DateTimeLocal),
|
|
"DateTimeLocal: expected %v, got %v", tc.expect.DateTimeLocal, result.DateTimeLocal)
|
|
}
|
|
if !tc.expect.Date.IsZero() {
|
|
assert.True(t, tc.expect.Date.Equal(result.Date),
|
|
"Date: expected %v, got %v", tc.expect.Date, result.Date)
|
|
}
|
|
if !tc.expect.CustomFormat.IsZero() {
|
|
assert.True(t, tc.expect.CustomFormat.Equal(result.CustomFormat),
|
|
"CustomFormat: expected %v, got %v", tc.expect.CustomFormat, result.CustomFormat)
|
|
}
|
|
if !tc.expect.DefaultTime.IsZero() {
|
|
assert.True(t, tc.expect.DefaultTime.Equal(result.DefaultTime),
|
|
"DefaultTime: expected %v, got %v", tc.expect.DefaultTime, result.DefaultTime)
|
|
}
|
|
if tc.expect.PtrTime != nil {
|
|
assert.NotNil(t, result.PtrTime)
|
|
if result.PtrTime != nil {
|
|
expectedPtr := time.Date(2023, 2, 20, 0, 0, 0, 0, time.UTC)
|
|
assert.True(t, expectedPtr.Equal(*result.PtrTime),
|
|
"PtrTime: expected %v, got %v", expectedPtr, *result.PtrTime)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|