// 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" "net/http" "net/http/httptest" "os" "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 TestShouldBindUri(t *testing.T) { DefaultWriter = os.Stdout router := New() type Person struct { Name string `uri:"name" binding:"required"` Id string `uri:"id" binding:"required"` } router.Handle("GET", "/rest/:name/:id", func(c *Context) { var person Person assert.NoError(t, c.ShouldBindUri(&person)) assert.True(t, "" != person.Name) assert.True(t, "" != person.Id) c.String(http.StatusOK, "ShouldBindUri test OK") }) path, _ := exampleFromPath("/rest/:name/:id") w := performRequest(router, "GET", path) assert.Equal(t, "ShouldBindUri test OK", w.Body.String()) assert.Equal(t, http.StatusOK, w.Code) } func TestBindUri(t *testing.T) { DefaultWriter = os.Stdout router := New() type Person struct { Name string `uri:"name" binding:"required"` Id string `uri:"id" binding:"required"` } router.Handle("GET", "/rest/:name/:id", func(c *Context) { var person Person assert.NoError(t, c.BindUri(&person)) assert.True(t, "" != person.Name) assert.True(t, "" != person.Id) c.String(http.StatusOK, "BindUri test OK") }) path, _ := exampleFromPath("/rest/:name/:id") w := performRequest(router, "GET", path) assert.Equal(t, "BindUri test OK", w.Body.String()) assert.Equal(t, http.StatusOK, w.Code) } func TestBindUriError(t *testing.T) { DefaultWriter = os.Stdout router := New() type Member struct { Number string `uri:"num" binding:"required,uuid"` } router.Handle("GET", "/new/rest/:num", func(c *Context) { var m Member assert.Error(t, c.BindUri(&m)) }) path1, _ := exampleFromPath("/new/rest/:num") w1 := performRequest(router, "GET", path1) assert.Equal(t, http.StatusBadRequest, w1.Code) } func TestRaceContextCopy(t *testing.T) { DefaultWriter = os.Stdout router := Default() router.GET("/test/copy/race", func(c *Context) { c.Set("1", 0) c.Set("2", 0) // Sending a copy of the Context to two separate routines go readWriteKeys(c.Copy()) go readWriteKeys(c.Copy()) c.String(http.StatusOK, "run OK, no panics") }) w := performRequest(router, "GET", "/test/copy/race") assert.Equal(t, "run OK, no panics", w.Body.String()) } func readWriteKeys(c *Context) { for { c.Set("1", rand.Int()) c.Set("2", c.Value("1")) } } func githubConfigRouter(router *Engine) { for _, route := range githubAPI { router.Handle(route.method, route.path, func(c *Context) { output := make(map[string]string, len(c.Params)+1) output["status"] = "good" for _, param := range c.Params { output[param.Key] = param.Value } c.JSON(http.StatusOK, output) }) } } func TestGithubAPI(t *testing.T) { DefaultWriter = os.Stdout router := New() githubConfigRouter(router) 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:], Value: value, }) output.WriteString(value) } return output.String(), params } func BenchmarkGithub(b *testing.B) { router := New() githubConfigRouter(router) runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword") } func BenchmarkParallelGithub(b *testing.B) { DefaultWriter = os.Stdout router := New() githubConfigRouter(router) req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) b.RunParallel(func(pb *testing.PB) { // Each goroutine has its own bytes.Buffer. for pb.Next() { w := httptest.NewRecorder() router.ServeHTTP(w, req) } }) } func BenchmarkParallelGithubDefault(b *testing.B) { DefaultWriter = os.Stdout router := New() githubConfigRouter(router) req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) b.RunParallel(func(pb *testing.PB) { // Each goroutine has its own bytes.Buffer. for pb.Next() { w := httptest.NewRecorder() router.ServeHTTP(w, req) } }) }