2016-05-01 21:38:51 +02:00
package echo
import (
"bytes"
2018-09-28 19:41:13 +02:00
"encoding/json"
"encoding/xml"
"errors"
2016-05-01 21:38:51 +02:00
"io"
"mime/multipart"
"net/http"
2016-09-23 07:53:44 +02:00
"net/http/httptest"
2021-04-06 09:05:33 +02:00
"net/url"
2016-05-01 21:38:51 +02:00
"reflect"
2018-09-28 19:41:13 +02:00
"strconv"
2016-05-01 21:38:51 +02:00
"strings"
"testing"
2016-12-15 20:23:14 +02:00
"time"
2016-05-01 21:38:51 +02:00
"github.com/stretchr/testify/assert"
)
type (
2016-12-15 21:08:56 +02:00
bindTestStruct struct {
2016-05-01 21:38:51 +02:00
I int
2017-04-28 20:07:20 +02:00
PtrI * int
2016-05-01 21:38:51 +02:00
I8 int8
2017-04-28 20:07:20 +02:00
PtrI8 * int8
2016-05-01 21:38:51 +02:00
I16 int16
2017-04-28 20:07:20 +02:00
PtrI16 * int16
2016-05-01 21:38:51 +02:00
I32 int32
2017-04-28 20:07:20 +02:00
PtrI32 * int32
2016-05-01 21:38:51 +02:00
I64 int64
2017-04-28 20:07:20 +02:00
PtrI64 * int64
2016-05-01 21:38:51 +02:00
UI uint
2017-04-28 20:07:20 +02:00
PtrUI * uint
2016-05-01 21:38:51 +02:00
UI8 uint8
2017-04-28 20:07:20 +02:00
PtrUI8 * uint8
2016-05-01 21:38:51 +02:00
UI16 uint16
2017-04-28 20:07:20 +02:00
PtrUI16 * uint16
2016-05-01 21:38:51 +02:00
UI32 uint32
2017-04-28 20:07:20 +02:00
PtrUI32 * uint32
2016-05-01 21:38:51 +02:00
UI64 uint64
2017-04-28 20:07:20 +02:00
PtrUI64 * uint64
2016-05-01 21:38:51 +02:00
B bool
2017-04-28 20:07:20 +02:00
PtrB * bool
2016-05-01 21:38:51 +02:00
F32 float32
2017-04-28 20:07:20 +02:00
PtrF32 * float32
2016-05-01 21:38:51 +02:00
F64 float64
2017-04-28 20:07:20 +02:00
PtrF64 * float64
2016-05-01 21:38:51 +02:00
S string
2017-04-28 20:07:20 +02:00
PtrS * string
2016-05-01 21:38:51 +02:00
cantSet string
DoesntExist string
2019-06-09 18:39:54 +02:00
GoT time . Time
GoTptr * time . Time
2016-12-15 20:23:14 +02:00
T Timestamp
Tptr * Timestamp
2016-12-23 20:01:42 +02:00
SA StringArray
2016-05-01 21:38:51 +02:00
}
2020-01-08 23:40:52 +02:00
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" `
}
2016-12-23 20:01:42 +02:00
Timestamp time . Time
TA [ ] Timestamp
StringArray [ ] string
2017-01-16 09:13:46 +02:00
Struct struct {
Foo string
}
2021-04-25 03:50:14 +02:00
Bar struct {
Baz int ` json:"baz" query:"baz" `
}
2016-05-01 21:38:51 +02:00
)
2016-12-15 20:23:14 +02:00
func ( t * Timestamp ) UnmarshalParam ( src string ) error {
ts , err := time . Parse ( time . RFC3339 , src )
* t = Timestamp ( ts )
return err
}
2016-12-23 20:01:42 +02:00
func ( a * StringArray ) UnmarshalParam ( src string ) error {
* a = StringArray ( strings . Split ( src , "," ) )
return nil
}
2017-01-16 09:13:46 +02:00
func ( s * Struct ) UnmarshalParam ( src string ) error {
* s = Struct {
Foo : src ,
}
return nil
}
2016-12-15 21:08:56 +02:00
func ( t bindTestStruct ) GetCantSet ( ) string {
2016-05-01 21:38:51 +02:00
return t . cantSet
}
var values = map [ string ] [ ] string {
"I" : { "0" } ,
2017-04-28 20:07:20 +02:00
"PtrI" : { "0" } ,
2016-05-01 21:38:51 +02:00
"I8" : { "8" } ,
2017-04-28 20:07:20 +02:00
"PtrI8" : { "8" } ,
2016-05-01 21:38:51 +02:00
"I16" : { "16" } ,
2017-04-28 20:07:20 +02:00
"PtrI16" : { "16" } ,
2016-05-01 21:38:51 +02:00
"I32" : { "32" } ,
2017-04-28 20:07:20 +02:00
"PtrI32" : { "32" } ,
2016-05-01 21:38:51 +02:00
"I64" : { "64" } ,
2017-04-28 20:07:20 +02:00
"PtrI64" : { "64" } ,
2016-05-01 21:38:51 +02:00
"UI" : { "0" } ,
2017-04-28 20:07:20 +02:00
"PtrUI" : { "0" } ,
2016-05-01 21:38:51 +02:00
"UI8" : { "8" } ,
2017-04-28 20:07:20 +02:00
"PtrUI8" : { "8" } ,
2016-05-01 21:38:51 +02:00
"UI16" : { "16" } ,
2017-04-28 20:07:20 +02:00
"PtrUI16" : { "16" } ,
2016-05-01 21:38:51 +02:00
"UI32" : { "32" } ,
2017-04-28 20:07:20 +02:00
"PtrUI32" : { "32" } ,
2016-05-01 21:38:51 +02:00
"UI64" : { "64" } ,
2017-04-28 20:07:20 +02:00
"PtrUI64" : { "64" } ,
2016-05-01 21:38:51 +02:00
"B" : { "true" } ,
2017-04-28 20:07:20 +02:00
"PtrB" : { "true" } ,
2016-05-01 21:38:51 +02:00
"F32" : { "32.5" } ,
2017-04-28 20:07:20 +02:00
"PtrF32" : { "32.5" } ,
2016-05-01 21:38:51 +02:00
"F64" : { "64.5" } ,
2017-04-28 20:07:20 +02:00
"PtrF64" : { "64.5" } ,
2016-05-01 21:38:51 +02:00
"S" : { "test" } ,
2017-04-28 20:07:20 +02:00
"PtrS" : { "test" } ,
2016-05-01 21:38:51 +02:00
"cantSet" : { "test" } ,
2016-12-15 20:23:14 +02:00
"T" : { "2016-12-06T19:09:05+01:00" } ,
"Tptr" : { "2016-12-06T19:09:05+01:00" } ,
2019-06-09 18:39:54 +02:00
"GoT" : { "2016-12-06T19:09:05+01:00" } ,
"GoTptr" : { "2016-12-06T19:09:05+01:00" } ,
2017-01-16 09:13:46 +02:00
"ST" : { "bar" } ,
2016-05-01 21:38:51 +02:00
}
2021-01-05 12:04:24 +02:00
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
}
}
2016-12-15 21:08:56 +02:00
func TestBindJSON ( t * testing . T ) {
2022-12-04 22:17:48 +02:00
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 { } )
2016-05-01 21:38:51 +02:00
}
2016-12-15 21:08:56 +02:00
func TestBindXML ( t * testing . T ) {
2022-12-04 22:17:48 +02:00
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 { } )
2016-05-01 21:38:51 +02:00
}
2016-12-15 21:08:56 +02:00
func TestBindForm ( t * testing . T ) {
2022-12-04 22:17:48 +02:00
testBindOkay ( t , strings . NewReader ( userForm ) , nil , MIMEApplicationForm )
testBindOkay ( t , strings . NewReader ( userForm ) , dummyQuery , MIMEApplicationForm )
2016-06-23 16:40:14 +02:00
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodPost , "/" , strings . NewReader ( userForm ) )
2016-09-23 07:53:44 +02:00
rec := httptest . NewRecorder ( )
2016-06-23 16:40:14 +02:00
c := e . NewContext ( req , rec )
2016-09-23 07:53:44 +02:00
req . Header . Set ( HeaderContentType , MIMEApplicationForm )
2018-02-21 20:44:17 +02:00
err := c . Bind ( & [ ] struct { Field string } { } )
2022-12-04 22:17:48 +02:00
assert . Error ( t , err )
2016-05-01 21:38:51 +02:00
}
2016-12-15 21:08:56 +02:00
func TestBindQueryParams ( t * testing . T ) {
2016-06-24 04:43:05 +02:00
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodGet , "/?id=1&name=Jon+Snow" , nil )
2016-09-23 07:53:44 +02:00
rec := httptest . NewRecorder ( )
2016-06-24 04:43:05 +02:00
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 )
}
}
2018-05-30 23:50:24 +02:00
func TestBindQueryParamsCaseInsensitive ( t * testing . T ) {
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodGet , "/?ID=1&NAME=Jon+Snow" , nil )
2018-05-30 23:50:24 +02:00
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 )
}
}
2018-05-31 00:09:39 +02:00
func TestBindQueryParamsCaseSensitivePrioritized ( t * testing . T ) {
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodGet , "/?id=1&ID=2&NAME=Jon+Snow&name=Jon+Doe" , nil )
2018-05-31 00:09:39 +02:00
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 )
}
}
2021-05-25 14:50:49 +02:00
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 )
2021-07-15 22:34:01 +02:00
err := BindHeaders ( c , u )
2021-05-25 14:50:49 +02:00
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 )
2021-07-15 22:34:01 +02:00
err := BindHeaders ( c , u )
2021-05-25 14:50:49 +02:00
assert . Error ( t , err )
httpErr , ok := err . ( * HTTPError )
if assert . True ( t , ok ) {
assert . Equal ( t , http . StatusBadRequest , httpErr . Code )
}
}
2021-07-15 22:34:01 +02:00
func TestBind_CombineQueryWithHeaderParam ( t * testing . T ) {
e := New ( )
req := httptest . NewRequest ( http . MethodGet , "/products/999?length=50&page=10&language=et" , nil )
req . Header . Set ( "language" , "de" )
req . Header . Set ( "length" , "99" )
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
c . SetPathParams ( PathParams { {
Name : "id" ,
Value : "999" ,
} } )
type SearchOpts struct {
ID int ` param:"id" `
Length int ` query:"length" `
Page int ` query:"page" `
Search string ` query:"search" `
Language string ` query:"language" header:"language" `
}
opts := SearchOpts {
Length : 100 ,
Page : 0 ,
Search : "default value" ,
Language : "en" ,
}
err := c . Bind ( & opts )
assert . NoError ( t , err )
assert . Equal ( t , 50 , opts . Length ) // bind from query
assert . Equal ( t , 10 , opts . Page ) // bind from query
assert . Equal ( t , 999 , opts . ID ) // bind from path param
assert . Equal ( t , "et" , opts . Language ) // bind from query
assert . Equal ( t , "default value" , opts . Search ) // default value stays
// make sure another bind will not mess already set values unless there are new values
err = BindHeaders ( c , & opts )
assert . NoError ( t , err )
assert . Equal ( t , 50 , opts . Length ) // does not have tag in struct although header exists
assert . Equal ( t , 10 , opts . Page )
assert . Equal ( t , 999 , opts . ID )
assert . Equal ( t , "de" , opts . Language ) // header overwrites now this value
assert . Equal ( t , "default value" , opts . Search )
}
2016-12-15 21:08:56 +02:00
func TestBindUnmarshalParam ( t * testing . T ) {
2016-12-15 20:23:14 +02:00
e := New ( )
2018-10-14 17:16:58 +02:00
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 )
2016-12-15 20:23:14 +02:00
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
2016-12-15 21:08:56 +02:00
result := struct {
2021-01-05 12:04:24 +02:00
T Timestamp ` query:"ts" `
TA [ ] Timestamp ` query:"ta" `
SA StringArray ` query:"sa" `
ST Struct
StWithTag struct {
Foo string ` query:"st" `
}
2016-12-15 21:08:56 +02:00
} { }
2016-12-15 20:23:14 +02:00
err := c . Bind ( & result )
2016-12-23 20:01:42 +02:00
ts := Timestamp ( time . Date ( 2016 , 12 , 6 , 19 , 9 , 5 , 0 , time . UTC ) )
2018-10-14 09:18:44 +02:00
2022-12-04 22:17:48 +02:00
if assert . NoError ( t , err ) {
2018-10-14 09:18:44 +02:00
// assert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)
2022-12-04 22:17:48 +02:00
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
2016-12-15 20:23:14 +02:00
}
}
2019-06-09 18:39:54 +02:00
func TestBindUnmarshalText ( t * testing . T ) {
e := New ( )
2021-07-15 22:34:01 +02:00
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 )
2019-06-09 18:39:54 +02:00
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 )
2021-01-05 12:04:24 +02:00
assert . Equal ( t , Struct { "" } , result . ST ) // field in child struct does not have tag
2019-06-09 18:39:54 +02:00
}
}
2016-12-15 21:08:56 +02:00
func TestBindUnmarshalParamPtr ( t * testing . T ) {
2016-12-15 20:23:14 +02:00
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodGet , "/?ts=2016-12-06T19:09:05Z" , nil )
2016-12-15 20:23:14 +02:00
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
2016-12-15 21:08:56 +02:00
result := struct {
2016-12-15 20:23:14 +02:00
Tptr * Timestamp ` query:"ts" `
2016-12-15 21:08:56 +02:00
} { }
2016-12-15 20:23:14 +02:00
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 )
}
}
2021-04-25 03:50:14 +02:00
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 )
2021-04-29 03:22:01 +02:00
assert . Contains ( t , err . Error ( ) , "query/param/form tags are not allowed with anonymous struct field" )
2021-04-25 03:50:14 +02:00
}
2019-06-09 18:39:54 +02:00
func TestBindUnmarshalTextPtr ( t * testing . T ) {
e := New ( )
2021-07-15 22:34:01 +02:00
req := httptest . NewRequest ( http . MethodGet , "/?ts=2016-12-06T19:09:05Z" , nil )
2019-06-09 18:39:54 +02:00
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 )
}
}
2016-12-15 21:08:56 +02:00
func TestBindMultipartForm ( t * testing . T ) {
2021-04-06 09:05:33 +02:00
bodyBuffer := new ( bytes . Buffer )
mw := multipart . NewWriter ( bodyBuffer )
2016-05-01 21:38:51 +02:00
mw . WriteField ( "id" , "1" )
mw . WriteField ( "name" , "Jon Snow" )
mw . Close ( )
2021-04-06 09:05:33 +02:00
body := bodyBuffer . Bytes ( )
2018-10-14 09:18:44 +02:00
2022-12-04 22:17:48 +02:00
testBindOkay ( t , bytes . NewReader ( body ) , nil , mw . FormDataContentType ( ) )
testBindOkay ( t , bytes . NewReader ( body ) , dummyQuery , mw . FormDataContentType ( ) )
2016-05-01 21:38:51 +02:00
}
2016-12-15 21:08:56 +02:00
func TestBindUnsupportedMediaType ( t * testing . T ) {
2022-12-04 22:17:48 +02:00
testBindError ( t , strings . NewReader ( invalidContent ) , MIMEApplicationJSON , & json . SyntaxError { } )
2016-05-01 21:38:51 +02:00
}
2016-12-15 21:08:56 +02:00
func TestBindbindData ( t * testing . T ) {
ts := new ( bindTestStruct )
2021-07-15 22:34:01 +02:00
err := bindData ( ts , values , "form" )
2022-12-04 22:17:48 +02:00
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 )
2016-05-01 21:38:51 +02:00
}
2019-06-21 15:12:55 +02:00
func TestBindParam ( t * testing . T ) {
e := New ( )
2021-07-15 22:34:01 +02:00
req := httptest . NewRequest ( http . MethodGet , "/" , nil )
2019-06-21 15:12:55 +02:00
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
2021-07-15 22:34:01 +02:00
cc := c . ( RoutableContext )
cc . SetRouteInfo ( routeInfo { path : "/users/:id/:name" } )
cc . SetRawPathParams ( & PathParams {
{ Name : "id" , Value : "1" } ,
{ Name : "name" , Value : "Jon Snow" } ,
} )
2019-06-21 15:12:55 +02:00
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 )
2021-07-15 22:34:01 +02:00
cc2 := c2 . ( RoutableContext )
cc2 . SetRouteInfo ( routeInfo { path : "/users/:id" } )
cc2 . SetRawPathParams ( & PathParams {
{ Name : "id" , Value : "1" } ,
} )
2019-06-21 15:12:55 +02:00
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 ( )
2021-07-15 22:34:01 +02:00
req2 := httptest . NewRequest ( http . MethodPost , "/" , body )
2019-06-21 15:12:55 +02:00
req2 . Header . Set ( HeaderContentType , MIMEApplicationJSON )
rec2 := httptest . NewRecorder ( )
c3 := e2 . NewContext ( req2 , rec2 )
2021-07-15 22:34:01 +02:00
cc3 := c3 . ( RoutableContext )
cc3 . SetRouteInfo ( routeInfo { path : "/users/:id" } )
cc3 . SetRawPathParams ( & PathParams {
{ Name : "id" , Value : "1" } ,
} )
2019-06-21 15:12:55 +02:00
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 )
}
}
2018-05-01 11:18:55 +02:00
func TestBindUnmarshalTypeError ( t * testing . T ) {
body := bytes . NewBufferString ( ` { "id": "text" } ` )
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodPost , "/" , body )
2018-05-01 11:18:55 +02:00
req . Header . Set ( HeaderContentType , MIMEApplicationJSON )
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
u := new ( user )
err := c . Bind ( u )
2018-09-28 19:41:13 +02:00
he := & HTTPError { Code : http . StatusBadRequest , Message : "Unmarshal type error: expected=int, got=string, field=id, offset=14" , Internal : err . ( * HTTPError ) . Internal }
2018-05-01 11:18:55 +02:00
assert . Equal ( t , he , err )
}
2016-12-15 21:08:56 +02:00
func TestBindSetWithProperType ( t * testing . T ) {
ts := new ( bindTestStruct )
2016-05-01 21:38:51 +02:00
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 )
2022-12-04 22:17:48 +02:00
assert . NoError ( t , err )
2016-05-01 21:38:51 +02:00
}
2022-12-04 22:17:48 +02:00
assertBindTestStruct ( t , ts )
2016-05-01 21:38:51 +02:00
type foo struct {
Bar bytes . Buffer
}
v := & foo { }
typ = reflect . TypeOf ( v ) . Elem ( )
val = reflect . ValueOf ( v ) . Elem ( )
2022-12-04 22:17:48 +02:00
assert . Error ( t , setWithProperType ( typ . Field ( 0 ) . Type . Kind ( ) , "5" , val . Field ( 0 ) ) )
2016-05-01 21:38:51 +02:00
}
2021-07-15 22:34:01 +02:00
func TestSetIntField ( t * testing . T ) {
ts := new ( bindTestStruct )
ts . I = 100
val := reflect . ValueOf ( ts ) . Elem ( )
// empty value does nothing to field
// in that way we can have default values by setting field value before binding
err := setIntField ( "" , 0 , val . FieldByName ( "I" ) )
assert . NoError ( t , err )
assert . Equal ( t , 100 , ts . I )
// second set with value sets the value
err = setIntField ( "5" , 0 , val . FieldByName ( "I" ) )
assert . NoError ( t , err )
assert . Equal ( t , 5 , ts . I )
// third set without value does nothing to the value
// in that way multiple binds (ala query + header) do not reset fields to 0s
err = setIntField ( "" , 0 , val . FieldByName ( "I" ) )
assert . NoError ( t , err )
assert . Equal ( t , 5 , ts . I )
}
2018-10-14 09:18:44 +02:00
2021-07-15 22:34:01 +02:00
func TestSetUintField ( t * testing . T ) {
2016-12-15 21:08:56 +02:00
ts := new ( bindTestStruct )
2021-07-15 22:34:01 +02:00
ts . UI = 100
2016-05-01 21:38:51 +02:00
val := reflect . ValueOf ( ts ) . Elem ( )
2021-07-15 22:34:01 +02:00
// empty value does nothing to field
// in that way we can have default values by setting field value before binding
err := setUintField ( "" , 0 , val . FieldByName ( "UI" ) )
assert . NoError ( t , err )
assert . Equal ( t , uint ( 100 ) , ts . UI )
// second set with value sets the value
err = setUintField ( "5" , 0 , val . FieldByName ( "UI" ) )
assert . NoError ( t , err )
assert . Equal ( t , uint ( 5 ) , ts . UI )
// third set without value does nothing to the value
// in that way multiple binds (ala query + header) do not reset fields to 0s
err = setUintField ( "" , 0 , val . FieldByName ( "UI" ) )
assert . NoError ( t , err )
assert . Equal ( t , uint ( 5 ) , ts . UI )
}
2016-05-01 21:38:51 +02:00
2021-07-15 22:34:01 +02:00
func TestSetFloatField ( t * testing . T ) {
ts := new ( bindTestStruct )
ts . F32 = 100
2016-05-01 21:38:51 +02:00
2021-07-15 22:34:01 +02:00
val := reflect . ValueOf ( ts ) . Elem ( )
// empty value does nothing to field
// in that way we can have default values by setting field value before binding
err := setFloatField ( "" , 0 , val . FieldByName ( "F32" ) )
assert . NoError ( t , err )
assert . Equal ( t , float32 ( 100 ) , ts . F32 )
// second set with value sets the value
err = setFloatField ( "15.5" , 0 , val . FieldByName ( "F32" ) )
assert . NoError ( t , err )
assert . Equal ( t , float32 ( 15.5 ) , ts . F32 )
// third set without value does nothing to the value
// in that way multiple binds (ala query + header) do not reset fields to 0s
err = setFloatField ( "" , 0 , val . FieldByName ( "F32" ) )
assert . NoError ( t , err )
assert . Equal ( t , float32 ( 15.5 ) , ts . F32 )
}
func TestSetBoolField ( t * testing . T ) {
ts := new ( bindTestStruct )
ts . B = true
val := reflect . ValueOf ( ts ) . Elem ( )
// empty value does nothing to field
// in that way we can have default values by setting field value before binding
err := setBoolField ( "" , val . FieldByName ( "B" ) )
assert . NoError ( t , err )
assert . Equal ( t , true , ts . B )
// second set with value sets the value
err = setBoolField ( "true" , val . FieldByName ( "B" ) )
assert . NoError ( t , err )
assert . Equal ( t , true , ts . B )
// third set without value does nothing to the value
// in that way multiple binds (ala query + header) do not reset fields to 0s
err = setBoolField ( "" , val . FieldByName ( "B" ) )
assert . NoError ( t , err )
assert . Equal ( t , true , ts . B )
// fourth set to false
err = setBoolField ( "false" , val . FieldByName ( "B" ) )
assert . NoError ( t , err )
assert . Equal ( t , false , ts . B )
}
func TestUnmarshalFieldNonPtr ( t * testing . T ) {
ts := new ( bindTestStruct )
val := reflect . ValueOf ( ts ) . Elem ( )
2016-12-15 20:23:14 +02:00
2016-12-23 20:01:42 +02:00
ok , err := unmarshalFieldNonPtr ( "2016-12-06T19:09:05Z" , val . FieldByName ( "T" ) )
2021-07-15 22:34:01 +02:00
if assert . NoError ( t , err ) {
assert . True ( t , ok )
assert . Equal ( t , Timestamp ( time . Date ( 2016 , 12 , 6 , 19 , 9 , 5 , 0 , time . UTC ) ) , ts . T )
2016-12-15 20:23:14 +02:00
}
2016-05-01 21:38:51 +02:00
}
2020-01-08 23:40:52 +02:00
func BenchmarkBindbindDataWithTags ( b * testing . B ) {
b . ReportAllocs ( )
assert := assert . New ( b )
ts := new ( bindTestStructWithTags )
var err error
b . ResetTimer ( )
for i := 0 ; i < b . N ; i ++ {
2021-07-15 22:34:01 +02:00
err = bindData ( ts , values , "form" )
2020-01-08 23:40:52 +02:00
}
assert . NoError ( err )
2022-12-04 22:17:48 +02:00
assertBindTestStruct ( b , ( * bindTestStruct ) ( ts ) )
}
func assertBindTestStruct ( t testing . TB , ts * bindTestStruct ) {
assert . Equal ( t , 0 , ts . I )
assert . Equal ( t , int8 ( 8 ) , ts . I8 )
assert . Equal ( t , int16 ( 16 ) , ts . I16 )
assert . Equal ( t , int32 ( 32 ) , ts . I32 )
assert . Equal ( t , int64 ( 64 ) , ts . I64 )
assert . Equal ( t , uint ( 0 ) , ts . UI )
assert . Equal ( t , uint8 ( 8 ) , ts . UI8 )
assert . Equal ( t , uint16 ( 16 ) , ts . UI16 )
assert . Equal ( t , uint32 ( 32 ) , ts . UI32 )
assert . Equal ( t , uint64 ( 64 ) , ts . UI64 )
assert . Equal ( t , true , ts . B )
assert . Equal ( t , float32 ( 32.5 ) , ts . F32 )
assert . Equal ( t , float64 ( 64.5 ) , ts . F64 )
assert . Equal ( t , "test" , ts . S )
assert . Equal ( t , "" , ts . GetCantSet ( ) )
}
func testBindOkay ( t testing . TB , r io . Reader , query url . Values , ctype string ) {
2016-05-01 21:38:51 +02:00
e := New ( )
2021-04-06 09:05:33 +02:00
path := "/"
if len ( query ) > 0 {
path += "?" + query . Encode ( )
}
req := httptest . NewRequest ( http . MethodPost , path , r )
2016-09-23 07:53:44 +02:00
rec := httptest . NewRecorder ( )
2016-05-01 21:38:51 +02:00
c := e . NewContext ( req , rec )
2016-09-23 07:53:44 +02:00
req . Header . Set ( HeaderContentType , ctype )
2016-05-01 21:38:51 +02:00
u := new ( user )
err := c . Bind ( u )
2022-12-04 22:17:48 +02:00
if assert . NoError ( t , err ) {
assert . Equal ( t , 1 , u . ID )
assert . Equal ( t , "Jon Snow" , u . Name )
2016-05-01 21:38:51 +02:00
}
}
2022-12-04 22:17:48 +02:00
func testBindArrayOkay ( t * testing . T , r io . Reader , query url . Values , ctype string ) {
2021-04-06 09:05:33 +02:00
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 )
2022-12-04 22:17:48 +02:00
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 )
2021-04-06 09:05:33 +02:00
}
}
2022-12-04 22:17:48 +02:00
func testBindError ( t * testing . T , r io . Reader , ctype string , expectedInternal error ) {
2016-05-01 21:38:51 +02:00
e := New ( )
2018-10-14 17:16:58 +02:00
req := httptest . NewRequest ( http . MethodPost , "/" , r )
2016-09-23 07:53:44 +02:00
rec := httptest . NewRecorder ( )
2016-05-01 21:38:51 +02:00
c := e . NewContext ( req , rec )
2016-09-23 07:53:44 +02:00
req . Header . Set ( HeaderContentType , ctype )
2016-05-01 21:38:51 +02:00
u := new ( user )
err := c . Bind ( u )
switch {
2017-02-28 22:04:29 +02:00
case strings . HasPrefix ( ctype , MIMEApplicationJSON ) , strings . HasPrefix ( ctype , MIMEApplicationXML ) , strings . HasPrefix ( ctype , MIMETextXML ) ,
2016-05-01 21:38:51 +02:00
strings . HasPrefix ( ctype , MIMEApplicationForm ) , strings . HasPrefix ( ctype , MIMEMultipartForm ) :
2022-12-04 22:17:48 +02:00
if assert . IsType ( t , new ( HTTPError ) , err ) {
assert . Equal ( t , http . StatusBadRequest , err . ( * HTTPError ) . Code )
assert . IsType ( t , expectedInternal , err . ( * HTTPError ) . Internal )
2016-05-01 21:38:51 +02:00
}
default :
2022-12-04 22:17:48 +02:00
if assert . IsType ( t , new ( HTTPError ) , err ) {
assert . Equal ( t , ErrUnsupportedMediaType , err )
assert . IsType ( t , expectedInternal , err . ( * HTTPError ) . Internal )
2016-05-01 21:38:51 +02:00
}
}
}
2020-11-12 12:28:45 +02:00
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
2020-12-20 11:05:42 +02:00
type Opts struct {
2021-01-05 12:04:24 +02:00
ID int ` json:"id" form:"id" query:"id" `
Node string ` json:"node" form:"node" query:"node" param:"node" `
Lang string
2020-11-12 12:28:45 +02:00
}
var testCases = [ ] struct {
name string
givenURL string
givenContent io . Reader
givenMethod string
whenBindTarget interface { }
whenNoPathParams bool
expect interface { }
expectError string
} {
{
2020-12-20 11:05:42 +02:00
name : "ok, POST bind to struct with: path param + query param + body" ,
2020-11-12 12:28:45 +02:00
givenMethod : http . MethodPost ,
givenURL : "/api/real_node/endpoint?node=xxx" ,
givenContent : strings . NewReader ( ` { "id": 1} ` ) ,
2020-12-20 11:05:42 +02:00
expect : & Opts { ID : 1 , Node : "node_from_path" } , // query params are not used, node is filled from path
2020-11-12 12:28:45 +02:00
} ,
{
2020-12-20 11:05:42 +02:00
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" ,
2020-11-12 12:28:45 +02:00
givenMethod : http . MethodPost ,
givenURL : "/api/real_node/endpoint" ,
givenContent : strings . NewReader ( ` { "id": 1} ` ) ,
2020-12-20 11:05:42 +02:00
expect : & Opts { ID : 1 , Node : "node_from_path" } ,
2020-11-12 12:28:45 +02:00
} ,
{
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"} ` ) ,
2020-12-20 11:05:42 +02:00
expect : & Opts { ID : 1 , Node : "zzz" } , // field value from content has higher priority
2020-11-12 12:28:45 +02:00
} ,
{
name : "nok, POST body bind failure" ,
givenMethod : http . MethodPost ,
givenURL : "/api/real_node/endpoint?node=xxx" ,
givenContent : strings . NewReader ( ` { ` ) ,
2020-12-20 11:05:42 +02:00
expect : & Opts { ID : 0 , Node : "node_from_path" } , // query binding has already modified bind target
2020-11-12 12:28:45 +02:00
expectError : "code=400, message=unexpected EOF, internal=unexpected EOF" ,
} ,
2020-12-20 11:05:42 +02:00
{
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" ,
} ,
2020-11-12 12:28:45 +02:00
{
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}] ` ) ,
2020-12-20 11:05:42 +02:00
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" ,
2020-11-12 12:28:45 +02:00
} ,
2021-04-06 09:05:33 +02:00
{ // 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" ,
2020-11-12 12:28:45 +02:00
givenMethod : http . MethodGet ,
givenURL : "/api/real_node/endpoint?node=xxx" ,
givenContent : strings . NewReader ( ` [ { "id": 1}] ` ) ,
whenNoPathParams : true ,
2020-12-20 11:05:42 +02:00
whenBindTarget : & [ ] Opts { } ,
2021-04-06 09:05:33 +02:00
expect : & [ ] Opts {
{ ID : 1 , Node : "" } ,
} ,
2020-11-12 12:28:45 +02:00
} ,
2020-12-20 11:05:42 +02:00
{ // 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 : "" ,
} ,
2021-04-06 09:05:33 +02:00
{ // 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" ,
2020-11-12 12:28:45 +02:00
givenMethod : http . MethodGet ,
givenURL : "/api/real_node/endpoint?node=xxx" ,
givenContent : strings . NewReader ( ` [ { "id": 1}] ` ) ,
2020-12-20 11:05:42 +02:00
whenBindTarget : & [ ] Opts { } ,
2021-04-06 09:05:33 +02:00
expect : & [ ] Opts {
{ ID : 1 , Node : "" } ,
} ,
2020-11-12 12:28:45 +02:00
} ,
{
name : "ok, GET body bind json array to slice" ,
givenMethod : http . MethodGet ,
givenURL : "/api/real_node/endpoint" ,
givenContent : strings . NewReader ( ` [ { "id": 1}] ` ) ,
whenNoPathParams : true ,
2020-12-20 11:05:42 +02:00
whenBindTarget : & [ ] Opts { } ,
expect : & [ ] Opts { { ID : 1 , Node : "" } } ,
2020-11-12 12:28:45 +02:00
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 {
2021-07-15 22:34:01 +02:00
cc := c . ( RoutableContext )
cc . SetRawPathParams ( & PathParams {
{ Name : "node" , Value : "node_from_path" } ,
} )
2020-11-12 12:28:45 +02:00
}
var bindTarget interface { }
if tc . whenBindTarget != nil {
bindTarget = tc . whenBindTarget
} else {
2020-12-20 11:05:42 +02:00
bindTarget = & Opts { }
2020-11-12 12:28:45 +02:00
}
b := new ( DefaultBinder )
2021-07-15 22:34:01 +02:00
err := b . Bind ( c , bindTarget )
2020-11-12 12:28:45 +02:00
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 {
2021-01-05 12:04:24 +02:00
ID int ` json:"id" xml:"id" form:"id" query:"id" `
Node string ` json:"node" xml:"node" form:"node" query:"node" param:"node" `
2020-11-12 12:28:45 +02:00
}
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
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" ,
} ,
{
2021-01-05 12:04:24 +02:00
name : "ok, FORM POST bind to struct with: path + query + body" ,
2020-11-12 12:28:45 +02:00
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" ,
} ,
}
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 )
}
rec := httptest . NewRecorder ( )
c := e . NewContext ( req , rec )
if ! tc . whenNoPathParams {
2021-07-15 22:34:01 +02:00
cc := c . ( RoutableContext )
cc . SetRawPathParams ( & PathParams {
{ Name : "node" , Value : "real_node" } ,
} )
2020-11-12 12:28:45 +02:00
}
var bindTarget interface { }
if tc . whenBindTarget != nil {
bindTarget = tc . whenBindTarget
} else {
bindTarget = & Node { }
}
2021-07-15 22:34:01 +02:00
err := BindBody ( c , bindTarget )
2020-11-12 12:28:45 +02:00
if tc . expectError != "" {
assert . EqualError ( t , err , tc . expectError )
} else {
assert . NoError ( t , err )
}
assert . Equal ( t , tc . expect , bindTarget )
} )
}
}