diff --git a/.gitignore b/.gitignore
index 96c135f3..9f48f142 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 a378c1a7..bb0ed734 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 26babeb7..83cae29e 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 00000000..e28ee158
--- /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",
+ "/", "/",
+ "", "")
+}
+
+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 a1717886..6226c51b 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 1f38618a..a21192c0 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 dfd7381f..9a0f0b61 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 00000000..ba0c18c1
--- /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 70f62932..6140ab1e 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 0e459898..78e1cc05 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 1d2b42c6..dd844737 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 1e1e5228..12a931eb 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 819c2941..04b6f121 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 9b79deb5..00000000
--- 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 8b293edf..00000000
--- a/examples/pluggable_renderer/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- {{ title }}
-
-
-
-
- Hello {{ name }} !
-
-
diff --git a/gin.go b/gin.go
index 90f83c07..829a28d9 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 baac9764..36877be9 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 00000000..4ce33c19
--- /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 00000000..01bf03e5
--- /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 00000000..9bd1d933
--- /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 00000000..b4061227
--- /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 469388ab..766e4e9a 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 843238cc..760bae4e 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 00000000..1db84456
--- /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 00000000..50f2fc44
--- /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 ad7d1be7..30017d6b 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/")