diff --git a/.gitignore b/.gitignore index 96c135f..9f48f14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ Godeps/* !Godeps/Godeps.json +coverage.out +count.out diff --git a/auth_test.go b/auth_test.go index a378c1a..bb0ed73 100644 --- a/auth_test.go +++ b/auth_test.go @@ -73,6 +73,10 @@ func TestBasicAuthSearchCredential(t *testing.T) { user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) assert.Empty(t, user) assert.False(t, found) + + user, found = pairs.searchCredential("") + assert.Empty(t, user) + assert.False(t, found) } func TestBasicAuthAuthorizationHeader(t *testing.T) { diff --git a/binding/binding.go b/binding/binding.go index 26babeb..83cae29 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -50,3 +50,10 @@ func Default(method, contentType string) Binding { } } } + +func Validate(obj interface{}) error { + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/binding_test.go b/binding/binding_test.go new file mode 100644 index 0000000..e28ee15 --- /dev/null +++ b/binding/binding_test.go @@ -0,0 +1,79 @@ +// 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" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +type FooStruct struct { + Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"` +} + +func TestBindingDefault(t *testing.T) { + assert.Equal(t, Default("GET", ""), GETForm) + assert.Equal(t, Default("GET", MIMEJSON), GETForm) + + assert.Equal(t, Default("POST", MIMEJSON), JSON) + assert.Equal(t, Default("PUT", MIMEJSON), JSON) + + assert.Equal(t, Default("POST", MIMEXML), XML) + assert.Equal(t, Default("PUT", MIMEXML2), XML) + + assert.Equal(t, Default("POST", MIMEPOSTForm), POSTForm) + assert.Equal(t, Default("DELETE", MIMEPOSTForm), POSTForm) +} + +func TestBindingJSON(t *testing.T) { + testBinding(t, + JSON, "json", + "/", "/", + `{"foo": "bar"}`, `{"bar": "foo"}`) +} + +func TestBindingPOSTForm(t *testing.T) { + testBinding(t, + POSTForm, "post_form", + "/", "/", + "foo=bar", "bar=foo") +} + +func TestBindingGETForm(t *testing.T) { + testBinding(t, + GETForm, "get_form", + "/?foo=bar", "/?bar=foo", + "", "") +} + +func TestBindingXML(t *testing.T) { + testBinding(t, + XML, "xml", + "/", "/", + "bar", "foo") +} + +func testBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody(path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody(badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(path, body string) (req *http.Request) { + req, _ = http.NewRequest("POST", path, bytes.NewBufferString(body)) + return +} diff --git a/binding/get_form.go b/binding/get_form.go index a171788..6226c51 100644 --- a/binding/get_form.go +++ b/binding/get_form.go @@ -19,8 +19,5 @@ func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.Form); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/json.go b/binding/json.go index 1f38618..a21192c 100644 --- a/binding/json.go +++ b/binding/json.go @@ -21,8 +21,5 @@ func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/post_form.go b/binding/post_form.go index dfd7381..9a0f0b6 100644 --- a/binding/post_form.go +++ b/binding/post_form.go @@ -19,8 +19,5 @@ func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.PostForm); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/validate_test.go b/binding/validate_test.go new file mode 100644 index 0000000..ba0c18c --- /dev/null +++ b/binding/validate_test.go @@ -0,0 +1,53 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type struct1 struct { + Value float64 `binding:"required"` +} + +type struct2 struct { + RequiredValue string `binding:"required"` + Value float64 +} + +type struct3 struct { + Integer int + String string + BasicSlice []int + Boolean bool + + RequiredInteger int `binding:"required"` + RequiredString string `binding:"required"` + RequiredAnotherStruct struct1 `binding:"required"` + RequiredBasicSlice []int `binding:"required"` + RequiredComplexSlice []struct2 `binding:"required"` + RequiredBoolean bool `binding:"required"` +} + +func createStruct() struct3 { + return struct3{ + RequiredInteger: 2, + RequiredString: "hello", + RequiredAnotherStruct: struct1{1.5}, + RequiredBasicSlice: []int{1, 2, 3, 4}, + RequiredComplexSlice: []struct2{ + {RequiredValue: "A"}, + {RequiredValue: "B"}, + }, + RequiredBoolean: true, + } +} + +func TestValidateGoodObject(t *testing.T) { + test := createStruct() + assert.Nil(t, Validate(&test)) +} diff --git a/binding/xml.go b/binding/xml.go index 70f6293..6140ab1 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -20,8 +20,5 @@ func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/context.go b/context.go index 0e45989..78e1cc0 100644 --- a/context.go +++ b/context.go @@ -61,6 +61,9 @@ func (c *Context) reset() { func (c *Context) Copy() *Context { var cp Context = *c + cp.writermem.ResponseWriter = nil + cp.Writer = &cp.writermem + cp.Input.context = &cp cp.index = AbortIndex cp.handlers = nil return &cp @@ -161,7 +164,7 @@ func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } else { - panic("Key " + key + " does not exist") + panic("Key \"" + key + "\" does not exist") } } diff --git a/context_test.go b/context_test.go index 1d2b42c..dd84473 100644 --- a/context_test.go +++ b/context_test.go @@ -16,6 +16,11 @@ import ( "github.com/stretchr/testify/assert" ) +// Unit tes TODO +// func (c *Context) File(filepath string) { +// func (c *Context) Negotiate(code int, config Negotiate) { +// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { + func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { w = httptest.NewRecorder() r = New() @@ -64,6 +69,25 @@ func TestContextSetGet(t *testing.T) { assert.Panics(t, func() { c.MustGet("no_exist") }) } +func TestContextCopy(t *testing.T) { + c, _, _ := createTestContext() + c.index = 2 + c.Request, _ = http.NewRequest("POST", "/hola", nil) + c.handlers = []HandlerFunc{func(c *Context) {}} + c.Params = Params{Param{Key: "foo", Value: "bar"}} + c.Set("foo", "bar") + + cp := c.Copy() + assert.Nil(t, cp.handlers) + assert.Equal(t, cp.Request, c.Request) + assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) + assert.Equal(t, cp.Input.context, cp) + assert.Equal(t, cp.Keys, c.Keys) + assert.Equal(t, cp.Engine, c.Engine) + assert.Equal(t, cp.Params, c.Params) +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { @@ -79,7 +103,7 @@ func TestContextRenderJSON(t *testing.T) { // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { c, w, router := createTestContext() - templ, _ := template.New("t").Parse(`Hello {{.name}}`) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) c.HTML(201, "t", H{"name": "alexandernyquist"}) @@ -160,6 +184,7 @@ func TestContextNegotiationFormat(t *testing.T) { c, _, _ := createTestContext() 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) } @@ -203,13 +228,19 @@ func TestContextAbortWithStatus(t *testing.T) { func TestContextError(t *testing.T) { c, _, _ := createTestContext() + assert.Nil(t, c.LastError()) + assert.Empty(t, c.Errors.String()) + c.Error(errors.New("first error"), "some data") assert.Equal(t, c.LastError().Error(), "first error") assert.Len(t, c.Errors, 1) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n") c.Error(errors.New("second error"), "some data 2") assert.Equal(t, c.LastError().Error(), "second error") assert.Len(t, c.Errors, 2) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+ + "Error #02: second error\n Meta: some data 2\n") assert.Equal(t, c.Errors[0].Err, "first error") assert.Equal(t, c.Errors[0].Meta, "some data") diff --git a/debug_test.go b/debug_test.go index 1e1e522..12a931e 100644 --- a/debug_test.go +++ b/debug_test.go @@ -10,6 +10,10 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +// func debugPrint(format string, values ...interface{}) { + func TestIsDebugging(t *testing.T) { SetMode(DebugMode) assert.True(t, IsDebugging()) diff --git a/errors.go b/errors.go index 819c294..04b6f12 100644 --- a/errors.go +++ b/errors.go @@ -43,7 +43,7 @@ func (a errorMsgs) String() string { } var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + text := fmt.Sprintf("Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Err, msg.Meta) buffer.WriteString(text) } return buffer.String() diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go deleted file mode 100644 index 9b79deb..0000000 --- a/examples/pluggable_renderer/example_pongo2.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/flosch/pongo2" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/render" -) - -func main() { - router := gin.Default() - router.HTMLRender = newPongoRender() - - router.GET("/index", func(c *gin.Context) { - c.HTML(200, "index.html", gin.H{ - "title": "Gin meets pongo2 !", - "name": c.Input.Get("name"), - }) - }) - router.Run(":8080") -} - -type pongoRender struct { - cache map[string]*pongo2.Template -} - -func newPongoRender() *pongoRender { - return &pongoRender{map[string]*pongo2.Template{}} -} - -func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - file := data[0].(string) - ctx := data[1].(pongo2.Context) - var t *pongo2.Template - - if tmpl, ok := p.cache[file]; ok { - t = tmpl - } else { - tmpl, err := pongo2.FromFile(file) - if err != nil { - return err - } - p.cache[file] = tmpl - t = tmpl - } - render.WriteHeader(w, code, "text/html") - return t.ExecuteWriter(ctx, w) -} diff --git a/examples/pluggable_renderer/index.html b/examples/pluggable_renderer/index.html deleted file mode 100644 index 8b293ed..0000000 --- a/examples/pluggable_renderer/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - {{ title }} - - - - - Hello {{ name }} ! - - diff --git a/gin.go b/gin.go index 90f83c0..829a28d 100644 --- a/gin.go +++ b/gin.go @@ -144,6 +144,13 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { if path[0] != '/' { panic("path must begin with '/'") } + if method == "" { + panic("HTTP method can not be empty") + } + if len(handlers) == 0 { + panic("there must be at least one handler") + } + root := engine.trees[method] if root == nil { root = new(node) diff --git a/gin_test.go b/gin_test.go index baac976..36877be 100644 --- a/gin_test.go +++ b/gin_test.go @@ -10,6 +10,12 @@ import ( "github.com/stretchr/testify/assert" ) +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + func init() { SetMode(TestMode) } @@ -20,9 +26,9 @@ func TestCreateEngine(t *testing.T) { assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) - // TODO - // assert.Equal(t, router.router.NotFound, router.handle404) - // assert.Equal(t, router.router.MethodNotAllowed, router.handle405) + assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "/", []HandlerFunc{}) }) } func TestCreateDefaultRouter(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go new file mode 100644 index 0000000..4ce33c1 --- /dev/null +++ b/githubapi_test.go @@ -0,0 +1,344 @@ +// 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" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +type route struct { + method string + path string +} + +// http://developer.github.com/v3/ +var githubAPI = []route{ + // OAuth Authorizations + {"GET", "/authorizations"}, + {"GET", "/authorizations/:id"}, + {"POST", "/authorizations"}, + //{"PUT", "/authorizations/clients/:client_id"}, + //{"PATCH", "/authorizations/:id"}, + {"DELETE", "/authorizations/:id"}, + {"GET", "/applications/:client_id/tokens/:access_token"}, + {"DELETE", "/applications/:client_id/tokens"}, + {"DELETE", "/applications/:client_id/tokens/:access_token"}, + + // Activity + {"GET", "/events"}, + {"GET", "/repos/:owner/:repo/events"}, + {"GET", "/networks/:owner/:repo/events"}, + {"GET", "/orgs/:org/events"}, + {"GET", "/users/:user/received_events"}, + {"GET", "/users/:user/received_events/public"}, + {"GET", "/users/:user/events"}, + {"GET", "/users/:user/events/public"}, + {"GET", "/users/:user/events/orgs/:org"}, + {"GET", "/feeds"}, + {"GET", "/notifications"}, + {"GET", "/repos/:owner/:repo/notifications"}, + {"PUT", "/notifications"}, + {"PUT", "/repos/:owner/:repo/notifications"}, + {"GET", "/notifications/threads/:id"}, + //{"PATCH", "/notifications/threads/:id"}, + {"GET", "/notifications/threads/:id/subscription"}, + {"PUT", "/notifications/threads/:id/subscription"}, + {"DELETE", "/notifications/threads/:id/subscription"}, + {"GET", "/repos/:owner/:repo/stargazers"}, + {"GET", "/users/:user/starred"}, + {"GET", "/user/starred"}, + {"GET", "/user/starred/:owner/:repo"}, + {"PUT", "/user/starred/:owner/:repo"}, + {"DELETE", "/user/starred/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/subscribers"}, + {"GET", "/users/:user/subscriptions"}, + {"GET", "/user/subscriptions"}, + {"GET", "/repos/:owner/:repo/subscription"}, + {"PUT", "/repos/:owner/:repo/subscription"}, + {"DELETE", "/repos/:owner/:repo/subscription"}, + {"GET", "/user/subscriptions/:owner/:repo"}, + {"PUT", "/user/subscriptions/:owner/:repo"}, + {"DELETE", "/user/subscriptions/:owner/:repo"}, + + // Gists + {"GET", "/users/:user/gists"}, + {"GET", "/gists"}, + //{"GET", "/gists/public"}, + //{"GET", "/gists/starred"}, + {"GET", "/gists/:id"}, + {"POST", "/gists"}, + //{"PATCH", "/gists/:id"}, + {"PUT", "/gists/:id/star"}, + {"DELETE", "/gists/:id/star"}, + {"GET", "/gists/:id/star"}, + {"POST", "/gists/:id/forks"}, + {"DELETE", "/gists/:id"}, + + // Git Data + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, + {"POST", "/repos/:owner/:repo/git/blobs"}, + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, + {"POST", "/repos/:owner/:repo/git/commits"}, + //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/refs"}, + {"POST", "/repos/:owner/:repo/git/refs"}, + //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, + //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, + {"POST", "/repos/:owner/:repo/git/tags"}, + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, + {"POST", "/repos/:owner/:repo/git/trees"}, + + // Issues + {"GET", "/issues"}, + {"GET", "/user/issues"}, + {"GET", "/orgs/:org/issues"}, + {"GET", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/issues/:number"}, + {"POST", "/repos/:owner/:repo/issues"}, + //{"PATCH", "/repos/:owner/:repo/issues/:number"}, + {"GET", "/repos/:owner/:repo/assignees"}, + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, + //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, + {"GET", "/repos/:owner/:repo/issues/:number/events"}, + //{"GET", "/repos/:owner/:repo/issues/events"}, + //{"GET", "/repos/:owner/:repo/issues/events/:id"}, + {"GET", "/repos/:owner/:repo/labels"}, + {"GET", "/repos/:owner/:repo/labels/:name"}, + {"POST", "/repos/:owner/:repo/labels"}, + //{"PATCH", "/repos/:owner/:repo/labels/:name"}, + {"DELETE", "/repos/:owner/:repo/labels/:name"}, + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones"}, + {"GET", "/repos/:owner/:repo/milestones/:number"}, + {"POST", "/repos/:owner/:repo/milestones"}, + //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, + + // Miscellaneous + {"GET", "/emojis"}, + {"GET", "/gitignore/templates"}, + {"GET", "/gitignore/templates/:name"}, + {"POST", "/markdown"}, + {"POST", "/markdown/raw"}, + {"GET", "/meta"}, + {"GET", "/rate_limit"}, + + // Organizations + {"GET", "/users/:user/orgs"}, + {"GET", "/user/orgs"}, + {"GET", "/orgs/:org"}, + //{"PATCH", "/orgs/:org"}, + {"GET", "/orgs/:org/members"}, + {"GET", "/orgs/:org/members/:user"}, + {"DELETE", "/orgs/:org/members/:user"}, + {"GET", "/orgs/:org/public_members"}, + {"GET", "/orgs/:org/public_members/:user"}, + {"PUT", "/orgs/:org/public_members/:user"}, + {"DELETE", "/orgs/:org/public_members/:user"}, + {"GET", "/orgs/:org/teams"}, + {"GET", "/teams/:id"}, + {"POST", "/orgs/:org/teams"}, + //{"PATCH", "/teams/:id"}, + {"DELETE", "/teams/:id"}, + {"GET", "/teams/:id/members"}, + {"GET", "/teams/:id/members/:user"}, + {"PUT", "/teams/:id/members/:user"}, + {"DELETE", "/teams/:id/members/:user"}, + {"GET", "/teams/:id/repos"}, + {"GET", "/teams/:id/repos/:owner/:repo"}, + {"PUT", "/teams/:id/repos/:owner/:repo"}, + {"DELETE", "/teams/:id/repos/:owner/:repo"}, + {"GET", "/user/teams"}, + + // Pull Requests + {"GET", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number"}, + {"POST", "/repos/:owner/:repo/pulls"}, + //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, + //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, + + // Repositories + {"GET", "/user/repos"}, + {"GET", "/users/:user/repos"}, + {"GET", "/orgs/:org/repos"}, + {"GET", "/repositories"}, + {"POST", "/user/repos"}, + {"POST", "/orgs/:org/repos"}, + {"GET", "/repos/:owner/:repo"}, + //{"PATCH", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/contributors"}, + {"GET", "/repos/:owner/:repo/languages"}, + {"GET", "/repos/:owner/:repo/teams"}, + {"GET", "/repos/:owner/:repo/tags"}, + {"GET", "/repos/:owner/:repo/branches"}, + {"GET", "/repos/:owner/:repo/branches/:branch"}, + {"DELETE", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/collaborators"}, + {"GET", "/repos/:owner/:repo/collaborators/:user"}, + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, + {"GET", "/repos/:owner/:repo/comments"}, + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, + {"GET", "/repos/:owner/:repo/comments/:id"}, + //{"PATCH", "/repos/:owner/:repo/comments/:id"}, + {"DELETE", "/repos/:owner/:repo/comments/:id"}, + {"GET", "/repos/:owner/:repo/commits"}, + {"GET", "/repos/:owner/:repo/commits/:sha"}, + {"GET", "/repos/:owner/:repo/readme"}, + //{"GET", "/repos/:owner/:repo/contents/*path"}, + //{"PUT", "/repos/:owner/:repo/contents/*path"}, + //{"DELETE", "/repos/:owner/:repo/contents/*path"}, + //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, + {"GET", "/repos/:owner/:repo/keys"}, + {"GET", "/repos/:owner/:repo/keys/:id"}, + {"POST", "/repos/:owner/:repo/keys"}, + //{"PATCH", "/repos/:owner/:repo/keys/:id"}, + {"DELETE", "/repos/:owner/:repo/keys/:id"}, + {"GET", "/repos/:owner/:repo/downloads"}, + {"GET", "/repos/:owner/:repo/downloads/:id"}, + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, + {"GET", "/repos/:owner/:repo/forks"}, + {"POST", "/repos/:owner/:repo/forks"}, + {"GET", "/repos/:owner/:repo/hooks"}, + {"GET", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks"}, + //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/merges"}, + {"GET", "/repos/:owner/:repo/releases"}, + {"GET", "/repos/:owner/:repo/releases/:id"}, + {"POST", "/repos/:owner/:repo/releases"}, + //{"PATCH", "/repos/:owner/:repo/releases/:id"}, + {"DELETE", "/repos/:owner/:repo/releases/:id"}, + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, + {"GET", "/repos/:owner/:repo/stats/contributors"}, + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, + {"GET", "/repos/:owner/:repo/stats/participation"}, + {"GET", "/repos/:owner/:repo/stats/punch_card"}, + {"GET", "/repos/:owner/:repo/statuses/:ref"}, + {"POST", "/repos/:owner/:repo/statuses/:ref"}, + + // Search + {"GET", "/search/repositories"}, + {"GET", "/search/code"}, + {"GET", "/search/issues"}, + {"GET", "/search/users"}, + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, + {"GET", "/legacy/repos/search/:keyword"}, + {"GET", "/legacy/user/search/:keyword"}, + {"GET", "/legacy/user/email/:email"}, + + // Users + {"GET", "/users/:user"}, + {"GET", "/user"}, + //{"PATCH", "/user"}, + {"GET", "/users"}, + {"GET", "/user/emails"}, + {"POST", "/user/emails"}, + {"DELETE", "/user/emails"}, + {"GET", "/users/:user/followers"}, + {"GET", "/user/followers"}, + {"GET", "/users/:user/following"}, + {"GET", "/user/following"}, + {"GET", "/user/following/:user"}, + {"GET", "/users/:user/following/:target_user"}, + {"PUT", "/user/following/:user"}, + {"DELETE", "/user/following/:user"}, + {"GET", "/users/:user/keys"}, + {"GET", "/user/keys"}, + {"GET", "/user/keys/:id"}, + {"POST", "/user/keys"}, + //{"PATCH", "/user/keys/:id"}, + {"DELETE", "/user/keys/:id"}, +} + +func TestGithubAPI(t *testing.T) { + router := New() + + for _, route := range githubAPI { + router.Handle(route.method, route.path, []HandlerFunc{func(c *Context) { + output := H{"status": "good"} + for _, param := range c.Params { + output[param.Key] = param.Value + } + c.JSON(200, output) + }}) + } + + for _, route := range githubAPI { + path, values := exampleFromPath(route.path) + w := performRequest(router, route.method, path) + + // TEST + assert.Contains(t, w.Body.String(), "\"status\":\"good\"") + for _, value := range values { + str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value) + assert.Contains(t, w.Body.String(), str) + } + } +} + +func exampleFromPath(path string) (string, Params) { + output := new(bytes.Buffer) + params := make(Params, 0, 6) + start := -1 + for i, c := range path { + if c == ':' { + start = i + 1 + } + if start >= 0 { + if c == '/' { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:i], + Value: value, + }) + output.WriteString(value) + output.WriteRune(c) + start = -1 + } + } else { + output.WriteRune(c) + } + } + if start >= 0 { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:len(path)], + Value: value, + }) + output.WriteString(value) + } + + return output.String(), params +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..01bf03e --- /dev/null +++ b/logger_test.go @@ -0,0 +1,35 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" +) + +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + +func init() { + SetMode(TestMode) +} + +func TestLogger(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithFile(buffer)) + router.GET("/example", func(c *Context) {}) + + performRequest(router, "GET", "/example") + + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..9bd1d93 --- /dev/null +++ b/path_test.go @@ -0,0 +1,88 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + assert.Equal(t, CleanPath(test.path), test.result) + assert.Equal(t, CleanPath(test.result), test.result) + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + assert.Equal(t, allocs, 0) + } +} diff --git a/render/render_test.go b/render/render_test.go new file mode 100644 index 0000000..b406122 --- /dev/null +++ b/render/render_test.go @@ -0,0 +1,79 @@ +// 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 render + +import ( + "html/template" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderJSON(t *testing.T) { + w := httptest.NewRecorder() + err := JSON.Render(w, 201, map[string]interface{}{ + "foo": "bar", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderIndentedJSON(t *testing.T) { + w := httptest.NewRecorder() + err := IndentedJSON.Render(w, 202, map[string]interface{}{ + "foo": "bar", + "bar": "foo", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 202) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderPlain(t *testing.T) { + w := httptest.NewRecorder() + err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") +} + +func TestRenderPlainHTML(t *testing.T) { + w := httptest.NewRecorder() + err := HTMLPlain.Render(w, 401, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderHTMLTemplate(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + htmlRender := HTMLRender{Template: templ} + err := htmlRender.Render(w, 402, "t", map[string]interface{}{ + "name": "alexandernyquist", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 402) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderJoinStrings(t *testing.T) { + assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") + assert.Equal(t, joinStrings("a", "", "c"), "ac") + assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") + +} diff --git a/response_writer_test.go b/response_writer_test.go index 469388a..766e4e9 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -12,6 +12,11 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +// func (w *responseWriter) CloseNotify() <-chan bool { +// func (w *responseWriter) Flush() { + var _ ResponseWriter = &responseWriter{} var _ http.ResponseWriter = &responseWriter{} var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) diff --git a/routergroup.go b/routergroup.go index 843238c..760bae4 100644 --- a/routergroup.go +++ b/routergroup.go @@ -119,9 +119,10 @@ func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*C func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, 0, finalSize) - mergedHandlers = append(mergedHandlers, group.Handlers...) - return append(mergedHandlers, handlers...) + mergedHandlers := make([]HandlerFunc, finalSize) + copy(mergedHandlers, group.Handlers) + copy(mergedHandlers[len(group.Handlers):], handlers) + return mergedHandlers } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { diff --git a/routergroup_test.go b/routergroup_test.go new file mode 100644 index 0000000..1db8445 --- /dev/null +++ b/routergroup_test.go @@ -0,0 +1,98 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestRouterGroupBasic(t *testing.T) { + router := New() + group := router.Group("/hola", func(c *Context) {}) + group.Use(func(c *Context) {}) + + assert.Len(t, group.Handlers, 2) + assert.Equal(t, group.absolutePath, "/hola") + assert.Equal(t, group.engine, router) + + group2 := group.Group("manu") + group2.Use(func(c *Context) {}, func(c *Context) {}) + + assert.Len(t, group2.Handlers, 4) + assert.Equal(t, group2.absolutePath, "/hola/manu") + assert.Equal(t, group2.engine, router) +} + +func TestRouterGroupBasicHandle(t *testing.T) { + performRequestInGroup(t, "GET") + performRequestInGroup(t, "POST") + performRequestInGroup(t, "PUT") + performRequestInGroup(t, "PATCH") + performRequestInGroup(t, "DELETE") + performRequestInGroup(t, "HEAD") + performRequestInGroup(t, "OPTIONS") + performRequestInGroup(t, "LINK") + performRequestInGroup(t, "UNLINK") + +} + +func performRequestInGroup(t *testing.T, method string) { + router := New() + v1 := router.Group("v1", func(c *Context) {}) + assert.Equal(t, v1.absolutePath, "/v1") + + login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) + assert.Equal(t, login.absolutePath, "/v1/login/") + + handler := func(c *Context) { + c.String(400, "the method was %s and index %d", c.Request.Method, c.index) + } + + switch method { + case "GET": + v1.GET("/test", handler) + login.GET("/test", handler) + case "POST": + v1.POST("/test", handler) + login.POST("/test", handler) + case "PUT": + v1.PUT("/test", handler) + login.PUT("/test", handler) + case "PATCH": + v1.PATCH("/test", handler) + login.PATCH("/test", handler) + case "DELETE": + v1.DELETE("/test", handler) + login.DELETE("/test", handler) + case "HEAD": + v1.HEAD("/test", handler) + login.HEAD("/test", handler) + case "OPTIONS": + v1.OPTIONS("/test", handler) + login.OPTIONS("/test", handler) + case "LINK": + v1.LINK("/test", handler) + login.LINK("/test", handler) + case "UNLINK": + v1.UNLINK("/test", handler) + login.UNLINK("/test", handler) + default: + panic("unknown method") + } + + w := performRequest(router, method, "/v1/login/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") + + w = performRequest(router, method, "/v1/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..50f2fc4 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,608 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func printChildren(n *node, prefix string) { + fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType) + for l := len(n.path); l > 0; l-- { + prefix += " " + } + for _, child := range n.children { + printChildren(child, prefix) + } +} + +// Used as a workaround since we can't compare functions or their adresses +var fakeHandlerValue string + +func fakeHandler(val string) []HandlerFunc { + return []HandlerFunc{func(c *Context) { + fakeHandlerValue = val + }} +} + +type testRequests []struct { + path string + nilHandler bool + route string + ps Params +} + +func checkRequests(t *testing.T, tree *node, requests testRequests) { + for _, request := range requests { + handler, ps, _ := tree.getValue(request.path, nil) + + if handler == nil { + if !request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + } + } else if request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) + } else { + handler[0](nil) + if fakeHandlerValue != request.route { + t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) + } + } + + if !reflect.DeepEqual(ps, request.ps) { + t.Errorf("Params mismatch for route '%s'", request.path) + } + } +} + +func checkPriorities(t *testing.T, n *node) uint32 { + var prio uint32 + for i := range n.children { + prio += checkPriorities(t, n.children[i]) + } + + if n.handlers != nil { + prio++ + } + + if n.priority != prio { + t.Errorf( + "priority mismatch for node '%s': is %d, should be %d", + n.path, n.priority, prio, + ) + } + + return prio +} + +func checkMaxParams(t *testing.T, n *node) uint8 { + var maxParams uint8 + for i := range n.children { + params := checkMaxParams(t, n.children[i]) + if params > maxParams { + maxParams = params + } + } + if n.nType != static && !n.wildChild { + maxParams++ + } + + if n.maxParams != maxParams { + t.Errorf( + "maxParams mismatch for node '%s': is %d, should be %d", + n.path, n.maxParams, maxParams, + ) + } + + return maxParams +} + +func TestCountParams(t *testing.T) { + if countParams("/path/:param1/static/*catch-all") != 2 { + t.Fail() + } + if countParams(strings.Repeat("/:param", 256)) != 255 { + t.Fail() + } +} + +func TestTreeAddAndGet(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/contact", + "/co", + "/c", + "/a", + "/ab", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/α", + "/β", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/a", false, "/a", nil}, + {"/", true, "", nil}, + {"/hi", false, "/hi", nil}, + {"/contact", false, "/contact", nil}, + {"/co", false, "/co", nil}, + {"/con", true, "", nil}, // key mismatch + {"/cona", true, "", nil}, // key mismatch + {"/no", true, "", nil}, // no matching child + {"/ab", false, "/ab", nil}, + {"/α", false, "/α", nil}, + {"/β", false, "/β", nil}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func TestTreeWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/info/:user/public", + "/info/:user/project/:project", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, + {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, + {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/", false, "/search/", nil}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, + {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, + {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func catchPanic(testFunc func()) (recv interface{}) { + defer func() { + recv = recover() + }() + + testFunc() + return +} + +type testRoute struct { + path string + conflict bool +} + +func testRoutes(t *testing.T, routes []testRoute) { + tree := &node{} + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route.path, nil) + }) + + if route.conflict { + if recv == nil { + t.Errorf("no panic for conflicting route '%s'", route.path) + } + } else if recv != nil { + t.Errorf("unexpected panic for route '%s': %v", route.path, recv) + } + } + + //printChildren(tree, "") +} + +func TestTreeWildcardConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/:tool/:sub", false}, + {"/cmd/vet", true}, + {"/src/*filepath", false}, + {"/src/*filepathx", true}, + {"/src/", true}, + {"/src1/", false}, + {"/src1/*filepath", true}, + {"/src2*filepath", true}, + {"/search/:query", false}, + {"/search/invalid", true}, + {"/user_:name", false}, + {"/user_x", true}, + {"/user_:name", false}, + {"/id:id", false}, + {"/id/:id", true}, + } + testRoutes(t, routes) +} + +func TestTreeChildConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/vet", false}, + {"/cmd/:tool/:sub", true}, + {"/src/AUTHORS", false}, + {"/src/*filepath", true}, + {"/user_x", false}, + {"/user_:name", true}, + {"/id/:id", false}, + {"/id:id", true}, + {"/:id", true}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDupliatePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/doc/", + "/src/*filepath", + "/search/:query", + "/user_:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + + // Add again + recv = catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting duplicate route '%s", route) + } + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/doc/", false, "/doc/", nil}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + }) +} + +func TestEmptyWildcardName(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/user:", + "/user:/", + "/cmd/:/", + "/src/*", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) + } + } +} + +func TestTreeCatchAllConflict(t *testing.T) { + routes := []testRoute{ + {"/src/*filepath/x", true}, + {"/src2/", false}, + {"/src2/*filepath/x", true}, + } + testRoutes(t, routes) +} + +func TestTreeCatchAllConflictRoot(t *testing.T) { + routes := []testRoute{ + {"/", false}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDoubleWildcard(t *testing.T) { + const panicMsg = "only one wildcard per path segment is allowed" + + routes := [...]string{ + "/:foo:bar", + "/:foo:bar/", + "/:foo*bar", + } + + for _, route := range routes { + tree := &node{} + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + + if rs, ok := recv.(string); !ok || rs != panicMsg { + t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) + } + } +} + +/*func TestTreeDuplicateWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/:id/:name/:id", + } + for _, route := range routes { + ... + } +}*/ + +func TestTreeTrailingSlashRedirect(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/no/a", + "/no/b", + "/api/hello/:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + //printChildren(tree, "") + + tsrRoutes := [...]string{ + "/hi/", + "/b", + "/search/gopher/", + "/cmd/vet", + "/src", + "/x/", + "/y", + "/0/go/", + "/1/go", + "/a", + "/doc/", + } + for _, route := range tsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for TSR route '%s", route) + } else if !tsr { + t.Errorf("expected TSR recommendation for route '%s'", route) + } + } + + noTsrRoutes := [...]string{ + "/", + "/no", + "/no/", + "/_", + "/_/", + "/api/world/abc", + } + for _, route := range noTsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for No-TSR route '%s", route) + } else if tsr { + t.Errorf("expected no TSR recommendation for route '%s'", route) + } + } +} + +func TestTreeFindCaseInsensitivePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/ABC/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/doc/go/away", + "/no/a", + "/no/b", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Check out == in for all registered routes + // With fixTrailingSlash = true + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, true) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + // With fixTrailingSlash = false + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, false) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + + tests := []struct { + in string + out string + found bool + slash bool + }{ + {"/HI", "/hi", true, false}, + {"/HI/", "/hi", true, true}, + {"/B", "/b/", true, true}, + {"/B/", "/b/", true, false}, + {"/abc", "/ABC/", true, true}, + {"/abc/", "/ABC/", true, false}, + {"/aBc", "/ABC/", true, true}, + {"/aBc/", "/ABC/", true, false}, + {"/abC", "/ABC/", true, true}, + {"/abC/", "/ABC/", true, false}, + {"/SEARCH/QUERY", "/search/QUERY", true, false}, + {"/SEARCH/QUERY/", "/search/QUERY", true, true}, + {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, + {"/CMD/TOOL", "/cmd/TOOL/", true, true}, + {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, + {"/x/Y", "/x/y", true, false}, + {"/x/Y/", "/x/y", true, true}, + {"/X/y", "/x/y", true, false}, + {"/X/y/", "/x/y", true, true}, + {"/X/Y", "/x/y", true, false}, + {"/X/Y/", "/x/y", true, true}, + {"/Y/", "/y/", true, false}, + {"/Y", "/y/", true, true}, + {"/Y/z", "/y/z", true, false}, + {"/Y/z/", "/y/z", true, true}, + {"/Y/Z", "/y/z", true, false}, + {"/Y/Z/", "/y/z", true, true}, + {"/y/Z", "/y/z", true, false}, + {"/y/Z/", "/y/z", true, true}, + {"/Aa", "/aa", true, false}, + {"/Aa/", "/aa", true, true}, + {"/AA", "/aa", true, false}, + {"/AA/", "/aa", true, true}, + {"/aA", "/aa", true, false}, + {"/aA/", "/aa", true, true}, + {"/A/", "/a/", true, false}, + {"/A", "/a/", true, true}, + {"/DOC", "/doc", true, false}, + {"/DOC/", "/doc", true, true}, + {"/NO", "", false, true}, + {"/DOC/GO", "", false, true}, + } + // With fixTrailingSlash = true + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, true) + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + // With fixTrailingSlash = false + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, false) + if test.slash { + if found { // test needs a trailingSlash fix. It must not be found! + t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) + } + } else { + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + } +} + +func TestTreeInvalidNodeType(t *testing.T) { + tree := &node{} + tree.addRoute("/", fakeHandler("/")) + tree.addRoute("/:page", fakeHandler("/:page")) + + // set invalid node type + tree.children[0].nType = 42 + + // normal lookup + recv := catchPanic(func() { + tree.getValue("/test", nil) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } + + // case-insensitive lookup + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/test", true) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } +} diff --git a/utils_test.go b/utils_test.go index ad7d1be..30017d6 100644 --- a/utils_test.go +++ b/utils_test.go @@ -45,7 +45,17 @@ func TestFilterFlags(t *testing.T) { assert.Equal(t, result, "text/html") } +func TestFunctionName(t *testing.T) { + assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction") +} + +func somefunction() { + +} + func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("", ""), "") + assert.Equal(t, joinPaths("", "/"), "/") assert.Equal(t, joinPaths("/a", ""), "/a") assert.Equal(t, joinPaths("/a/", ""), "/a/") assert.Equal(t, joinPaths("/a/", "/"), "/a/")