// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package binding import ( "bytes" "encoding/json" "errors" "io" "mime/multipart" "net/http" "os" "reflect" "strconv" "strings" "testing" "time" "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) type appkey struct { Appkey string `json:"appkey" form:"appkey"` } type QueryTest struct { Page int `json:"page" form:"page"` Size int `json:"size" form:"size"` appkey } type FooStruct struct { Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"` } type FooBarStruct struct { FooStruct Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` } type FooBarFileStruct struct { FooBarStruct File *multipart.FileHeader `form:"file" binding:"required"` } type FooBarFileFailStruct struct { FooBarStruct File *multipart.FileHeader `invalid_name:"file" binding:"required"` // for unexport test data *multipart.FileHeader `form:"data" binding:"required"` } type FooDefaultBarStruct struct { FooStruct Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"` } type FooStructUseNumber struct { Foo any `json:"foo" binding:"required"` } type FooStructDisallowUnknownFields struct { Foo any `json:"foo" binding:"required"` } type FooBarStructForTimeType struct { TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` CreateTime time.Time `form:"createTime" time_format:"unixNano"` UnixTime time.Time `form:"unixTime" time_format:"unix"` } type FooStructForTimeTypeNotUnixFormat struct { CreateTime time.Time `form:"createTime" time_format:"unixNano"` UnixTime time.Time `form:"unixTime" time_format:"unix"` } type FooStructForTimeTypeNotFormat struct { TimeFoo time.Time `form:"time_foo"` } type FooStructForTimeTypeFailFormat struct { TimeFoo time.Time `form:"time_foo" time_format:"2017-11-15"` } type FooStructForTimeTypeFailLocation struct { TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_location:"/asia/chongqing"` } type FooStructForMapType struct { MapFoo map[string]any `form:"map_foo"` } type FooStructForIgnoreFormTag struct { Foo *string `form:"-"` } type InvalidNameType struct { TestName string `invalid_name:"test_name"` } type InvalidNameMapType struct { TestName struct { MapFoo map[string]any `form:"map_foo"` } } type FooStructForSliceType struct { SliceFoo []int `form:"slice_foo"` } type FooStructForStructType struct { StructFoo struct { Idx int `form:"idx"` } } type FooStructForStructPointerType struct { StructPointerFoo *struct { Name string `form:"name"` } } type FooStructForSliceMapType struct { // Unknown type: not support map SliceMapFoo []map[string]any `form:"slice_map_foo"` } type FooStructForBoolType struct { BoolFoo bool `form:"bool_foo"` } type FooStructForStringPtrType struct { PtrFoo *string `form:"ptr_foo"` PtrBar *string `form:"ptr_bar" binding:"required"` } type FooStructForMapPtrType struct { PtrBar *map[string]any `form:"ptr_bar"` } func TestBindingDefault(t *testing.T) { assert.Equal(t, Form, Default(http.MethodGet, "")) assert.Equal(t, Form, Default(http.MethodGet, MIMEJSON)) assert.Equal(t, JSON, Default(http.MethodPost, MIMEJSON)) assert.Equal(t, JSON, Default(http.MethodPut, MIMEJSON)) assert.Equal(t, XML, Default(http.MethodPost, MIMEXML)) assert.Equal(t, XML, Default(http.MethodPut, MIMEXML2)) assert.Equal(t, Form, Default(http.MethodPost, MIMEPOSTForm)) assert.Equal(t, Form, Default(http.MethodPut, MIMEPOSTForm)) assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm)) assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm)) assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF)) assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML)) assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML)) assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML2)) assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML2)) assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) } func TestBindingJSONNilBody(t *testing.T) { var obj FooStruct req, _ := http.NewRequest(http.MethodPost, "/", nil) err := JSON.Bind(req, &obj) require.Error(t, err) } func TestBindingJSON(t *testing.T) { testBodyBinding(t, JSON, "json", "/", "/", `{"foo": "bar"}`, `{"bar": "foo"}`) } func TestBindingJSONSlice(t *testing.T) { EnableDecoderDisallowUnknownFields = true defer func() { EnableDecoderDisallowUnknownFields = false }() testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``) testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`) testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`) testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`) testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`) testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`) } func TestBindingJSONUseNumber(t *testing.T) { testBodyBindingUseNumber(t, JSON, "json", "/", "/", `{"foo": 123}`, `{"bar": "foo"}`) } func TestBindingJSONUseNumber2(t *testing.T) { testBodyBindingUseNumber2(t, JSON, "json", "/", "/", `{"foo": 123}`, `{"bar": "foo"}`) } func TestBindingJSONDisallowUnknownFields(t *testing.T) { testBodyBindingDisallowUnknownFields(t, JSON, "/", "/", `{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`) } func TestBindingJSONStringMap(t *testing.T) { testBodyBindingStringMap(t, JSON, "/", "/", `{"foo": "bar", "hello": "world"}`, `{"num": 2}`) } func TestBindingForm(t *testing.T) { testFormBinding(t, http.MethodPost, "/", "/", "foo=bar&bar=foo", "bar2=foo") } func TestBindingForm2(t *testing.T) { testFormBinding(t, http.MethodGet, "/?foo=bar&bar=foo", "/?bar2=foo", "", "") } func TestBindingFormEmbeddedStruct(t *testing.T) { testFormBindingEmbeddedStruct(t, http.MethodPost, "/", "/", "page=1&size=2&appkey=test-appkey", "bar2=foo") } func TestBindingFormEmbeddedStruct2(t *testing.T) { testFormBindingEmbeddedStruct(t, http.MethodGet, "/?page=1&size=2&appkey=test-appkey", "/?bar2=foo", "", "") } func TestBindingFormDefaultValue(t *testing.T) { testFormBindingDefaultValue(t, http.MethodPost, "/", "/", "foo=bar", "bar2=foo") } func TestBindingFormDefaultValue2(t *testing.T) { testFormBindingDefaultValue(t, http.MethodGet, "/?foo=bar", "/?bar2=foo", "", "") } func TestBindingFormForTime(t *testing.T) { testFormBindingForTime(t, http.MethodPost, "/", "/", "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo") testFormBindingForTimeNotUnixFormat(t, http.MethodPost, "/", "/", "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, http.MethodPost, "/", "/", "time_foo=2017-11-15", "bar2=foo") testFormBindingForTimeFailFormat(t, http.MethodPost, "/", "/", "time_foo=2017-11-15", "bar2=foo") testFormBindingForTimeFailLocation(t, http.MethodPost, "/", "/", "time_foo=2017-11-15", "bar2=foo") } func TestBindingFormForTime2(t *testing.T) { testFormBindingForTime(t, http.MethodGet, "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo", "", "") testFormBindingForTimeNotUnixFormat(t, http.MethodPost, "/", "/", "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, http.MethodGet, "/?time_foo=2017-11-15", "/?bar2=foo", "", "") testFormBindingForTimeFailFormat(t, http.MethodGet, "/?time_foo=2017-11-15", "/?bar2=foo", "", "") testFormBindingForTimeFailLocation(t, http.MethodGet, "/?time_foo=2017-11-15", "/?bar2=foo", "", "") } func TestFormBindingIgnoreField(t *testing.T) { testFormBindingIgnoreField(t, http.MethodPost, "/", "/", "-=bar", "") } func TestBindingFormInvalidName(t *testing.T) { testFormBindingInvalidName(t, http.MethodPost, "/", "/", "test_name=bar", "bar2=foo") } func TestBindingFormInvalidName2(t *testing.T) { testFormBindingInvalidName2(t, http.MethodPost, "/", "/", "map_foo=bar", "bar2=foo") } func TestBindingFormForType(t *testing.T) { testFormBindingForType(t, http.MethodPost, "/", "/", "map_foo={\"bar\":123}", "map_foo=1", "Map") testFormBindingForType(t, http.MethodPost, "/", "/", "slice_foo=1&slice_foo=2", "bar2=1&bar2=2", "Slice") testFormBindingForType(t, http.MethodGet, "/?slice_foo=1&slice_foo=2", "/?bar2=1&bar2=2", "", "", "Slice") testFormBindingForType(t, http.MethodPost, "/", "/", "slice_map_foo=1&slice_map_foo=2", "bar2=1&bar2=2", "SliceMap") testFormBindingForType(t, http.MethodGet, "/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2", "", "", "SliceMap") testFormBindingForType(t, http.MethodPost, "/", "/", "ptr_bar=test", "bar2=test", "Ptr") testFormBindingForType(t, http.MethodGet, "/?ptr_bar=test", "/?bar2=test", "", "", "Ptr") testFormBindingForType(t, http.MethodPost, "/", "/", "idx=123", "id1=1", "Struct") testFormBindingForType(t, http.MethodGet, "/?idx=123", "/?id1=1", "", "", "Struct") testFormBindingForType(t, http.MethodPost, "/", "/", "name=thinkerou", "name1=ou", "StructPointer") testFormBindingForType(t, http.MethodGet, "/?name=thinkerou", "/?name1=ou", "", "", "StructPointer") } func TestBindingFormStringMap(t *testing.T) { testBodyBindingStringMap(t, Form, "/", "", `foo=bar&hello=world`, "") // Should pick the last value testBodyBindingStringMap(t, Form, "/", "", `foo=something&foo=bar&hello=world`, "") } func TestBindingFormStringSliceMap(t *testing.T) { obj := make(map[string][]string) req := requestWithBody(http.MethodPost, "/", "foo=something&foo=bar&hello=world") req.Header.Add("Content-Type", MIMEPOSTForm) err := Form.Bind(req, &obj) require.NoError(t, err) assert.NotNil(t, obj) assert.Len(t, obj, 2) target := map[string][]string{ "foo": {"something", "bar"}, "hello": {"world"}, } assert.True(t, reflect.DeepEqual(obj, target)) objInvalid := make(map[string][]int) req = requestWithBody(http.MethodPost, "/", "foo=something&foo=bar&hello=world") req.Header.Add("Content-Type", MIMEPOSTForm) err = Form.Bind(req, &objInvalid) require.Error(t, err) } func TestBindingQuery(t *testing.T) { testQueryBinding(t, http.MethodPost, "/?foo=bar&bar=foo", "/", "foo=unused", "bar2=foo") } func TestBindingQuery2(t *testing.T) { testQueryBinding(t, http.MethodGet, "/?foo=bar&bar=foo", "/?bar2=foo", "foo=unused", "") } func TestBindingQueryFail(t *testing.T) { testQueryBindingFail(t, http.MethodPost, "/?map_foo=", "/", "map_foo=unused", "bar2=foo") } func TestBindingQueryFail2(t *testing.T) { testQueryBindingFail(t, http.MethodGet, "/?map_foo=", "/?bar2=foo", "map_foo=unused", "") } func TestBindingQueryBoolFail(t *testing.T) { testQueryBindingBoolFail(t, http.MethodGet, "/?bool_foo=fasl", "/?bar2=foo", "bool_foo=unused", "") } func TestBindingQueryStringMap(t *testing.T) { b := Query obj := make(map[string]string) req := requestWithBody(http.MethodGet, "/?foo=bar&hello=world", "") err := b.Bind(req, &obj) require.NoError(t, err) assert.NotNil(t, obj) assert.Len(t, obj, 2) assert.Equal(t, "bar", obj["foo"]) assert.Equal(t, "world", obj["hello"]) obj = make(map[string]string) req = requestWithBody(http.MethodGet, "/?foo=bar&foo=2&hello=world", "") // should pick last err = b.Bind(req, &obj) require.NoError(t, err) assert.NotNil(t, obj) assert.Len(t, obj, 2) assert.Equal(t, "2", obj["foo"]) assert.Equal(t, "world", obj["hello"]) } func TestBindingXML(t *testing.T) { testBodyBinding(t, XML, "xml", "/", "/", "bar", "foo") } func TestBindingXMLFail(t *testing.T) { testBodyBindingFail(t, XML, "xml", "/", "/", "bar", "foo") } func TestBindingTOML(t *testing.T) { testBodyBinding(t, TOML, "toml", "/", "/", `foo="bar"`, `bar="foo"`) } func TestBindingTOMLFail(t *testing.T) { testBodyBindingFail(t, TOML, "toml", "/", "/", `foo=\n"bar"`, `bar="foo"`) } func TestBindingYAML(t *testing.T) { testBodyBinding(t, YAML, "yaml", "/", "/", `foo: bar`, `bar: foo`) } func TestBindingYAMLStringMap(t *testing.T) { // YAML is a superset of JSON, so the test below is JSON (to avoid newlines) testBodyBindingStringMap(t, YAML, "/", "/", `{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`) } func TestBindingYAMLFail(t *testing.T) { testBodyBindingFail(t, YAML, "yaml", "/", "/", `foo:\nbar`, `bar: foo`) } func createFormPostRequest(t *testing.T) *http.Request { req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) require.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } func createDefaultFormPostRequest(t *testing.T) *http.Request { req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar")) require.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } func createFormPostRequestForMap(t *testing.T) *http.Request { req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}")) require.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } func createFormPostRequestForMapFail(t *testing.T) *http.Request { req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello")) require.NoError(t, err) req.Header.Set("Content-Type", MIMEPOSTForm) return req } func createFormFilesMultipartRequest(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() require.NoError(t, mw.SetBoundary(boundary)) require.NoError(t, mw.WriteField("foo", "bar")) require.NoError(t, mw.WriteField("bar", "foo")) f, err := os.Open("form.go") require.NoError(t, err) defer f.Close() fw, err1 := mw.CreateFormFile("file", "form.go") require.NoError(t, err1) _, err = io.Copy(fw, f) require.NoError(t, err) req, err2 := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body) require.NoError(t, err2) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func createFormFilesMultipartRequestFail(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() require.NoError(t, mw.SetBoundary(boundary)) require.NoError(t, mw.WriteField("foo", "bar")) require.NoError(t, mw.WriteField("bar", "foo")) f, err := os.Open("form.go") require.NoError(t, err) defer f.Close() fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go") require.NoError(t, err1) _, err = io.Copy(fw, f) require.NoError(t, err) req, err2 := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body) require.NoError(t, err2) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func createFormMultipartRequest(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() require.NoError(t, mw.SetBoundary(boundary)) require.NoError(t, mw.WriteField("foo", "bar")) require.NoError(t, mw.WriteField("bar", "foo")) req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body) require.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func createFormMultipartRequestForMap(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() require.NoError(t, mw.SetBoundary(boundary)) require.NoError(t, mw.WriteField("map_foo", "{\"bar\":123, \"name\":\"thinkerou\", \"pai\": 3.14}")) req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", body) require.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func createFormMultipartRequestForMapFail(t *testing.T) *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) mw := multipart.NewWriter(body) defer mw.Close() require.NoError(t, mw.SetBoundary(boundary)) require.NoError(t, mw.WriteField("map_foo", "3.14")) req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", body) require.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) return req } func TestBindingFormPost(t *testing.T) { req := createFormPostRequest(t) var obj FooBarStruct require.NoError(t, FormPost.Bind(req, &obj)) assert.Equal(t, "form-urlencoded", FormPost.Name()) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "foo", obj.Bar) } func TestBindingDefaultValueFormPost(t *testing.T) { req := createDefaultFormPostRequest(t) var obj FooDefaultBarStruct require.NoError(t, FormPost.Bind(req, &obj)) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "hello", obj.Bar) } func TestBindingFormPostForMap(t *testing.T) { req := createFormPostRequestForMap(t) var obj FooStructForMapType err := FormPost.Bind(req, &obj) require.NoError(t, err) assert.InDelta(t, float64(123), obj.MapFoo["bar"].(float64), 0.01) } func TestBindingFormPostForMapFail(t *testing.T) { req := createFormPostRequestForMapFail(t) var obj FooStructForMapType err := FormPost.Bind(req, &obj) require.Error(t, err) } func TestBindingFormFilesMultipart(t *testing.T) { req := createFormFilesMultipartRequest(t) var obj FooBarFileStruct err := FormMultipart.Bind(req, &obj) require.NoError(t, err) // file from os f, _ := os.Open("form.go") defer f.Close() fileActual, _ := io.ReadAll(f) // file from multipart mf, _ := obj.File.Open() defer mf.Close() fileExpect, _ := io.ReadAll(mf) assert.Equal(t, "multipart/form-data", FormMultipart.Name()) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "foo", obj.Bar) assert.Equal(t, fileExpect, fileActual) } func TestBindingFormFilesMultipartFail(t *testing.T) { req := createFormFilesMultipartRequestFail(t) var obj FooBarFileFailStruct err := FormMultipart.Bind(req, &obj) require.Error(t, err) } func TestBindingFormMultipart(t *testing.T) { req := createFormMultipartRequest(t) var obj FooBarStruct require.NoError(t, FormMultipart.Bind(req, &obj)) assert.Equal(t, "multipart/form-data", FormMultipart.Name()) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "foo", obj.Bar) } func TestBindingFormMultipartForMap(t *testing.T) { req := createFormMultipartRequestForMap(t) var obj FooStructForMapType err := FormMultipart.Bind(req, &obj) require.NoError(t, err) assert.InDelta(t, float64(123), obj.MapFoo["bar"].(float64), 0.01) assert.Equal(t, "thinkerou", obj.MapFoo["name"].(string)) assert.InDelta(t, float64(3.14), obj.MapFoo["pai"].(float64), 0.01) } func TestBindingFormMultipartForMapFail(t *testing.T) { req := createFormMultipartRequestForMapFail(t) var obj FooStructForMapType err := FormMultipart.Bind(req, &obj) require.Error(t, err) } func TestBindingProtoBuf(t *testing.T) { test := &protoexample.Test{ Label: proto.String("yes"), } data, _ := proto.Marshal(test) testProtoBodyBinding(t, ProtoBuf, "protobuf", "/", "/", string(data), string(data[1:])) } func TestBindingProtoBufFail(t *testing.T) { test := &protoexample.Test{ Label: proto.String("yes"), } data, _ := proto.Marshal(test) testProtoBodyBindingFail(t, ProtoBuf, "protobuf", "/", "/", string(data), string(data[1:])) } func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) err := JSON.Bind(req, &obj) require.Error(t, err) } func TestValidationDisabled(t *testing.T) { backup := Validator Validator = nil defer func() { Validator = backup }() var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) err := JSON.Bind(req, &obj) require.NoError(t, err) } func TestRequiredSucceeds(t *testing.T) { type HogeStruct struct { Hoge *int `json:"hoge" binding:"required"` } var obj HogeStruct req := requestWithBody(http.MethodPost, "/", `{"hoge": 0}`) err := JSON.Bind(req, &obj) require.NoError(t, err) } func TestRequiredFails(t *testing.T) { type HogeStruct struct { Hoge *int `json:"foo" binding:"required"` } var obj HogeStruct req := requestWithBody(http.MethodPost, "/", `{"boen": 0}`) err := JSON.Bind(req, &obj) require.Error(t, err) } func TestHeaderBinding(t *testing.T) { h := Header assert.Equal(t, "header", h.Name()) type tHeader struct { Limit int `header:"limit"` } var theader tHeader req := requestWithBody(http.MethodGet, "/", "") req.Header.Add("limit", "1000") require.NoError(t, h.Bind(req, &theader)) assert.Equal(t, 1000, theader.Limit) req = requestWithBody(http.MethodGet, "/", "") req.Header.Add("fail", `{fail:fail}`) type failStruct struct { Fail map[string]any `header:"fail"` } err := h.Bind(req, &failStruct{}) require.Error(t, err) } func TestUriBinding(t *testing.T) { b := Uri assert.Equal(t, "uri", b.Name()) type Tag struct { Name string `uri:"name"` } var tag Tag m := make(map[string][]string) m["name"] = []string{"thinkerou"} require.NoError(t, b.BindUri(m, &tag)) assert.Equal(t, "thinkerou", tag.Name) type NotSupportStruct struct { Name map[string]any `uri:"name"` } var not NotSupportStruct require.Error(t, b.BindUri(m, ¬)) assert.Equal(t, map[string]any(nil), not.Name) } func TestUriInnerBinding(t *testing.T) { type Tag struct { Name string `uri:"name"` S struct { Age int `uri:"age"` } } expectedName := "mike" expectedAge := 25 m := map[string][]string{ "name": {expectedName}, "age": {strconv.Itoa(expectedAge)}, } var tag Tag require.NoError(t, Uri.BindUri(m, &tag)) assert.Equal(t, expectedName, tag.Name) assert.Equal(t, expectedAge, tag.S.Age) } func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := QueryTest{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, 1, obj.Page) assert.Equal(t, 2, obj.Size) assert.Equal(t, "test-appkey", obj.Appkey) } func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooBarStruct{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "foo", obj.Bar) obj = FooBarStruct{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingDefaultValue(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooDefaultBarStruct{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "hello", obj.Bar) obj = FooDefaultBarStruct{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func TestFormBindingFail(t *testing.T) { b := Form assert.Equal(t, "form", b.Name()) obj := FooBarStruct{} req, _ := http.NewRequest(http.MethodPost, "/", nil) err := b.Bind(req, &obj) require.Error(t, err) } func TestFormBindingMultipartFail(t *testing.T) { obj := FooBarStruct{} req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")) require.NoError(t, err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+";boundary=testboundary") _, err = req.MultipartReader() require.NoError(t, err) err = Form.Bind(req, &obj) require.Error(t, err) } func TestFormPostBindingFail(t *testing.T) { b := FormPost assert.Equal(t, "form-urlencoded", b.Name()) obj := FooBarStruct{} req, _ := http.NewRequest(http.MethodPost, "/", nil) err := b.Bind(req, &obj) require.Error(t, err) } func TestFormMultipartBindingFail(t *testing.T) { b := FormMultipart assert.Equal(t, "multipart/form-data", b.Name()) obj := FooBarStruct{} req, _ := http.NewRequest(http.MethodPost, "/", nil) err := b.Bind(req, &obj) require.Error(t, err) } func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooBarStructForTimeType{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, int64(1510675200), obj.TimeFoo.Unix()) assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, "UTC", obj.TimeBar.Location().String()) assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano()) assert.Equal(t, int64(1562400033), obj.UnixTime.Unix()) obj = FooBarStructForTimeType{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooStructForTimeTypeNotUnixFormat{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) obj = FooStructForTimeTypeNotUnixFormat{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooStructForTimeTypeNotFormat{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) obj = FooStructForTimeTypeNotFormat{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooStructForTimeTypeFailFormat{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) obj = FooStructForTimeTypeFailFormat{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooStructForTimeTypeFailLocation{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) obj = FooStructForTimeTypeFailLocation{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := FooStructForIgnoreFormTag{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Nil(t, obj.Foo) } func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := InvalidNameType{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "", obj.TestName) obj = InvalidNameType{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) obj := InvalidNameMapType{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) obj = InvalidNameMapType{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) { b := Form assert.Equal(t, "form", b.Name()) req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } switch typ { case "Slice": obj := FooStructForSliceType{} err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, []int{1, 2}, obj.SliceFoo) obj = FooStructForSliceType{} req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) case "Struct": obj := FooStructForStructType{} err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, struct { Idx int "form:\"idx\"" }{Idx: 123}, obj.StructFoo) case "StructPointer": obj := FooStructForStructPointerType{} err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, struct { Name string "form:\"name\"" }{Name: "thinkerou"}, *obj.StructPointerFoo) case "Map": obj := FooStructForMapType{} err := b.Bind(req, &obj) require.NoError(t, err) assert.InDelta(t, float64(123), obj.MapFoo["bar"].(float64), 0.01) case "SliceMap": obj := FooStructForSliceMapType{} err := b.Bind(req, &obj) require.Error(t, err) case "Ptr": obj := FooStructForStringPtrType{} err := b.Bind(req, &obj) require.NoError(t, err) assert.Nil(t, obj.PtrFoo) assert.Equal(t, "test", *obj.PtrBar) obj = FooStructForStringPtrType{} obj.PtrBar = new(string) err = b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "test", *obj.PtrBar) objErr := FooStructForMapPtrType{} err = b.Bind(req, &objErr) require.Error(t, err) obj = FooStructForStringPtrType{} req = requestWithBody(method, badPath, badBody) err = b.Bind(req, &obj) require.Error(t, err) } } func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) { b := Query assert.Equal(t, "query", b.Name()) obj := FooBarStruct{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "foo", obj.Bar) } func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody string) { b := Query assert.Equal(t, "query", b.Name()) obj := FooStructForMapType{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) } func testQueryBindingBoolFail(t *testing.T, method, path, badPath, body, badBody string) { b := Query assert.Equal(t, "query", b.Name()) obj := FooStructForBoolType{} req := requestWithBody(method, path, body) if method == http.MethodPost { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.Error(t, err) } func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := FooStruct{} req := requestWithBody(http.MethodPost, path, body) err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "bar", obj.Foo) obj = FooStruct{} req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) var obj1 []FooStruct req := requestWithBody(http.MethodPost, path, body) err := b.Bind(req, &obj1) require.NoError(t, err) var obj2 []FooStruct req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj2) require.Error(t, err) } func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) { obj := make(map[string]string) req := requestWithBody(http.MethodPost, path, body) if b.Name() == "form" { req.Header.Add("Content-Type", MIMEPOSTForm) } err := b.Bind(req, &obj) require.NoError(t, err) assert.NotNil(t, obj) assert.Len(t, obj, 2) assert.Equal(t, "bar", obj["foo"]) assert.Equal(t, "world", obj["hello"]) if badPath != "" && badBody != "" { obj = make(map[string]string) req = requestWithBody(http.MethodPost, badPath, badBody) err = b.Bind(req, &obj) require.Error(t, err) } objInt := make(map[string]int) req = requestWithBody(http.MethodPost, path, body) err = b.Bind(req, &objInt) require.Error(t, err) } func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := FooStructUseNumber{} req := requestWithBody(http.MethodPost, path, body) EnableDecoderUseNumber = true err := b.Bind(req, &obj) require.NoError(t, err) // we hope it is int64(123) v, e := obj.Foo.(json.Number).Int64() require.NoError(t, e) assert.Equal(t, int64(123), v) obj = FooStructUseNumber{} req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := FooStructUseNumber{} req := requestWithBody(http.MethodPost, path, body) EnableDecoderUseNumber = false err := b.Bind(req, &obj) require.NoError(t, err) // it will return float64(123) if not use EnableDecoderUseNumber // maybe it is not hoped assert.InDelta(t, float64(123), obj.Foo, 0.01) obj = FooStructUseNumber{} req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) { EnableDecoderDisallowUnknownFields = true defer func() { EnableDecoderDisallowUnknownFields = false }() obj := FooStructDisallowUnknownFields{} req := requestWithBody(http.MethodPost, path, body) err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "bar", obj.Foo) obj = FooStructDisallowUnknownFields{} req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) assert.Contains(t, err.Error(), "what") } func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := FooStruct{} req := requestWithBody(http.MethodPost, path, body) err := b.Bind(req, &obj) require.Error(t, err) assert.Equal(t, "", obj.Foo) obj = FooStruct{} req = requestWithBody(http.MethodPost, badPath, badBody) err = JSON.Bind(req, &obj) require.Error(t, err) } func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := protoexample.Test{} req := requestWithBody(http.MethodPost, path, body) req.Header.Add("Content-Type", MIMEPROTOBUF) err := b.Bind(req, &obj) require.NoError(t, err) assert.Equal(t, "yes", *obj.Label) obj = protoexample.Test{} req = requestWithBody(http.MethodPost, badPath, badBody) req.Header.Add("Content-Type", MIMEPROTOBUF) err = ProtoBuf.Bind(req, &obj) require.Error(t, err) } type hook struct{} func (h hook) Read([]byte) (int, error) { return 0, errors.New("error") } type failRead struct{} func (f *failRead) Read(b []byte) (n int, err error) { return 0, errors.New("my fail") } func (f *failRead) Close() error { return nil } func TestPlainBinding(t *testing.T) { p := Plain assert.Equal(t, "plain", p.Name()) var s string req := requestWithBody(http.MethodPost, "/", "test string") require.NoError(t, p.Bind(req, &s)) assert.Equal(t, "test string", s) var bs []byte req = requestWithBody(http.MethodPost, "/", "test []byte") require.NoError(t, p.Bind(req, &bs)) assert.Equal(t, bs, []byte("test []byte")) var i int req = requestWithBody(http.MethodPost, "/", "test fail") require.Error(t, p.Bind(req, &i)) req = requestWithBody(http.MethodPost, "/", "") req.Body = &failRead{} require.Error(t, p.Bind(req, &s)) req = requestWithBody(http.MethodPost, "/", "") require.NoError(t, p.Bind(req, nil)) var ptr *string req = requestWithBody(http.MethodPost, "/", "") require.NoError(t, p.Bind(req, ptr)) } func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) obj := protoexample.Test{} req := requestWithBody(http.MethodPost, path, body) req.Body = io.NopCloser(&hook{}) req.Header.Add("Content-Type", MIMEPROTOBUF) err := b.Bind(req, &obj) require.Error(t, err) invalidobj := FooStruct{} req.Body = io.NopCloser(strings.NewReader(`{"msg":"hello"}`)) req.Header.Add("Content-Type", MIMEPROTOBUF) err = b.Bind(req, &invalidobj) require.Error(t, err) assert.Equal(t, "obj is not ProtoMessage", err.Error()) obj = protoexample.Test{} req = requestWithBody(http.MethodPost, badPath, badBody) req.Header.Add("Content-Type", MIMEPROTOBUF) err = ProtoBuf.Bind(req, &obj) require.Error(t, err) } func requestWithBody(method, path, body string) (req *http.Request) { req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return }