package echo import ( "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" ) var ( api = []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 TestRouterStatic(t *testing.T) { e := New() r := e.router path := "/folders/a/files/echo.gif" r.Add(GET, path, func(c Context) error { c.Set("path", path) return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, path, c) c.handler(c) assert.Equal(t, path, c.Get("path")) } func TestRouterParam(t *testing.T) { e := New() r := e.router r.Add(GET, "/users/:id", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/1", c) assert.Equal(t, "1", c.Param("id")) } func TestRouterTwoParam(t *testing.T) { e := New() r := e.router r.Add(GET, "/users/:uid/files/:fid", func(Context) error { return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/1/files/1", c) assert.Equal(t, "1", c.Param("uid")) assert.Equal(t, "1", c.Param("fid")) } // Issue #378 func TestRouterParamWithSlash(t *testing.T) { e := New() r := e.router r.Add(GET, "/a/:b/c/d/:e", func(c Context) error { return nil }) r.Add(GET, "/a/:b/c/:d/:f", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) assert.NotPanics(t, func() { r.Find(GET, "/a/1/c/d/2/3", c) }) } func TestRouterMatchAny(t *testing.T) { e := New() r := e.router // Routes r.Add(GET, "/", func(Context) error { return nil }) r.Add(GET, "/*", func(Context) error { return nil }) r.Add(GET, "/users/*", func(Context) error { return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, "/", c) assert.Equal(t, "", c.Param("_*")) r.Find(GET, "/download", c) assert.Equal(t, "download", c.Param("*")) r.Find(GET, "/users/joe", c) assert.Equal(t, "joe", c.Param("*")) } func TestRouterMicroParam(t *testing.T) { e := New() r := e.router r.Add(GET, "/:a/:b/:c", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, "/1/2/3", c) assert.Equal(t, "1", c.Param("a")) assert.Equal(t, "2", c.Param("b")) assert.Equal(t, "3", c.Param("c")) } func TestRouterMixParamMatchAny(t *testing.T) { e := New() r := e.router // Route r.Add(GET, "/users/:id/*", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/joe/comments", c) c.handler(c) assert.Equal(t, "joe", c.Param("id")) } func TestRouterMultiRoute(t *testing.T) { e := New() r := e.router // Routes r.Add(GET, "/users", func(c Context) error { c.Set("path", "/users") return nil }) r.Add(GET, "/users/:id", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) c.handler(c) assert.Equal(t, "/users", c.Get("path")) // Route > /users/:id r.Find(GET, "/users/1", c) assert.Equal(t, "1", c.Param("id")) // Route > /user c = e.NewContext(nil, nil).(*context) r.Find(GET, "/user", c) he := c.handler(c).(*HTTPError) assert.Equal(t, http.StatusNotFound, he.Code) } func TestRouterPriority(t *testing.T) { e := New() r := e.router // Routes r.Add(GET, "/users", func(c Context) error { c.Set("a", 1) return nil }) r.Add(GET, "/users/new", func(c Context) error { c.Set("b", 2) return nil }) r.Add(GET, "/users/:id", func(c Context) error { c.Set("c", 3) return nil }) r.Add(GET, "/users/dew", func(c Context) error { c.Set("d", 4) return nil }) r.Add(GET, "/users/:id/files", func(c Context) error { c.Set("e", 5) return nil }) r.Add(GET, "/users/newsee", func(c Context) error { c.Set("f", 6) return nil }) r.Add(GET, "/users/*", func(c Context) error { c.Set("g", 7) return nil }) c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) c.handler(c) assert.Equal(t, 1, c.Get("a")) // Route > /users/new r.Find(GET, "/users/new", c) c.handler(c) assert.Equal(t, 2, c.Get("b")) // Route > /users/:id r.Find(GET, "/users/1", c) c.handler(c) assert.Equal(t, 3, c.Get("c")) // Route > /users/dew r.Find(GET, "/users/dew", c) c.handler(c) assert.Equal(t, 4, c.Get("d")) // Route > /users/:id/files r.Find(GET, "/users/1/files", c) c.handler(c) assert.Equal(t, 5, c.Get("e")) // Route > /users/:id r.Find(GET, "/users/news", c) c.handler(c) assert.Equal(t, 3, c.Get("c")) // Route > /users/* r.Find(GET, "/users/joe/books", c) c.handler(c) assert.Equal(t, 7, c.Get("g")) assert.Equal(t, "joe/books", c.Param("*")) } // Issue #372 func TestRouterPriorityNotFound(t *testing.T) { e := New() r := e.router c := e.NewContext(nil, nil).(*context) // Add r.Add(GET, "/a/foo", func(c Context) error { c.Set("a", 1) return nil }) r.Add(GET, "/a/bar", func(c Context) error { c.Set("b", 2) return nil }) // Find r.Find(GET, "/a/foo", c) c.handler(c) assert.Equal(t, 1, c.Get("a")) r.Find(GET, "/a/bar", c) c.handler(c) assert.Equal(t, 2, c.Get("b")) c = e.NewContext(nil, nil).(*context) r.Find(GET, "/abc/def", c) he := c.handler(c).(*HTTPError) assert.Equal(t, http.StatusNotFound, he.Code) } func TestRouterParamNames(t *testing.T) { e := New() r := e.router // Routes r.Add(GET, "/users", func(c Context) error { c.Set("path", "/users") return nil }) r.Add(GET, "/users/:id", func(c Context) error { return nil }) r.Add(GET, "/users/:uid/files/:fid", func(c Context) error { return nil }) c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) c.handler(c) assert.Equal(t, "/users", c.Get("path")) // Route > /users/:id r.Find(GET, "/users/1", c) assert.Equal(t, "id", c.pnames[0]) assert.Equal(t, "1", c.Param("id")) // Route > /users/:uid/files/:fid r.Find(GET, "/users/1/files/1", c) assert.Equal(t, "uid", c.pnames[0]) assert.Equal(t, "1", c.Param("uid")) assert.Equal(t, "fid", c.pnames[1]) assert.Equal(t, "1", c.Param("fid")) } // Issue #623 func TestRouterStaticDynamicConflict(t *testing.T) { e := New() r := e.router c := e.NewContext(nil, nil) r.Add(GET, "/dictionary/skills", func(c Context) error { c.Set("a", 1) return nil }) r.Add(GET, "/dictionary/:name", func(c Context) error { c.Set("b", 2) return nil }) r.Add(GET, "/server", func(c Context) error { c.Set("c", 3) return nil }) r.Find(GET, "/dictionary/skills", c) c.Handler()(c) assert.Equal(t, 1, c.Get("a")) c = e.NewContext(nil, nil) r.Find(GET, "/dictionary/type", c) c.Handler()(c) assert.Equal(t, 2, c.Get("b")) c = e.NewContext(nil, nil) r.Find(GET, "/server", c) c.Handler()(c) assert.Equal(t, 3, c.Get("c")) } func TestRouterAPI(t *testing.T) { e := New() r := e.router for _, route := range api { r.Add(route.Method, route.Path, func(c Context) error { return nil }) } c := e.NewContext(nil, nil).(*context) for _, route := range api { r.Find(route.Method, route.Path, c) for i, n := range c.pnames { if assert.NotEmpty(t, n) { assert.Equal(t, n, c.pnames[i]) } } } } func BenchmarkRouterGitHubAPI(b *testing.B) { e := New() r := e.router b.ReportAllocs() // Add routes for _, route := range api { r.Add(route.Method, route.Path, func(c Context) error { return nil }) } // Find routes for i := 0; i < b.N; i++ { for _, route := range api { // c := e.pool.Get().(*context) c := e.AcquireContext() r.Find(route.Method, route.Path, c) // router.Find(r.Method, r.Path, c) e.ReleaseContext(c) // e.pool.Put(c) } } } func (n *node) printTree(pfx string, tail bool) { p := prefix(tail, pfx, "└── ", "├── ") fmt.Printf("%s%s, %p: type=%d, parent=%p, handler=%v\n", p, n.prefix, n, n.kind, n.parent, n.methodHandler) children := n.children l := len(children) p = prefix(tail, pfx, " ", "│ ") for i := 0; i < l-1; i++ { children[i].printTree(p, false) } if l > 0 { children[l-1].printTree(p, true) } } func prefix(tail bool, p, on, off string) string { if tail { return fmt.Sprintf("%s%s", p, on) } return fmt.Sprintf("%s%s", p, off) }