Skip to content

Commit

Permalink
[bugfix] Fix method subrouter handler matching (#300) (#317)
Browse files Browse the repository at this point in the history
* Test method-based subrouters for multiple matching paths

* Pass TestMethodsSubrouter

* Change http.Method* constants to string literals
- Make compatible with Go v1.5

* Make TestMethodsSubrouter stateless and concurrent

* Remove t.Run and break up tests for concurrency

* Use backticks to remove quote escaping

* Remove global method handlers and HTTP method constants
  • Loading branch information
mtso authored and kisielk committed Nov 28, 2017
1 parent 2d5fef0 commit 4a3d4f3
Show file tree
Hide file tree
Showing 2 changed files with 314 additions and 0 deletions.
312 changes: 312 additions & 0 deletions mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,318 @@ func TestErrMatchNotFound(t *testing.T) {
}
}

// methodsSubrouterTest models the data necessary for testing handler
// matching for subrouters created after HTTP methods matcher registration.
type methodsSubrouterTest struct {
title string
wantCode int
router *Router
// method is the input into the request and expected response
method string
// input request path
path string
// redirectTo is the expected location path for strict-slash matches
redirectTo string
}

// methodHandler writes the method string in response.
func methodHandler(method string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(method))
}
}

// TestMethodsSubrouterCatchall matches handlers for subrouters where a
// catchall handler is set for a mis-matching method.
func TestMethodsSubrouterCatchall(t *testing.T) {
t.Parallel()

router := NewRouter()
router.Methods("PATCH").Subrouter().PathPrefix("/").HandlerFunc(methodHandler("PUT"))
router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET"))
router.Methods("POST").Subrouter().HandleFunc("/foo", methodHandler("POST"))
router.Methods("DELETE").Subrouter().HandleFunc("/foo", methodHandler("DELETE"))

tests := []methodsSubrouterTest{
{
title: "match GET handler",
router: router,
path: "http://localhost/foo",
method: "GET",
wantCode: http.StatusOK,
},
{
title: "match POST handler",
router: router,
method: "POST",
path: "http://localhost/foo",
wantCode: http.StatusOK,
},
{
title: "match DELETE handler",
router: router,
method: "DELETE",
path: "http://localhost/foo",
wantCode: http.StatusOK,
},
{
title: "disallow PUT method",
router: router,
method: "PUT",
path: "http://localhost/foo",
wantCode: http.StatusMethodNotAllowed,
},
}

for _, test := range tests {
testMethodsSubrouter(t, test)
}
}

// TestMethodsSubrouterStrictSlash matches handlers on subrouters with
// strict-slash matchers.
func TestMethodsSubrouterStrictSlash(t *testing.T) {
t.Parallel()

router := NewRouter()
sub := router.PathPrefix("/").Subrouter()
sub.StrictSlash(true).Path("/foo").Methods("GET").Subrouter().HandleFunc("", methodHandler("GET"))
sub.StrictSlash(true).Path("/foo/").Methods("PUT").Subrouter().HandleFunc("/", methodHandler("PUT"))
sub.StrictSlash(true).Path("/foo/").Methods("POST").Subrouter().HandleFunc("/", methodHandler("POST"))

tests := []methodsSubrouterTest{
{
title: "match POST handler",
router: router,
method: "POST",
path: "http://localhost/foo/",
wantCode: http.StatusOK,
},
{
title: "match GET handler",
router: router,
method: "GET",
path: "http://localhost/foo",
wantCode: http.StatusOK,
},
{
title: "match POST handler, redirect strict-slash",
router: router,
method: "POST",
path: "http://localhost/foo",
redirectTo: "http://localhost/foo/",
wantCode: http.StatusMovedPermanently,
},
{
title: "match GET handler, redirect strict-slash",
router: router,
method: "GET",
path: "http://localhost/foo/",
redirectTo: "http://localhost/foo",
wantCode: http.StatusMovedPermanently,
},
{
title: "disallow DELETE method",
router: router,
method: "DELETE",
path: "http://localhost/foo",
wantCode: http.StatusMethodNotAllowed,
},
}

for _, test := range tests {
testMethodsSubrouter(t, test)
}
}

// TestMethodsSubrouterPathPrefix matches handlers on subrouters created
// on a router with a path prefix matcher and method matcher.
func TestMethodsSubrouterPathPrefix(t *testing.T) {
t.Parallel()

router := NewRouter()
router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST"))
router.PathPrefix("/1").Methods("DELETE").Subrouter().HandleFunc("/2", methodHandler("DELETE"))
router.PathPrefix("/1").Methods("PUT").Subrouter().HandleFunc("/2", methodHandler("PUT"))
router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST2"))

tests := []methodsSubrouterTest{
{
title: "match first POST handler",
router: router,
method: "POST",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "match DELETE handler",
router: router,
method: "DELETE",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "match PUT handler",
router: router,
method: "PUT",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "disallow PATCH method",
router: router,
method: "PATCH",
path: "http://localhost/1/2",
wantCode: http.StatusMethodNotAllowed,
},
}

for _, test := range tests {
testMethodsSubrouter(t, test)
}
}

// TestMethodsSubrouterSubrouter matches handlers on subrouters produced
// from method matchers registered on a root subrouter.
func TestMethodsSubrouterSubrouter(t *testing.T) {
t.Parallel()

router := NewRouter()
sub := router.PathPrefix("/1").Subrouter()
sub.Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST"))
sub.Methods("GET").Subrouter().HandleFunc("/2", methodHandler("GET"))
sub.Methods("PATCH").Subrouter().HandleFunc("/2", methodHandler("PATCH"))
sub.HandleFunc("/2", methodHandler("PUT")).Subrouter().Methods("PUT")
sub.HandleFunc("/2", methodHandler("POST2")).Subrouter().Methods("POST")

tests := []methodsSubrouterTest{
{
title: "match first POST handler",
router: router,
method: "POST",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "match GET handler",
router: router,
method: "GET",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "match PATCH handler",
router: router,
method: "PATCH",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "match PUT handler",
router: router,
method: "PUT",
path: "http://localhost/1/2",
wantCode: http.StatusOK,
},
{
title: "disallow DELETE method",
router: router,
method: "DELETE",
path: "http://localhost/1/2",
wantCode: http.StatusMethodNotAllowed,
},
}

for _, test := range tests {
testMethodsSubrouter(t, test)
}
}

// TestMethodsSubrouterPathVariable matches handlers on matching paths
// with path variables in them.
func TestMethodsSubrouterPathVariable(t *testing.T) {
t.Parallel()

router := NewRouter()
router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET"))
router.Methods("POST").Subrouter().HandleFunc("/{any}", methodHandler("POST"))
router.Methods("DELETE").Subrouter().HandleFunc("/1/{any}", methodHandler("DELETE"))
router.Methods("PUT").Subrouter().HandleFunc("/1/{any}", methodHandler("PUT"))

tests := []methodsSubrouterTest{
{
title: "match GET handler",
router: router,
method: "GET",
path: "http://localhost/foo",
wantCode: http.StatusOK,
},
{
title: "match POST handler",
router: router,
method: "POST",
path: "http://localhost/foo",
wantCode: http.StatusOK,
},
{
title: "match DELETE handler",
router: router,
method: "DELETE",
path: "http://localhost/1/foo",
wantCode: http.StatusOK,
},
{
title: "match PUT handler",
router: router,
method: "PUT",
path: "http://localhost/1/foo",
wantCode: http.StatusOK,
},
{
title: "disallow PATCH method",
router: router,
method: "PATCH",
path: "http://localhost/1/foo",
wantCode: http.StatusMethodNotAllowed,
},
}

for _, test := range tests {
testMethodsSubrouter(t, test)
}
}

// testMethodsSubrouter runs an individual methodsSubrouterTest.
func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) {
// Execute request
req, _ := http.NewRequest(test.method, test.path, nil)
resp := NewRecorder()
test.router.ServeHTTP(resp, req)

switch test.wantCode {
case http.StatusMethodNotAllowed:
if resp.Code != http.StatusMethodNotAllowed {
t.Errorf(`(%s) Expected "405 Method Not Allowed", but got %d code`, test.title, resp.Code)
} else if matchedMethod := resp.Body.String(); matchedMethod != "" {
t.Errorf(`(%s) Expected "405 Method Not Allowed", but %q handler was called`, test.title, matchedMethod)
}

case http.StatusMovedPermanently:
if gotLocation := resp.HeaderMap.Get("Location"); gotLocation != test.redirectTo {
t.Errorf("(%s) Expected %q route-match to redirect to %q, but got %q", test.title, test.method, test.redirectTo, gotLocation)
}

case http.StatusOK:
if matchedMethod := resp.Body.String(); matchedMethod != test.method {
t.Errorf("(%s) Expected %q handler to be called, but %q handler was called", test.title, test.method, matchedMethod)
}

default:
expectedCodes := []int{http.StatusMethodNotAllowed, http.StatusMovedPermanently, http.StatusOK}
t.Errorf("(%s) Expected wantCode to be one of: %v, but got %d", test.title, expectedCodes, test.wantCode)
}
}

// mapToPairs converts a string map to a slice of string pairs
func mapToPairs(m map[string]string) []string {
var i int
Expand Down
2 changes: 2 additions & 0 deletions route.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if match.MatchErr == ErrMethodMismatch {
// We found a route which matches request method, clear MatchErr
match.MatchErr = nil
// Then override the mis-matched handler
match.Handler = r.handler
}

// Yay, we have a match. Let's collect some info about it.
Expand Down

0 comments on commit 4a3d4f3

Please sign in to comment.