diff --git a/.gitignore b/.gitignore index f3b636df..14dc8f20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor/* !vendor/vendor.json coverage.out count.out +test diff --git a/Makefile b/Makefile index 008a951e..2c985304 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ GOFMT ?= gofmt "-s" PACKAGES ?= $(shell go list ./... | grep -v /vendor/) GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") -all: build +all: install install: deps govendor sync .PHONY: test test: - go test -v -covermode=count -coverprofile=coverage.out + sh coverage.sh .PHONY: fmt fmt: diff --git a/README.md b/README.md index 7113058d..6e421f02 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,10 @@ BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104 BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 -(1): Total Repetitions achieved in constant time, higher means more confident result -(2): Single Repetition Duration (ns/op), lower is better -(3): Heap Memory (B/op), lower is better -(4): Average Allocations per Repetition (allocs/op), lower is better +- (1): Total Repetitions achieved in constant time, higher means more confident result +- (2): Single Repetition Duration (ns/op), lower is better +- (3): Heap Memory (B/op), lower is better +- (4): Average Allocations per Repetition (allocs/op), lower is better ## Gin v1. stable @@ -117,7 +117,7 @@ $ go get github.com/kardianos/govendor 2. Create your project folder and `cd` inside ```sh -$ mkdir -p ~/go/src/github.com/myusername/project && cd "$_" +$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_" ``` 3. Vendor init your project and add gin @@ -385,7 +385,7 @@ func main() { r := gin.New() // Global middleware - // Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release. + // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. // By default gin.DefaultWriter = os.Stdout r.Use(gin.Logger()) @@ -435,7 +435,7 @@ func main() { c.String(200, "pong") }) - r.Run(":8080") + router.Run(":8080") } ``` @@ -472,7 +472,7 @@ func main() { // Example for binding JSON ({"user": "manu", "password": "123"}) router.POST("/loginJSON", func(c *gin.Context) { var json Login - if err = c.ShouldBindJSON(&json); err == nil { + if err := c.ShouldBindJSON(&json); err == nil { if json.User == "manu" && json.Password == "123" { c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { @@ -540,7 +540,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" - validator "gopkg.in/go-playground/validator.v8" + "gopkg.in/go-playground/validator.v8" ) type Booking struct { @@ -1071,7 +1071,7 @@ func main() { ### Goroutines inside a middleware -When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. +When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. ```go func main() { @@ -1133,7 +1133,7 @@ func main() { example for 1-line LetsEncrypt HTTPS servers. -[embedmd]:# (examples/auto-tls/example1.go go) +[embedmd]:# (examples/auto-tls/example1/main.go go) ```go package main @@ -1158,7 +1158,7 @@ func main() { example for custom autocert manager. -[embedmd]:# (examples/auto-tls/example2.go go) +[embedmd]:# (examples/auto-tls/example2/main.go go) ```go package main @@ -1190,7 +1190,7 @@ func main() { ### Run multiple service using Gin -See the [question](https://github.com/gin-gonic/gin/issues/346) and try the folling example: +See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: [embedmd]:# (examples/multiple-service/main.go go) ```go @@ -1344,6 +1344,52 @@ func main() { } ``` +## Testing + +The `net/http/httptest` package is preferable way for HTTP testing. + +```go +package main + +func setupRouter() *gin.Engine { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + return r +} + +func main() { + r := setupRouter() + r.Run(":8080") +} +``` + +Test for code example above: + +```go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} +``` + ## Users [](https://sourcegraph.com/github.com/gin-gonic/gin?badge) Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. diff --git a/auth.go b/auth.go index 302a4fad..c2143091 100644 --- a/auth.go +++ b/auth.go @@ -17,8 +17,8 @@ const AuthUserKey = "user" type Accounts map[string]string type authPair struct { - Value string - User string + value string + user string } type authPairs []authPair @@ -28,8 +28,8 @@ func (a authPairs) searchCredential(authValue string) (string, bool) { return "", false } for _, pair := range a { - if pair.Value == authValue { - return pair.User, true + if pair.value == authValue { + return pair.user, true } } return "", false @@ -74,8 +74,8 @@ func processAccounts(accounts Accounts) authPairs { assert1(user != "", "User can not be empty") value := authorizationHeader(user, password) pairs = append(pairs, authPair{ - Value: value, - User: user, + value: value, + user: user, }) } return pairs diff --git a/auth_test.go b/auth_test.go index 2f1ae70e..dc8523b0 100644 --- a/auth_test.go +++ b/auth_test.go @@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) { assert.Len(t, pairs, 3) assert.Contains(t, pairs, authPair{ - User: "bar", - Value: "Basic YmFyOmZvbw==", + user: "bar", + value: "Basic YmFyOmZvbw==", }) assert.Contains(t, pairs, authPair{ - User: "foo", - Value: "Basic Zm9vOmJhcg==", + user: "foo", + value: "Basic Zm9vOmJhcg==", }) assert.Contains(t, pairs, authPair{ - User: "admin", - Value: "Basic YWRtaW46cGFzc3dvcmQ=", + user: "admin", + value: "Basic YWRtaW46cGFzc3dvcmQ=", }) } diff --git a/benchmarks_test.go b/benchmarks_test.go index a2c62ba3..e7970034 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -59,8 +59,6 @@ func BenchmarkOneRouteJSON(B *testing.B) { runRequest(B, router, "GET", "/json") } -var htmlContentType = []string{"text/html; charset=utf-8"} - func BenchmarkOneRouteHTML(B *testing.B) { router := New() t := template.Must(template.New("index").Parse(` diff --git a/binding/binding.go b/binding/binding.go index a09cc221..dc32d538 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -7,7 +7,7 @@ package binding import ( "net/http" - validator "gopkg.in/go-playground/validator.v8" + "gopkg.in/go-playground/validator.v8" ) const ( diff --git a/binding/binding_test.go b/binding/binding_test.go index 5575e166..0c0f9f81 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -6,9 +6,13 @@ package binding import ( "bytes" + "encoding/json" + "errors" + "io/ioutil" "mime/multipart" "net/http" "testing" + "time" "github.com/gin-gonic/gin/binding/example" "github.com/golang/protobuf/proto" @@ -25,6 +29,116 @@ type FooBarStruct struct { Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` } +type FooStructUseNumber struct { + Foo interface{} `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"` +} + +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 { + // Unknown type: not support map + MapFoo map[string]interface{} `form:"map_foo"` +} + +type InvalidNameType struct { + TestName string `invalid_name:"test_name"` +} + +type InvalidNameMapType struct { + TestName struct { + MapFoo map[string]interface{} `form:"map_foo"` + } +} + +type FooStructForSliceType struct { + SliceFoo []int `form:"slice_foo"` +} + +type FooStructForSliceMapType struct { + // Unknown type: not support map + SliceMapFoo []map[string]interface{} `form:"slice_map_foo"` +} + +type FooBarStructForIntType struct { + IntFoo int `form:"int_foo"` + IntBar int `form:"int_bar" binding:"required"` +} + +type FooBarStructForInt8Type struct { + Int8Foo int8 `form:"int8_foo"` + Int8Bar int8 `form:"int8_bar" binding:"required"` +} + +type FooBarStructForInt16Type struct { + Int16Foo int16 `form:"int16_foo"` + Int16Bar int16 `form:"int16_bar" binding:"required"` +} + +type FooBarStructForInt32Type struct { + Int32Foo int32 `form:"int32_foo"` + Int32Bar int32 `form:"int32_bar" binding:"required"` +} + +type FooBarStructForInt64Type struct { + Int64Foo int64 `form:"int64_foo"` + Int64Bar int64 `form:"int64_bar" binding:"required"` +} + +type FooBarStructForUintType struct { + UintFoo uint `form:"uint_foo"` + UintBar uint `form:"uint_bar" binding:"required"` +} + +type FooBarStructForUint8Type struct { + Uint8Foo uint8 `form:"uint8_foo"` + Uint8Bar uint8 `form:"uint8_bar" binding:"required"` +} + +type FooBarStructForUint16Type struct { + Uint16Foo uint16 `form:"uint16_foo"` + Uint16Bar uint16 `form:"uint16_bar" binding:"required"` +} + +type FooBarStructForUint32Type struct { + Uint32Foo uint32 `form:"uint32_foo"` + Uint32Bar uint32 `form:"uint32_bar" binding:"required"` +} + +type FooBarStructForUint64Type struct { + Uint64Foo uint64 `form:"uint64_foo"` + Uint64Bar uint64 `form:"uint64_bar" binding:"required"` +} + +type FooBarStructForBoolType struct { + BoolFoo bool `form:"bool_foo"` + BoolBar bool `form:"bool_bar" binding:"required"` +} + +type FooBarStructForFloat32Type struct { + Float32Foo float32 `form:"float32_foo"` + Float32Bar float32 `form:"float32_bar" binding:"required"` +} + +type FooBarStructForFloat64Type struct { + Float64Foo float64 `form:"float64_foo"` + Float64Bar float64 `form:"float64_bar" binding:"required"` +} + func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("GET", ""), Form) assert.Equal(t, Default("GET", MIMEJSON), Form) @@ -55,6 +169,20 @@ func TestBindingJSON(t *testing.T) { `{"foo": "bar"}`, `{"bar": "foo"}`) } +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 TestBindingForm(t *testing.T) { testFormBinding(t, "POST", "/", "/", @@ -67,6 +195,174 @@ func TestBindingForm2(t *testing.T) { "", "") } +func TestBindingFormForTime(t *testing.T) { + testFormBindingForTime(t, "POST", + "/", "/", + "time_foo=2017-11-15&time_bar=", "bar2=foo") + testFormBindingForTimeNotFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") + testFormBindingForTimeFailFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") + testFormBindingForTimeFailLocation(t, "POST", + "/", "/", + "time_foo=2017-11-15", "bar2=foo") +} + +func TestBindingFormForTime2(t *testing.T) { + testFormBindingForTime(t, "GET", + "/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", + "", "") + testFormBindingForTimeNotFormat(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") + testFormBindingForTimeFailFormat(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") + testFormBindingForTimeFailLocation(t, "GET", + "/?time_foo=2017-11-15", "/?bar2=foo", + "", "") +} + +func TestBindingFormInvalidName(t *testing.T) { + testFormBindingInvalidName(t, "POST", + "/", "/", + "test_name=bar", "bar2=foo") +} + +func TestBindingFormInvalidName2(t *testing.T) { + testFormBindingInvalidName2(t, "POST", + "/", "/", + "map_foo=bar", "bar2=foo") +} + +func TestBindingFormForType(t *testing.T) { + testFormBindingForType(t, "POST", + "/", "/", + "map_foo=", "bar2=1", "Map") + + testFormBindingForType(t, "POST", + "/", "/", + "slice_foo=1&slice_foo=2", "bar2=1&bar2=2", "Slice") + + testFormBindingForType(t, "GET", + "/?slice_foo=1&slice_foo=2", "/?bar2=1&bar2=2", + "", "", "Slice") + + testFormBindingForType(t, "POST", + "/", "/", + "slice_map_foo=1&slice_map_foo=2", "bar2=1&bar2=2", "SliceMap") + + testFormBindingForType(t, "GET", + "/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2", + "", "", "SliceMap") + + testFormBindingForType(t, "POST", + "/", "/", + "int_foo=&int_bar=-12", "bar2=-123", "Int") + + testFormBindingForType(t, "GET", + "/?int_foo=&int_bar=-12", "/?bar2=-123", + "", "", "Int") + + testFormBindingForType(t, "POST", + "/", "/", + "int8_foo=&int8_bar=-12", "bar2=-123", "Int8") + + testFormBindingForType(t, "GET", + "/?int8_foo=&int8_bar=-12", "/?bar2=-123", + "", "", "Int8") + + testFormBindingForType(t, "POST", + "/", "/", + "int16_foo=&int16_bar=-12", "bar2=-123", "Int16") + + testFormBindingForType(t, "GET", + "/?int16_foo=&int16_bar=-12", "/?bar2=-123", + "", "", "Int16") + + testFormBindingForType(t, "POST", + "/", "/", + "int32_foo=&int32_bar=-12", "bar2=-123", "Int32") + + testFormBindingForType(t, "GET", + "/?int32_foo=&int32_bar=-12", "/?bar2=-123", + "", "", "Int32") + + testFormBindingForType(t, "POST", + "/", "/", + "int64_foo=&int64_bar=-12", "bar2=-123", "Int64") + + testFormBindingForType(t, "GET", + "/?int64_foo=&int64_bar=-12", "/?bar2=-123", + "", "", "Int64") + + testFormBindingForType(t, "POST", + "/", "/", + "uint_foo=&uint_bar=12", "bar2=123", "Uint") + + testFormBindingForType(t, "GET", + "/?uint_foo=&uint_bar=12", "/?bar2=123", + "", "", "Uint") + + testFormBindingForType(t, "POST", + "/", "/", + "uint8_foo=&uint8_bar=12", "bar2=123", "Uint8") + + testFormBindingForType(t, "GET", + "/?uint8_foo=&uint8_bar=12", "/?bar2=123", + "", "", "Uint8") + + testFormBindingForType(t, "POST", + "/", "/", + "uint16_foo=&uint16_bar=12", "bar2=123", "Uint16") + + testFormBindingForType(t, "GET", + "/?uint16_foo=&uint16_bar=12", "/?bar2=123", + "", "", "Uint16") + + testFormBindingForType(t, "POST", + "/", "/", + "uint32_foo=&uint32_bar=12", "bar2=123", "Uint32") + + testFormBindingForType(t, "GET", + "/?uint32_foo=&uint32_bar=12", "/?bar2=123", + "", "", "Uint32") + + testFormBindingForType(t, "POST", + "/", "/", + "uint64_foo=&uint64_bar=12", "bar2=123", "Uint64") + + testFormBindingForType(t, "GET", + "/?uint64_foo=&uint64_bar=12", "/?bar2=123", + "", "", "Uint64") + + testFormBindingForType(t, "POST", + "/", "/", + "bool_foo=&bool_bar=true", "bar2=true", "Bool") + + testFormBindingForType(t, "GET", + "/?bool_foo=&bool_bar=true", "/?bar2=true", + "", "", "Bool") + + testFormBindingForType(t, "POST", + "/", "/", + "float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32") + + testFormBindingForType(t, "GET", + "/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3", + "", "", "Float32") + + testFormBindingForType(t, "POST", + "/", "/", + "float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64") + + testFormBindingForType(t, "GET", + "/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3", + "", "", "Float64") +} + func TestBindingQuery(t *testing.T) { testQueryBinding(t, "POST", "/?foo=bar&bar=foo", "/", @@ -79,6 +375,18 @@ func TestBindingQuery2(t *testing.T) { "foo=unused", "") } +func TestBindingQueryFail(t *testing.T) { + testQueryBindingFail(t, "POST", + "/?map_foo=", "/", + "map_foo=unused", "bar2=foo") +} + +func TestBindingQueryFail2(t *testing.T) { + testQueryBindingFail(t, "GET", + "/?map_foo=", "/?bar2=foo", + "map_foo=unused", "") +} + func TestBindingXML(t *testing.T) { testBodyBinding(t, XML, "xml", @@ -86,12 +394,25 @@ func TestBindingXML(t *testing.T) { "", "") } +func TestBindingXMLFail(t *testing.T) { + testBodyBindingFail(t, + XML, "xml", + "/", "/", + "", "") +} + func createFormPostRequest() *http.Request { req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) req.Header.Set("Content-Type", MIMEPOSTForm) return req } +func createFormPostRequestFail() *http.Request { + req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar")) + req.Header.Set("Content-Type", MIMEPOSTForm) + return req +} + func createFormMultipartRequest() *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) @@ -106,24 +427,53 @@ func createFormMultipartRequest() *http.Request { return req } +func createFormMultipartRequestFail() *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + mw.SetBoundary(boundary) + mw.WriteField("map_foo", "bar") + req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + return req +} + func TestBindingFormPost(t *testing.T) { req := createFormPostRequest() var obj FooBarStruct FormPost.Bind(req, &obj) + assert.Equal(t, FormPost.Name(), "form-urlencoded") assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Bar, "foo") } +func TestBindingFormPostFail(t *testing.T) { + req := createFormPostRequestFail() + var obj FooStructForMapType + err := FormPost.Bind(req, &obj) + assert.Error(t, err) +} + func TestBindingFormMultipart(t *testing.T) { req := createFormMultipartRequest() var obj FooBarStruct FormMultipart.Bind(req, &obj) + assert.Equal(t, FormMultipart.Name(), "multipart/form-data") assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Bar, "foo") } +func TestBindingFormMultipartFail(t *testing.T) { + req := createFormMultipartRequestFail() + var obj FooStructForMapType + err := FormMultipart.Bind(req, &obj) + assert.Error(t, err) +} + func TestBindingProtoBuf(t *testing.T) { test := &example.Test{ Label: proto.String("yes"), @@ -136,6 +486,18 @@ func TestBindingProtoBuf(t *testing.T) { string(data), string(data[1:])) } +func TestBindingProtoBufFail(t *testing.T) { + test := &example.Test{ + Label: proto.String("yes"), + } + data, _ := proto.Marshal(test) + + testProtoBodyBindingFail(t, + ProtoBuf, "protobuf", + "/", "/", + string(data), string(data[1:])) +} + func TestBindingMsgPack(t *testing.T) { test := FooStruct{ Foo: "bar", @@ -216,6 +578,323 @@ func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) assert.Error(t, err) } +func TestFormBindingFail(t *testing.T) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func TestFormPostBindingFail(t *testing.T) { + b := FormPost + assert.Equal(t, b.Name(), "form-urlencoded") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func TestFormMultipartBindingFail(t *testing.T) { + b := FormMultipart + assert.Equal(t, b.Name(), "multipart/form-data") + + obj := FooBarStruct{} + req, _ := http.NewRequest("POST", "/", nil) + err := b.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooBarStructForTimeType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + + assert.NoError(t, err) + assert.Equal(t, obj.TimeFoo.Unix(), int64(1510675200)) + assert.Equal(t, obj.TimeFoo.Location().String(), "Asia/Chongqing") + assert.Equal(t, obj.TimeBar.Unix(), int64(-62135596800)) + assert.Equal(t, obj.TimeBar.Location().String(), "UTC") + + obj = FooBarStructForTimeType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeNotFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeNotFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeFailFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeFailFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooStructForTimeTypeFailLocation{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeFailLocation{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := InvalidNameType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.TestName, "") + + obj = InvalidNameType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := InvalidNameMapType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = InvalidNameMapType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) { + b := Form + assert.Equal(t, b.Name(), "form") + + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + switch typ { + case "Int": + obj := FooBarStructForIntType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.IntFoo, int(0)) + assert.Equal(t, obj.IntBar, int(-12)) + + obj = FooBarStructForIntType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int8": + obj := FooBarStructForInt8Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int8Foo, int8(0)) + assert.Equal(t, obj.Int8Bar, int8(-12)) + + obj = FooBarStructForInt8Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int16": + obj := FooBarStructForInt16Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int16Foo, int16(0)) + assert.Equal(t, obj.Int16Bar, int16(-12)) + + obj = FooBarStructForInt16Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int32": + obj := FooBarStructForInt32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int32Foo, int32(0)) + assert.Equal(t, obj.Int32Bar, int32(-12)) + + obj = FooBarStructForInt32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Int64": + obj := FooBarStructForInt64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Int64Foo, int64(0)) + assert.Equal(t, obj.Int64Bar, int64(-12)) + + obj = FooBarStructForInt64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint": + obj := FooBarStructForUintType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.UintFoo, uint(0x0)) + assert.Equal(t, obj.UintBar, uint(0xc)) + + obj = FooBarStructForUintType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint8": + obj := FooBarStructForUint8Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint8Foo, uint8(0x0)) + assert.Equal(t, obj.Uint8Bar, uint8(0xc)) + + obj = FooBarStructForUint8Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint16": + obj := FooBarStructForUint16Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint16Foo, uint16(0x0)) + assert.Equal(t, obj.Uint16Bar, uint16(0xc)) + + obj = FooBarStructForUint16Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint32": + obj := FooBarStructForUint32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint32Foo, uint32(0x0)) + assert.Equal(t, obj.Uint32Bar, uint32(0xc)) + + obj = FooBarStructForUint32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Uint64": + obj := FooBarStructForUint64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Uint64Foo, uint64(0x0)) + assert.Equal(t, obj.Uint64Bar, uint64(0xc)) + + obj = FooBarStructForUint64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Float32": + obj := FooBarStructForFloat32Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Float32Foo, float32(0.0)) + assert.Equal(t, obj.Float32Bar, float32(-12.34)) + + obj = FooBarStructForFloat32Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Float64": + obj := FooBarStructForFloat64Type{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Float64Foo, float64(0.0)) + assert.Equal(t, obj.Float64Bar, float64(-12.34)) + + obj = FooBarStructForFloat64Type{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Bool": + obj := FooBarStructForBoolType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.BoolFoo, false) + assert.Equal(t, obj.BoolBar, true) + + obj = FooBarStructForBoolType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Slice": + obj := FooStructForSliceType{} + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.SliceFoo, []int{1, 2}) + + obj = FooStructForSliceType{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) + case "Map": + obj := FooStructForMapType{} + err := b.Bind(req, &obj) + assert.Error(t, err) + case "SliceMap": + obj := FooStructForSliceMapType{} + err := b.Bind(req, &obj) + assert.Error(t, err) + } +} + func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) { b := Query assert.Equal(t, b.Name(), "query") @@ -231,6 +910,19 @@ func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) assert.Equal(t, obj.Bar, "foo") } +func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody string) { + b := Query + assert.Equal(t, b.Name(), "query") + + obj := FooStructForMapType{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) +} + func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) @@ -246,6 +938,58 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody assert.Error(t, err) } +func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStructUseNumber{} + req := requestWithBody("POST", path, body) + EnableDecoderUseNumber = true + err := b.Bind(req, &obj) + assert.NoError(t, err) + // we hope it is int64(123) + v, e := obj.Foo.(json.Number).Int64() + assert.NoError(t, e) + assert.Equal(t, v, int64(123)) + + obj = FooStructUseNumber{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStructUseNumber{} + req := requestWithBody("POST", path, body) + EnableDecoderUseNumber = false + err := b.Bind(req, &obj) + assert.NoError(t, err) + // it will return float64(123) if not use EnableDecoderUseNumber + // maybe it is not hoped + assert.Equal(t, obj.Foo, float64(123)) + + obj = FooStructUseNumber{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj) + assert.Error(t, err) + assert.Equal(t, obj.Foo, "") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) @@ -263,6 +1007,30 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba assert.Error(t, err) } +type hook struct{} + +func (h hook) Read([]byte) (int, error) { + return 0, errors.New("error") +} + +func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := example.Test{} + req := requestWithBody("POST", path, body) + + req.Body = ioutil.NopCloser(&hook{}) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = example.Test{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err = ProtoBuf.Bind(req, &obj) + assert.Error(t, err) +} + func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, b.Name(), name) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index c968dc08..dd8c6246 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -179,12 +179,3 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val value.Set(reflect.ValueOf(t)) return nil } - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") - } -} diff --git a/binding/json.go b/binding/json.go index f600a5f4..b7c856af 100644 --- a/binding/json.go +++ b/binding/json.go @@ -10,9 +10,7 @@ import ( "github.com/gin-gonic/gin/json" ) -var ( - EnableDecoderUseNumber = false -) +var EnableDecoderUseNumber = false type jsonBinding struct{} diff --git a/binding/query.go b/binding/query.go index a789f798..219743f2 100644 --- a/binding/query.go +++ b/binding/query.go @@ -4,9 +4,7 @@ package binding -import ( - "net/http" -) +import "net/http" type queryBinding struct{} diff --git a/binding/validate_test.go b/binding/validate_test.go index 523e1298..8ca79989 100644 --- a/binding/validate_test.go +++ b/binding/validate_test.go @@ -10,9 +10,8 @@ import ( "testing" "time" - validator "gopkg.in/go-playground/validator.v8" - "github.com/stretchr/testify/assert" + "gopkg.in/go-playground/validator.v8" ) type testInterface interface { diff --git a/context.go b/context.go index 5b67dcc0..90d4c6e5 100644 --- a/context.go +++ b/context.go @@ -33,9 +33,7 @@ const ( MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -const ( - abortIndex int8 = math.MaxInt8 / 2 -) +const abortIndex int8 = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -105,8 +103,7 @@ func (c *Context) Handler() HandlerFunc { // See example in GitHub. func (c *Context) Next() { c.index++ - s := int8(len(c.handlers)) - for ; c.index < s; c.index++ { + for s := int8(len(c.handlers)); c.index < s; c.index++ { c.handlers[c.index](c) } } @@ -452,10 +449,10 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding // "application/xml" --> XML binding -// otherwise --> returns an error +// otherwise --> returns an error. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It decodes the json payload into the struct specified as a pointer. -// It will writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. +// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.MustBindWith(obj, b) @@ -521,11 +518,11 @@ func (c *Context) ClientIP() string { clientIP = clientIP[0:index] } clientIP = strings.TrimSpace(clientIP) - if len(clientIP) > 0 { + if clientIP != "" { return clientIP } clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) - if len(clientIP) > 0 { + if clientIP != "" { return clientIP } } @@ -588,7 +585,7 @@ func (c *Context) Status(code int) { // It writes a header in the response. // If value == "", this method removes the header `c.Writer.Header().Del(key)` func (c *Context) Header(key, value string) { - if len(value) == 0 { + if value == "" { c.Writer.Header().Del(key) } else { c.Writer.Header().Set(key, value) diff --git a/context_test.go b/context_test.go index a508ef95..2f723892 100644 --- a/context_test.go +++ b/context_test.go @@ -180,14 +180,14 @@ func TestContextSetGet(t *testing.T) { c.Set("foo", "bar") value, err := c.Get("foo") - assert.Equal(t, value, "bar") + assert.Equal(t, "bar", value) assert.True(t, err) value, err = c.Get("foo2") assert.Nil(t, value) assert.False(t, err) - assert.Equal(t, c.MustGet("foo"), "bar") + assert.Equal(t, "bar", c.MustGet("foo")) assert.Panics(t, func() { c.MustGet("no_exist") }) } @@ -221,7 +221,7 @@ func TestContextGetString(t *testing.T) { func TestContextSetGetBool(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("bool", true) - assert.Equal(t, true, c.GetBool("bool")) + assert.True(t, c.GetBool("bool")) } func TestContextGetInt(t *testing.T) { @@ -342,26 +342,26 @@ func TestContextQuery(t *testing.T) { value, ok := c.GetQuery("foo") assert.True(t, ok) - assert.Equal(t, value, "bar") - assert.Equal(t, c.DefaultQuery("foo", "none"), "bar") - assert.Equal(t, c.Query("foo"), "bar") + assert.Equal(t, "bar", value) + assert.Equal(t, "bar", c.DefaultQuery("foo", "none")) + assert.Equal(t, "bar", c.Query("foo")) value, ok = c.GetQuery("page") assert.True(t, ok) - assert.Equal(t, value, "10") - assert.Equal(t, c.DefaultQuery("page", "0"), "10") - assert.Equal(t, c.Query("page"), "10") + assert.Equal(t, "10", value) + assert.Equal(t, "10", c.DefaultQuery("page", "0")) + assert.Equal(t, "10", c.Query("page")) value, ok = c.GetQuery("id") assert.True(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultQuery("id", "nada"), "") + assert.Empty(t, c.DefaultQuery("id", "nada")) assert.Empty(t, c.Query("id")) value, ok = c.GetQuery("NoKey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultQuery("NoKey", "nada"), "nada") + assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada")) assert.Empty(t, c.Query("NoKey")) // postform should not mess @@ -377,29 +377,29 @@ func TestContextQueryAndPostForm(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) - assert.Equal(t, c.DefaultPostForm("foo", "none"), "bar") - assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, "bar", c.DefaultPostForm("foo", "none")) + assert.Equal(t, "bar", c.PostForm("foo")) assert.Empty(t, c.Query("foo")) value, ok := c.GetPostForm("page") assert.True(t, ok) - assert.Equal(t, value, "11") - assert.Equal(t, c.DefaultPostForm("page", "0"), "11") - assert.Equal(t, c.PostForm("page"), "11") - assert.Equal(t, c.Query("page"), "") + assert.Equal(t, "11", value) + assert.Equal(t, "11", c.DefaultPostForm("page", "0")) + assert.Equal(t, "11", c.PostForm("page")) + assert.Empty(t, c.Query("page")) value, ok = c.GetPostForm("both") assert.True(t, ok) assert.Empty(t, value) assert.Empty(t, c.PostForm("both")) - assert.Equal(t, c.DefaultPostForm("both", "nothing"), "") - assert.Equal(t, c.Query("both"), "GET") + assert.Empty(t, c.DefaultPostForm("both", "nothing")) + assert.Equal(t, "GET", c.Query("both"), "GET") value, ok = c.GetQuery("id") assert.True(t, ok) - assert.Equal(t, value, "main") - assert.Equal(t, c.DefaultPostForm("id", "000"), "000") - assert.Equal(t, c.Query("id"), "main") + assert.Equal(t, "main", value) + assert.Equal(t, "000", c.DefaultPostForm("id", "000")) + assert.Equal(t, "main", c.Query("id")) assert.Empty(t, c.PostForm("id")) value, ok = c.GetQuery("NoKey") @@ -408,8 +408,8 @@ func TestContextQueryAndPostForm(t *testing.T) { value, ok = c.GetPostForm("NoKey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultPostForm("NoKey", "nada"), "nada") - assert.Equal(t, c.DefaultQuery("NoKey", "nothing"), "nothing") + assert.Equal(t, "nada", c.DefaultPostForm("NoKey", "nada")) + assert.Equal(t, "nothing", c.DefaultQuery("NoKey", "nothing")) assert.Empty(t, c.PostForm("NoKey")) assert.Empty(t, c.Query("NoKey")) @@ -421,11 +421,11 @@ func TestContextQueryAndPostForm(t *testing.T) { Array []string `form:"array[]"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.ID, "main") - assert.Equal(t, obj.Page, 11) - assert.Equal(t, obj.Both, "") - assert.Equal(t, obj.Array, []string{"first", "second"}) + assert.Equal(t, "bar", obj.Foo, "bar") + assert.Equal(t, "main", obj.ID, "main") + assert.Equal(t, 11, obj.Page, 11) + assert.Empty(t, obj.Both) + assert.Equal(t, []string{"first", "second"}, obj.Array) values, ok := c.GetQueryArray("array[]") assert.True(t, ok) @@ -460,37 +460,37 @@ func TestContextPostFormMultipart(t *testing.T) { BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "10") - assert.Equal(t, obj.BarAsInt, 10) - assert.Equal(t, obj.Array, []string{"first", "second"}) - assert.Equal(t, obj.ID, "") - assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55") - assert.Equal(t, obj.TimeLocal.Location(), time.Local) - assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55") - assert.Equal(t, obj.TimeUTC.Location(), time.UTC) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, "10", obj.Bar) + assert.Equal(t, 10, obj.BarAsInt) + assert.Equal(t, []string{"first", "second"}, obj.Array) + assert.Empty(t, obj.ID) + assert.Equal(t, "31/12/2016 14:55", obj.TimeLocal.Format("02/01/2006 15:04")) + assert.Equal(t, time.Local, obj.TimeLocal.Location()) + assert.Equal(t, "31/12/2016 14:55", obj.TimeUTC.Format("02/01/2006 15:04")) + assert.Equal(t, time.UTC, obj.TimeUTC.Location()) loc, _ := time.LoadLocation("Asia/Tokyo") - assert.Equal(t, obj.TimeLocation.Format("02/01/2006 15:04"), "31/12/2016 14:55") - assert.Equal(t, obj.TimeLocation.Location(), loc) + assert.Equal(t, "31/12/2016 14:55", obj.TimeLocation.Format("02/01/2006 15:04")) + assert.Equal(t, loc, obj.TimeLocation.Location()) assert.True(t, obj.BlankTime.IsZero()) value, ok := c.GetQuery("foo") assert.False(t, ok) assert.Empty(t, value) assert.Empty(t, c.Query("bar")) - assert.Equal(t, c.DefaultQuery("id", "nothing"), "nothing") + assert.Equal(t, "nothing", c.DefaultQuery("id", "nothing")) value, ok = c.GetPostForm("foo") assert.True(t, ok) - assert.Equal(t, value, "bar") - assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, "bar", value) + assert.Equal(t, "bar", c.PostForm("foo")) value, ok = c.GetPostForm("array") assert.True(t, ok) - assert.Equal(t, value, "first") - assert.Equal(t, c.PostForm("array"), "first") + assert.Equal(t, "first", value) + assert.Equal(t, "first", c.PostForm("array")) - assert.Equal(t, c.DefaultPostForm("bar", "nothing"), "10") + assert.Equal(t, "10", c.DefaultPostForm("bar", "nothing")) value, ok = c.GetPostForm("id") assert.True(t, ok) @@ -501,7 +501,7 @@ func TestContextPostFormMultipart(t *testing.T) { value, ok = c.GetPostForm("nokey") assert.False(t, ok) assert.Empty(t, value) - assert.Equal(t, c.DefaultPostForm("nokey", "nothing"), "nothing") + assert.Equal(t, "nothing", c.DefaultPostForm("nokey", "nothing")) values, ok := c.GetPostFormArray("array") assert.True(t, ok) @@ -523,13 +523,13 @@ func TestContextPostFormMultipart(t *testing.T) { func TestContextSetCookie(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.SetCookie("user", "gin", 1, "/", "localhost", true, true) - assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) } func TestContextSetCookiePathEmpty(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.SetCookie("user", "gin", 1, "", "localhost", true, true) - assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) } func TestContextGetCookie(t *testing.T) { @@ -537,17 +537,17 @@ func TestContextGetCookie(t *testing.T) { c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Cookie", "user=gin") cookie, _ := c.Cookie("user") - assert.Equal(t, cookie, "gin") + assert.Equal(t, "gin", cookie) _, err := c.Cookie("nokey") assert.Error(t, err) } func TestContextBodyAllowedForStatus(t *testing.T) { - assert.Equal(t, false, bodyAllowedForStatus(102)) - assert.Equal(t, false, bodyAllowedForStatus(204)) - assert.Equal(t, false, bodyAllowedForStatus(304)) - assert.Equal(t, true, bodyAllowedForStatus(500)) + assert.False(t, false, bodyAllowedForStatus(102)) + assert.False(t, false, bodyAllowedForStatus(204)) + assert.False(t, false, bodyAllowedForStatus(304)) + assert.True(t, true, bodyAllowedForStatus(500)) } type TestPanicRender struct { @@ -593,7 +593,7 @@ func TestContextRenderNoContentJSON(t *testing.T) { c.JSON(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -620,7 +620,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) { c.JSON(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") } @@ -632,7 +632,7 @@ func TestContextRenderIndentedJSON(t *testing.T) { c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) - assert.Equal(t, w.Code, 201) + assert.Equal(t, 201, w.Code) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -645,7 +645,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) { c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) + assert.Empty(t, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } @@ -658,9 +658,9 @@ func TestContextRenderSecureJSON(t *testing.T) { router.SecureJsonPrefix("&&&START&&&") c.SecureJSON(201, []string{"foo", "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "&&&START&&&[\"foo\",\"bar\"]") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no Custom JSON is rendered if code is 204 @@ -671,8 +671,8 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { c.SecureJSON(204, []string{"foo", "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that the response executes the templates @@ -680,14 +680,39 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { func TestContextRenderHTML(t *testing.T) { w := httptest.NewRecorder() c, router := CreateTestContext(w) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) c.HTML(201, "t", H{"name": "alexandernyquist"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "Hello alexandernyquist") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "Hello alexandernyquist", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + +func TestContextRenderHTML2(t *testing.T) { + w := httptest.NewRecorder() + c, router := CreateTestContext(w) + + // print debug warning log when Engine.trees > 0 + router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) + assert.Len(t, router.trees, 1) + + var b bytes.Buffer + setup(&b) + defer teardown() + + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + router.SetHTMLTemplate(templ) + + assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String()) + + c.HTML(201, "t", H{"name": "alexandernyquist"}) + + assert.Equal(t, 201, w.Code) + assert.Equal(t, "Hello alexandernyquist", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no HTML is rendered if code is 204 @@ -700,8 +725,8 @@ func TestContextRenderNoContentHTML(t *testing.T) { c.HTML(204, "t", H{"name": "alexandernyquist"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextXML tests that the response is serialized as XML @@ -712,9 +737,9 @@ func TestContextRenderXML(t *testing.T) { c.XML(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no XML is rendered if code is 204 @@ -725,8 +750,8 @@ func TestContextRenderNoContentXML(t *testing.T) { c.XML(204, H{"foo": "bar"}) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextString tests that the response is returned @@ -737,9 +762,9 @@ func TestContextRenderString(t *testing.T) { c.String(201, "test %s %d", "string", 2) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "test string 2") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "test string 2", w.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no String is rendered if code is 204 @@ -750,8 +775,8 @@ func TestContextRenderNoContentString(t *testing.T) { c.String(204, "test %s %d", "string", 2) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextString tests that the response is returned @@ -763,9 +788,9 @@ func TestContextRenderHTMLString(t *testing.T) { c.Header("Content-Type", "text/html; charset=utf-8") c.String(201, "%s %d", "string", 3) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "string 3") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "string 3", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that no HTML String is rendered if code is 204 @@ -777,8 +802,8 @@ func TestContextRenderNoContentHTMLString(t *testing.T) { c.String(204, "%s %d", "string", 3) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextData tests that the response can be written from `bytesting` @@ -789,9 +814,9 @@ func TestContextRenderData(t *testing.T) { c.Data(201, "text/csv", []byte(`foo,bar`)) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "foo,bar") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "foo,bar", w.Body.String()) + assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) } // Tests that no Custom Data is rendered if code is 204 @@ -802,8 +827,8 @@ func TestContextRenderNoContentData(t *testing.T) { c.Data(204, "text/csv", []byte(`foo,bar`)) assert.Equal(t, 204, w.Code) - assert.Equal(t, "", w.Body.String()) - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") + assert.Empty(t, w.Body.String()) + assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) } func TestContextRenderSSE(t *testing.T) { @@ -830,9 +855,9 @@ func TestContextRenderFile(t *testing.T) { c.Request, _ = http.NewRequest("GET", "/", nil) c.File("./gin.go") - assert.Equal(t, w.Code, 200) + assert.Equal(t, 200, w.Code) assert.Contains(t, w.Body.String(), "func New() *Engine {") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // TestContextRenderYAML tests that the response is serialized as YAML @@ -843,9 +868,9 @@ func TestContextRenderYAML(t *testing.T) { c.YAML(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "foo: bar\n") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/x-yaml; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "foo: bar\n", w.Body.String()) + assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type")) } func TestContextHeaders(t *testing.T) { @@ -853,13 +878,13 @@ func TestContextHeaders(t *testing.T) { c.Header("Content-Type", "text/plain") c.Header("X-Custom", "value") - assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain") - assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value") + assert.Equal(t, "text/plain", c.Writer.Header().Get("Content-Type")) + assert.Equal(t, "value", c.Writer.Header().Get("X-Custom")) c.Header("Content-Type", "text/html") c.Header("X-Custom", "") - assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html") + assert.Equal(t, "text/html", c.Writer.Header().Get("Content-Type")) _, exist := c.Writer.Header()["X-Custom"] assert.False(t, exist) } @@ -875,8 +900,8 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) { c.Redirect(301, "/path") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 301) - assert.Equal(t, w.Header().Get("Location"), "/path") + assert.Equal(t, 301, w.Code) + assert.Equal(t, "/path", w.Header().Get("Location")) } func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { @@ -887,8 +912,8 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { c.Redirect(302, "http://google.com") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 302) - assert.Equal(t, w.Header().Get("Location"), "http://google.com") + assert.Equal(t, 302, w.Code) + assert.Equal(t, "http://google.com", w.Header().Get("Location")) } func TestContextRenderRedirectWith201(t *testing.T) { @@ -899,8 +924,8 @@ func TestContextRenderRedirectWith201(t *testing.T) { c.Redirect(201, "/resource") c.Writer.WriteHeaderNow() - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Header().Get("Location"), "/resource") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "/resource", w.Header().Get("Location")) } func TestContextRenderRedirectAll(t *testing.T) { @@ -981,8 +1006,8 @@ func TestContextNegotiationFormat(t *testing.T) { c.Request, _ = http.NewRequest("POST", "", nil) assert.Panics(t, func() { c.NegotiateFormat() }) - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) - assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML, MIMEJSON)) } func TestContextNegotiationFormatWithAccept(t *testing.T) { @@ -990,9 +1015,9 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") + assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML)) + assert.Empty(t, c.NegotiateFormat(MIMEJSON)) } func TestContextNegotiationFormatCustum(t *testing.T) { @@ -1003,9 +1028,9 @@ func TestContextNegotiationFormatCustum(t *testing.T) { c.Accepted = nil c.SetAccepted(MIMEJSON, MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) - assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) - assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML)) + assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML, MIMEHTML)) + assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON)) } func TestContextIsAborted(t *testing.T) { @@ -1031,9 +1056,9 @@ func TestContextAbortWithStatus(t *testing.T) { c.index = 4 c.AbortWithStatus(401) - assert.Equal(t, c.index, abortIndex) - assert.Equal(t, c.Writer.Status(), 401) - assert.Equal(t, w.Code, 401) + assert.Equal(t, abortIndex, c.index) + assert.Equal(t, 401, c.Writer.Status()) + assert.Equal(t, 401, w.Code) assert.True(t, c.IsAborted()) } @@ -1053,13 +1078,13 @@ func TestContextAbortWithStatusJSON(t *testing.T) { c.AbortWithStatusJSON(415, in) - assert.Equal(t, c.index, abortIndex) - assert.Equal(t, c.Writer.Status(), 415) - assert.Equal(t, w.Code, 415) + assert.Equal(t, abortIndex, c.index) + assert.Equal(t, 415, c.Writer.Status()) + assert.Equal(t, 415, w.Code) assert.True(t, c.IsAborted()) contentType := w.Header().Get("Content-Type") - assert.Equal(t, contentType, "application/json; charset=utf-8") + assert.Equal(t, "application/json; charset=utf-8", contentType) buf := new(bytes.Buffer) buf.ReadFrom(w.Body) @@ -1073,7 +1098,7 @@ func TestContextError(t *testing.T) { c.Error(errors.New("first error")) assert.Len(t, c.Errors, 1) - assert.Equal(t, c.Errors.String(), "Error #01: first error\n") + assert.Equal(t, "Error #01: first error\n", c.Errors.String()) c.Error(&Error{ Err: errors.New("second error"), @@ -1082,13 +1107,13 @@ func TestContextError(t *testing.T) { }) assert.Len(t, c.Errors, 2) - assert.Equal(t, c.Errors[0].Err, errors.New("first error")) + assert.Equal(t, errors.New("first error"), c.Errors[0].Err) assert.Nil(t, c.Errors[0].Meta) - assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate) + assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type) - assert.Equal(t, c.Errors[1].Err, errors.New("second error")) - assert.Equal(t, c.Errors[1].Meta, "some data 2") - assert.Equal(t, c.Errors[1].Type, ErrorTypePublic) + assert.Equal(t, errors.New("second error"), c.Errors[1].Err) + assert.Equal(t, "some data 2", c.Errors[1].Meta) + assert.Equal(t, ErrorTypePublic, c.Errors[1].Type) assert.Equal(t, c.Errors.Last(), c.Errors[1]) @@ -1106,12 +1131,12 @@ func TestContextTypedError(t *testing.T) { c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) for _, err := range c.Errors.ByType(ErrorTypePublic) { - assert.Equal(t, err.Type, ErrorTypePublic) + assert.Equal(t, ErrorTypePublic, err.Type) } for _, err := range c.Errors.ByType(ErrorTypePrivate) { - assert.Equal(t, err.Type, ErrorTypePrivate) + assert.Equal(t, ErrorTypePrivate, err.Type) } - assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"}) + assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors()) } func TestContextAbortWithError(t *testing.T) { @@ -1120,8 +1145,8 @@ func TestContextAbortWithError(t *testing.T) { c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") - assert.Equal(t, w.Code, 401) - assert.Equal(t, c.index, abortIndex) + assert.Equal(t, 401, w.Code) + assert.Equal(t, abortIndex, c.index) assert.True(t, c.IsAborted()) } @@ -1152,7 +1177,7 @@ func TestContextClientIP(t *testing.T) { // no port c.Request.RemoteAddr = "50.50.50.50" - assert.Equal(t, "", c.ClientIP()) + assert.Empty(t, c.ClientIP()) } func TestContextContentType(t *testing.T) { @@ -1160,7 +1185,7 @@ func TestContextContentType(t *testing.T) { c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") - assert.Equal(t, c.ContentType(), "application/json") + assert.Equal(t, "application/json", c.ContentType()) } func TestContextAutoBindJSON(t *testing.T) { @@ -1173,8 +1198,8 @@ func TestContextAutoBindJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.Bind(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) assert.Empty(t, c.Errors) } @@ -1190,9 +1215,9 @@ func TestContextBindWithJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.BindJSON(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, w.Body.Len(), 0) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) } func TestContextBindWithQuery(t *testing.T) { @@ -1228,7 +1253,7 @@ func TestContextBadAutoBind(t *testing.T) { assert.Empty(t, obj.Bar) assert.Empty(t, obj.Foo) - assert.Equal(t, w.Code, 400) + assert.Equal(t, 400, w.Code) assert.True(t, c.IsAborted()) } @@ -1242,8 +1267,8 @@ func TestContextAutoShouldBindJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.ShouldBind(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) assert.Empty(t, c.Errors) } @@ -1259,9 +1284,9 @@ func TestContextShouldBindWithJSON(t *testing.T) { Bar string `json:"bar"` } assert.NoError(t, c.ShouldBindJSON(&obj)) - assert.Equal(t, obj.Bar, "foo") - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, w.Body.Len(), 0) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) } func TestContextShouldBindWithQuery(t *testing.T) { @@ -1311,7 +1336,7 @@ func TestContextGolangContext(t *testing.T) { assert.Nil(t, c.Value("foo")) c.Set("foo", "bar") - assert.Equal(t, c.Value("foo"), "bar") + assert.Equal(t, "bar", c.Value("foo")) assert.Nil(t, c.Value(1)) } @@ -1343,7 +1368,7 @@ func TestGetRequestHeaderValue(t *testing.T) { c.Request.Header.Set("Gin-Version", "1.0.0") assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version")) - assert.Equal(t, "", c.GetHeader("Connection")) + assert.Empty(t, c.GetHeader("Connection")) } func TestContextGetRawData(t *testing.T) { diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 00000000..81437f91 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +echo "mode: count" > coverage.out + +for d in $(go list ./... | grep -E 'gin$|binding$|render$'); do + go test -v -covermode=count -coverprofile=profile.out $d + if [ -f profile.out ]; then + cat profile.out | grep -v "mode:" >> coverage.out + rm profile.out + fi +done diff --git a/debug.go b/debug.go index 6df9d2e2..ab0fb8a1 100644 --- a/debug.go +++ b/debug.go @@ -19,7 +19,7 @@ func init() { } // IsDebugging returns true if the framework is running in debug mode. -// Use SetMode(gin.Release) to disable debug mode. +// Use SetMode(gin.ReleaseMode) to disable debug mode. func IsDebugging() bool { return ginMode == debugCode } diff --git a/deprecated_test.go b/deprecated_test.go new file mode 100644 index 00000000..7a875fe4 --- /dev/null +++ b/deprecated_test.go @@ -0,0 +1,31 @@ +// 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 gin + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin/binding" + "github.com/stretchr/testify/assert" +) + +func TestBindWith(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.BindWith(&obj, binding.Form)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} diff --git a/errors.go b/errors.go index 6f3c9868..dbfccd85 100644 --- a/errors.go +++ b/errors.go @@ -148,7 +148,7 @@ func (a errorMsgs) String() string { } var buffer bytes.Buffer for i, msg := range a { - fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) + fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) if msg.Meta != nil { fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) } diff --git a/examples/auto-tls/example1.go b/examples/auto-tls/example1/main.go similarity index 100% rename from examples/auto-tls/example1.go rename to examples/auto-tls/example1/main.go diff --git a/examples/auto-tls/example2.go b/examples/auto-tls/example2/main.go similarity index 100% rename from examples/auto-tls/example2.go rename to examples/auto-tls/example2/main.go diff --git a/examples/basic/main.go b/examples/basic/main.go index 984c06ab..473c6a09 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -6,7 +6,7 @@ import ( var DB = make(map[string]string) -func main() { +func setupRouter() *gin.Engine { // Disable Console Color // gin.DisableConsoleColor() r := gin.Default() @@ -53,6 +53,11 @@ func main() { } }) + return r +} + +func main() { + r := setupRouter() // Listen and Server in 0.0.0.0:8080 r.Run(":8080") } diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go new file mode 100644 index 00000000..61203d66 --- /dev/null +++ b/examples/basic/main_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} diff --git a/examples/custom-validation/server.go b/examples/custom-validation/server.go index 0b67ce10..31d449f0 100644 --- a/examples/custom-validation/server.go +++ b/examples/custom-validation/server.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" - validator "gopkg.in/go-playground/validator.v8" + "gopkg.in/go-playground/validator.v8" ) type Booking struct { diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go index 4bca3ae4..4afedcb5 100644 --- a/examples/realtime-advanced/stats.go +++ b/examples/realtime-advanced/stats.go @@ -29,8 +29,8 @@ func statsWorker() { "timestamp": uint64(time.Now().Unix()), "HeapInuse": stats.HeapInuse, "StackInuse": stats.StackInuse, - "Mallocs": (stats.Mallocs - lastMallocs), - "Frees": (stats.Frees - lastFrees), + "Mallocs": stats.Mallocs - lastMallocs, + "Frees": stats.Frees - lastFrees, "Inbound": uint64(messages.Get("inbound")), "Outbound": uint64(messages.Get("outbound")), "Connected": connectedUsers(), diff --git a/fixtures/testdata/cert.pem b/fixtures/testdata/cert.pem new file mode 100644 index 00000000..c1d3d632 --- /dev/null +++ b/fixtures/testdata/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9DCCAdygAwIBAgIQUNSK+OxWHYYFxHVJV0IlpDANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTExNjEyMDA0N1oXDTE4MTExNjEyMDA0 +N1owEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKmyj/YZpD59Bpy4w3qf6VzMw9uUBsWp+IP4kl7z5cmGHYUHn/YopTLH +vR23GAB12p6Km5QWzCBuJF4j61PJXHfg3/rjShZ77JcQ3kzxuy1iKDI+DNKN7Klz +rdjJ49QD0lczZHeBvvCL7JsJFKFjGy62rnStuW8LaIEdtjXT+GUZTxJh6G7yPYfD +MS1IsdMQGOdbGwNa+qogMuQPh0TzHw+C73myKrjY6pREijknMC/rnIMz9dLPt6Kl +xXy4br443dpY6dYGIhDuKhROT+vZ05HKasuuQUFhY7v/KoUpEZMB9rfUSzjQ5fak +eDUAMniXRcd+DmwvboG2TI6ixmuPK+ECAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0RBAgwBocE +fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAMXOLvj7BFsxdbcfRPBd0OFrH/8lI7vPV +LRcJ6r5iv0cnNvZXXbIOQLbg4clJAWjoE08nRm1KvNXhKdns0ELEV86YN2S6jThq +rIGrBqKtaJLB3M9BtDSiQ6SGPLYrWvmhj3Avi8PbSGy51bpGbqopd16j6LYU7Cp2 +TefMRlOAFtHojpCVon1CMpqcNxS0WNlQ3lUBSrw3HB0o12x++roja2ibF54tSHXB +KUuadoEzN+mMBwenEBychmAGzdiG4GQHRmhigh85+mtW6UMGiqyCZHs0EgE9FCLL +sRrsTI/VOzLz6lluygXkOsXrP+PP0SvmE3eylWjj9e2nj/u/Cy2YKg== +-----END CERTIFICATE----- diff --git a/fixtures/testdata/key.pem b/fixtures/testdata/key.pem new file mode 100644 index 00000000..c2a0181f --- /dev/null +++ b/fixtures/testdata/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAqbKP9hmkPn0GnLjDep/pXMzD25QGxan4g/iSXvPlyYYdhQef +9iilMse9HbcYAHXanoqblBbMIG4kXiPrU8lcd+Df+uNKFnvslxDeTPG7LWIoMj4M +0o3sqXOt2Mnj1APSVzNkd4G+8IvsmwkUoWMbLraudK25bwtogR22NdP4ZRlPEmHo +bvI9h8MxLUix0xAY51sbA1r6qiAy5A+HRPMfD4LvebIquNjqlESKOScwL+ucgzP1 +0s+3oqXFfLhuvjjd2ljp1gYiEO4qFE5P69nTkcpqy65BQWFju/8qhSkRkwH2t9RL +ONDl9qR4NQAyeJdFx34ObC9ugbZMjqLGa48r4QIDAQABAoIBAD5mhd+GMEo2KU9J +9b/Ku8I/HapJtW/L/7Fvn0tBPncrVQGM+zpGWfDhV95sbGwG6lwwNeNvuqIWPlNL +vAY0XkdKrrIQEDdSXH50WnpKzXxzwrou7QIj5Cmvevbjzl4xBZDBOilj0XWczmV4 +IljyG5XC4UXQeAaoWEZaSZ1jk8yAt2Zq1Hgg7HqhHsK/arWXBgax+4K5nV/s9gZx +yjKU9mXTIs7k/aNnZqwQKqcZF+l3mvbZttOaFwsP14H0I8OFWhnM9hie54Dejqxi +f4/llNxDqUs6lqJfP3qNxtORLcFe75M+Yl8v7g2hkjtLdZBakPzSTEx3TAK/UHgi +aM8DdxECgYEA3fmg/PI4EgUEj0C3SCmQXR/CnQLMUQgb54s0asp4akvp+M7YCcr1 +pQd3HFUpBwhBcJg5LeSe87vLupY7pHCKk56cl9WY6hse0b9sP/7DWJuGiO62m0E0 +vNjQ2jpG99oR2ROIHHeWsGCpGLmrRT/kY+vR3M+AOLZniXlOCw8k0aUCgYEAw7WL +XFWLxgZYQYilywqrQmfv1MBfaUCvykO6oWB+f6mmnihSFjecI+nDw/b3yXVYGEgy +0ebkuw0jP8suC8wBqX9WuXj+9nZNomJRssJyOMiEhDEqUiTztFPSp9pdruoakLTh +Wk1p9NralOqGPUmxpXlFKVmYRTUbluikVxDypI0CgYBn6sqEQH0hann0+o4TWWn9 +PrYkPUAbm1k8771tVTZERR/W3Dbldr/DL5iCihe39BR2urziEEqdvkglJNntJMar +TzDuIBADYQjvltb9qq4XGFBGYMLaMg+XbUVxNKEuvUdnwa4R7aZ9EfN34MwekkfA +w5Cu9/GGG1ajVEfGA6PwBQKBgA3o71jGs8KFXOx7e90sivOTU5Z5fc6LTHNB0Rf7 +NcJ5GmCPWRY/KZfb25AoE4B8GKDRMNt+X69zxZeZJ1KrU0rqxA02rlhyHB54gnoE +G/4xMkn6/JkOC0w70PMhMBtohC7YzFOQwQEoNPT0nkno3Pl33xSLS6lPlwBo1JVj +nPtZAoGACXNLXYkR5vexE+w6FGl59r4RQhu1XU8Mr5DIHeB7kXPN3RKbS201M+Tb +SB5jbu0iDV477XkzSNmhaksFf2wM9MT6CaE+8n3UU5tMa+MmBGgwYTp/i9HkqVh5 +jjpJifn1VWBINd4cpNzwCg9LXoo0tbtUPWwGzqVeyo/YE5GIHGo= +-----END RSA PRIVATE KEY----- diff --git a/gin.go b/gin.go index 8cfb1f13..4205eff0 100644 --- a/gin.go +++ b/gin.go @@ -14,8 +14,8 @@ import ( "github.com/gin-gonic/gin/render" ) -// Version is Framework's version. const ( + // Version is Framework's version. Version = "v1.2" defaultMultipartMemory = 32 << 20 // 32 MB ) @@ -49,16 +49,6 @@ type RoutesInfo []RouteInfo // Create an instance of Engine, by using New() or Default() type Engine struct { RouterGroup - delims render.Delims - secureJsonPrefix string - HTMLRender render.HTMLRender - FuncMap template.FuncMap - allNoRoute HandlersChain - allNoMethod HandlersChain - noRoute HandlersChain - noMethod HandlersChain - pool sync.Pool - trees methodTrees // Enables automatic redirection if the current route can't be matched but a // handler for the path with (without) the trailing slash exists. @@ -102,6 +92,17 @@ type Engine struct { // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm // method call. MaxMultipartMemory int64 + + delims render.Delims + secureJsonPrefix string + HTMLRender render.HTMLRender + FuncMap template.FuncMap + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain + pool sync.Pool + trees methodTrees } var _ IRouter = &Engine{} @@ -159,11 +160,14 @@ func (engine *Engine) Delims(left, right string) *Engine { return engine } +// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON. func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { engine.secureJsonPrefix = prefix return engine } +// LoadHTMLGlob loads HTML files identified by glob pattern +// and associates the result with HTML renderer. func (engine *Engine) LoadHTMLGlob(pattern string) { left := engine.delims.Left right := engine.delims.Right @@ -178,6 +182,8 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { engine.SetHTMLTemplate(templ) } +// LoadHTMLFiles loads a slice of HTML files +// and associates the result with HTML renderer. func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} @@ -188,6 +194,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { engine.SetHTMLTemplate(templ) } +// SetHTMLTemplate associate a template with HTML renderer. func (engine *Engine) SetHTMLTemplate(templ *template.Template) { if len(engine.trees) > 0 { debugPrintWARNINGSetHTMLTemplate() @@ -196,6 +203,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} } +// SetFuncMap sets the FuncMap used for template.FuncMap. func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.FuncMap = funcMap } @@ -403,8 +411,8 @@ func redirectTrailingSlash(c *Context) { code = 307 } - if len(path) > 1 && path[len(path)-1] == '/' { - req.URL.Path = path[:len(path)-1] + if length := len(path); length > 1 && path[length-1] == '/' { + req.URL.Path = path[:length-1] } else { req.URL.Path = path + "/" } diff --git a/gin_integration_test.go b/gin_integration_test.go index f45dd6c1..52f78842 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -94,7 +94,7 @@ func TestUnixSocket(t *testing.T) { c, err := net.Dial("unix", "/tmp/unix_unit_test") assert.NoError(t, err) - fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") + fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") scanner := bufio.NewScanner(c) var response string for scanner.Scan() { diff --git a/gin_test.go b/gin_test.go index bdf5a9a9..3ac60577 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,6 +5,7 @@ package gin import ( + "crypto/tls" "fmt" "html/template" "io/ioutil" @@ -21,9 +22,9 @@ func formatAsDate(t time.Time) string { return fmt.Sprintf("%d/%02d/%02d", year, month, day) } -func setupHTMLFiles(t *testing.T) func() { +func setupHTMLFiles(t *testing.T, mode string, tls bool) func() { go func() { - SetMode(TestMode) + SetMode(mode) router := New() router.Delims("{[{", "}]}") router.SetFuncMap(template.FuncMap{ @@ -38,16 +39,21 @@ func setupHTMLFiles(t *testing.T) func() { "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) - router.Run(":8888") + if tls { + // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` + router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem") + } else { + router.Run(":8888") + } }() t.Log("waiting 1 second for server startup") time.Sleep(1 * time.Second) return func() {} } -func setupHTMLGlob(t *testing.T) func() { +func setupHTMLGlob(t *testing.T, mode string, tls bool) func() { go func() { - SetMode(DebugMode) + SetMode(mode) router := New() router.Delims("{[{", "}]}") router.SetFuncMap(template.FuncMap{ @@ -62,16 +68,20 @@ func setupHTMLGlob(t *testing.T) func() { "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) - router.Run(":8888") + if tls { + // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1` + router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem") + } else { + router.Run(":8888") + } }() t.Log("waiting 1 second for server startup") time.Sleep(1 * time.Second) return func() {} } -//TODO func TestLoadHTMLGlob(t *testing.T) { - td := setupHTMLGlob(t) + td := setupHTMLGlob(t, DebugMode, false) res, err := http.Get("http://127.0.0.1:8888/test") if err != nil { fmt.Println(err) @@ -83,9 +93,55 @@ func TestLoadHTMLGlob(t *testing.T) { td() } +func TestLoadHTMLGlob2(t *testing.T) { + td := setupHTMLGlob(t, TestMode, false) + res, err := http.Get("http://127.0.0.1:8888/test") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "