| // 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 httprouter |
| |
| import ( |
| "errors" |
| "fmt" |
| "net/http" |
| "net/http/httptest" |
| "reflect" |
| "testing" |
| ) |
| |
| type mockResponseWriter struct{} |
| |
| func (m *mockResponseWriter) Header() (h http.Header) { |
| return http.Header{} |
| } |
| |
| func (m *mockResponseWriter) Write(p []byte) (n int, err error) { |
| return len(p), nil |
| } |
| |
| func (m *mockResponseWriter) WriteString(s string) (n int, err error) { |
| return len(s), nil |
| } |
| |
| func (m *mockResponseWriter) WriteHeader(int) {} |
| |
| func TestParams(t *testing.T) { |
| ps := Params{ |
| Param{"param1", "value1"}, |
| Param{"param2", "value2"}, |
| Param{"param3", "value3"}, |
| } |
| for i := range ps { |
| if val := ps.ByName(ps[i].Key); val != ps[i].Value { |
| t.Errorf("Wrong value for %s: Got %s; Want %s", ps[i].Key, val, ps[i].Value) |
| } |
| } |
| if val := ps.ByName("noKey"); val != "" { |
| t.Errorf("Expected empty string for not found key; got: %s", val) |
| } |
| } |
| |
| func TestRouter(t *testing.T) { |
| router := New() |
| |
| routed := false |
| router.Handle(http.MethodGet, "/user/:name", func(w http.ResponseWriter, r *http.Request, ps Params) { |
| routed = true |
| want := Params{Param{"name", "gopher"}} |
| if !reflect.DeepEqual(ps, want) { |
| t.Fatalf("wrong wildcard values: want %v, got %v", want, ps) |
| } |
| }) |
| |
| w := new(mockResponseWriter) |
| |
| req, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) |
| router.ServeHTTP(w, req) |
| |
| if !routed { |
| t.Fatal("routing failed") |
| } |
| } |
| |
| type handlerStruct struct { |
| handled *bool |
| } |
| |
| func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| *h.handled = true |
| } |
| |
| func TestRouterAPI(t *testing.T) { |
| var get, head, options, post, put, patch, delete, handler, handlerFunc bool |
| |
| httpHandler := handlerStruct{&handler} |
| |
| router := New() |
| router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| get = true |
| }) |
| router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| head = true |
| }) |
| router.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| options = true |
| }) |
| router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| post = true |
| }) |
| router.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| put = true |
| }) |
| router.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| patch = true |
| }) |
| router.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| delete = true |
| }) |
| router.Handler(http.MethodGet, "/Handler", httpHandler) |
| router.HandlerFunc(http.MethodGet, "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) { |
| handlerFunc = true |
| }) |
| |
| w := new(mockResponseWriter) |
| |
| r, _ := http.NewRequest(http.MethodGet, "/GET", nil) |
| router.ServeHTTP(w, r) |
| if !get { |
| t.Error("routing GET failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodHead, "/GET", nil) |
| router.ServeHTTP(w, r) |
| if !head { |
| t.Error("routing HEAD failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodOptions, "/GET", nil) |
| router.ServeHTTP(w, r) |
| if !options { |
| t.Error("routing OPTIONS failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodPost, "/POST", nil) |
| router.ServeHTTP(w, r) |
| if !post { |
| t.Error("routing POST failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodPut, "/PUT", nil) |
| router.ServeHTTP(w, r) |
| if !put { |
| t.Error("routing PUT failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodPatch, "/PATCH", nil) |
| router.ServeHTTP(w, r) |
| if !patch { |
| t.Error("routing PATCH failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodDelete, "/DELETE", nil) |
| router.ServeHTTP(w, r) |
| if !delete { |
| t.Error("routing DELETE failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodGet, "/Handler", nil) |
| router.ServeHTTP(w, r) |
| if !handler { |
| t.Error("routing Handler failed") |
| } |
| |
| r, _ = http.NewRequest(http.MethodGet, "/HandlerFunc", nil) |
| router.ServeHTTP(w, r) |
| if !handlerFunc { |
| t.Error("routing HandlerFunc failed") |
| } |
| } |
| |
| func TestRouterInvalidInput(t *testing.T) { |
| router := New() |
| |
| handle := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} |
| |
| recv := catchPanic(func() { |
| router.Handle("", "/", handle) |
| }) |
| if recv == nil { |
| t.Fatal("registering empty method did not panic") |
| } |
| |
| recv = catchPanic(func() { |
| router.GET("", handle) |
| }) |
| if recv == nil { |
| t.Fatal("registering empty path did not panic") |
| } |
| |
| recv = catchPanic(func() { |
| router.GET("noSlashRoot", handle) |
| }) |
| if recv == nil { |
| t.Fatal("registering path not beginning with '/' did not panic") |
| } |
| |
| recv = catchPanic(func() { |
| router.GET("/", nil) |
| }) |
| if recv == nil { |
| t.Fatal("registering nil handler did not panic") |
| } |
| } |
| |
| func TestRouterChaining(t *testing.T) { |
| router1 := New() |
| router2 := New() |
| router1.NotFound = router2 |
| |
| fooHit := false |
| router1.POST("/foo", func(w http.ResponseWriter, req *http.Request, _ Params) { |
| fooHit = true |
| w.WriteHeader(http.StatusOK) |
| }) |
| |
| barHit := false |
| router2.POST("/bar", func(w http.ResponseWriter, req *http.Request, _ Params) { |
| barHit = true |
| w.WriteHeader(http.StatusOK) |
| }) |
| |
| r, _ := http.NewRequest(http.MethodPost, "/foo", nil) |
| w := httptest.NewRecorder() |
| router1.ServeHTTP(w, r) |
| if !(w.Code == http.StatusOK && fooHit) { |
| t.Errorf("Regular routing failed with router chaining.") |
| t.FailNow() |
| } |
| |
| r, _ = http.NewRequest(http.MethodPost, "/bar", nil) |
| w = httptest.NewRecorder() |
| router1.ServeHTTP(w, r) |
| if !(w.Code == http.StatusOK && barHit) { |
| t.Errorf("Chained routing failed with router chaining.") |
| t.FailNow() |
| } |
| |
| r, _ = http.NewRequest(http.MethodPost, "/qax", nil) |
| w = httptest.NewRecorder() |
| router1.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNotFound) { |
| t.Errorf("NotFound behavior failed with router chaining.") |
| t.FailNow() |
| } |
| } |
| |
| func BenchmarkAllowed(b *testing.B) { |
| handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} |
| |
| router := New() |
| router.POST("/path", handlerFunc) |
| router.GET("/path", handlerFunc) |
| |
| b.Run("Global", func(b *testing.B) { |
| b.ReportAllocs() |
| for i := 0; i < b.N; i++ { |
| _ = router.allowed("*", http.MethodOptions) |
| } |
| }) |
| b.Run("Path", func(b *testing.B) { |
| b.ReportAllocs() |
| for i := 0; i < b.N; i++ { |
| _ = router.allowed("/path", http.MethodOptions) |
| } |
| }) |
| } |
| |
| func TestRouterOPTIONS(t *testing.T) { |
| handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} |
| |
| router := New() |
| router.POST("/path", handlerFunc) |
| |
| // test not allowed |
| // * (server) |
| r, _ := http.NewRequest(http.MethodOptions, "*", nil) |
| w := httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusOK) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| // path |
| r, _ = http.NewRequest(http.MethodOptions, "/path", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusOK) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| r, _ = http.NewRequest(http.MethodOptions, "/doesnotexist", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNotFound) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } |
| |
| // add another method |
| router.GET("/path", handlerFunc) |
| |
| // set a global OPTIONS handler |
| router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| // Adjust status code to 204 |
| w.WriteHeader(http.StatusNoContent) |
| }) |
| |
| // test again |
| // * (server) |
| r, _ = http.NewRequest(http.MethodOptions, "*", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNoContent) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| // path |
| r, _ = http.NewRequest(http.MethodOptions, "/path", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNoContent) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| // custom handler |
| var custom bool |
| router.OPTIONS("/path", func(w http.ResponseWriter, r *http.Request, _ Params) { |
| custom = true |
| }) |
| |
| // test again |
| // * (server) |
| r, _ = http.NewRequest(http.MethodOptions, "*", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNoContent) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| if custom { |
| t.Error("custom handler called on *") |
| } |
| |
| // path |
| r, _ = http.NewRequest(http.MethodOptions, "/path", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusOK) { |
| t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } |
| if !custom { |
| t.Error("custom handler not called") |
| } |
| } |
| |
| func TestRouterNotAllowed(t *testing.T) { |
| handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} |
| |
| router := New() |
| router.POST("/path", handlerFunc) |
| |
| // test not allowed |
| r, _ := http.NewRequest(http.MethodGet, "/path", nil) |
| w := httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusMethodNotAllowed) { |
| t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| // add another method |
| router.DELETE("/path", handlerFunc) |
| router.OPTIONS("/path", handlerFunc) // must be ignored |
| |
| // test again |
| r, _ = http.NewRequest(http.MethodGet, "/path", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusMethodNotAllowed) { |
| t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } else if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| |
| // test custom handler |
| w = httptest.NewRecorder() |
| responseText := "custom method" |
| router.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| w.WriteHeader(http.StatusTeapot) |
| w.Write([]byte(responseText)) |
| }) |
| router.ServeHTTP(w, r) |
| if got := w.Body.String(); !(got == responseText) { |
| t.Errorf("unexpected response got %q want %q", got, responseText) |
| } |
| if w.Code != http.StatusTeapot { |
| t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot) |
| } |
| if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { |
| t.Error("unexpected Allow header value: " + allow) |
| } |
| } |
| |
| func TestRouterNotFound(t *testing.T) { |
| handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} |
| |
| router := New() |
| router.GET("/path", handlerFunc) |
| router.GET("/dir/", handlerFunc) |
| router.GET("/", handlerFunc) |
| |
| testRoutes := []struct { |
| route string |
| code int |
| location string |
| }{ |
| {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/ |
| {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/ |
| {"", http.StatusMovedPermanently, "/"}, // TSR +/ |
| {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case |
| {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case |
| {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ |
| {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ |
| {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath |
| {"/nope", http.StatusNotFound, ""}, // NotFound |
| } |
| for _, tr := range testRoutes { |
| r, _ := http.NewRequest(http.MethodGet, tr.route, nil) |
| w := httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == tr.code && (w.Code == http.StatusNotFound || fmt.Sprint(w.Header().Get("Location")) == tr.location)) { |
| t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header().Get("Location")) |
| } |
| } |
| |
| // Test custom not found handler |
| var notFound bool |
| router.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
| rw.WriteHeader(http.StatusNotFound) |
| notFound = true |
| }) |
| r, _ := http.NewRequest(http.MethodGet, "/nope", nil) |
| w := httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNotFound && notFound == true) { |
| t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } |
| |
| // Test other method than GET (want 308 instead of 301) |
| router.PATCH("/path", handlerFunc) |
| r, _ = http.NewRequest(http.MethodPatch, "/path/", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusPermanentRedirect && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { |
| t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) |
| } |
| |
| // Test special case where no node for the prefix "/" exists |
| router = New() |
| router.GET("/a", handlerFunc) |
| r, _ = http.NewRequest(http.MethodGet, "/", nil) |
| w = httptest.NewRecorder() |
| router.ServeHTTP(w, r) |
| if !(w.Code == http.StatusNotFound) { |
| t.Errorf("NotFound handling route / failed: Code=%d", w.Code) |
| } |
| } |
| |
| func TestRouterPanicHandler(t *testing.T) { |
| router := New() |
| panicHandled := false |
| |
| router.PanicHandler = func(rw http.ResponseWriter, r *http.Request, p interface{}) { |
| panicHandled = true |
| } |
| |
| router.Handle(http.MethodPut, "/user/:name", func(_ http.ResponseWriter, _ *http.Request, _ Params) { |
| panic("oops!") |
| }) |
| |
| w := new(mockResponseWriter) |
| req, _ := http.NewRequest(http.MethodPut, "/user/gopher", nil) |
| |
| defer func() { |
| if rcv := recover(); rcv != nil { |
| t.Fatal("handling panic failed") |
| } |
| }() |
| |
| router.ServeHTTP(w, req) |
| |
| if !panicHandled { |
| t.Fatal("simulating failed") |
| } |
| } |
| |
| func TestRouterLookup(t *testing.T) { |
| routed := false |
| wantHandle := func(_ http.ResponseWriter, _ *http.Request, _ Params) { |
| routed = true |
| } |
| wantParams := Params{Param{"name", "gopher"}} |
| |
| router := New() |
| |
| // try empty router first |
| handle, _, tsr := router.Lookup(http.MethodGet, "/nope") |
| if handle != nil { |
| t.Fatalf("Got handle for unregistered pattern: %v", handle) |
| } |
| if tsr { |
| t.Error("Got wrong TSR recommendation!") |
| } |
| |
| // insert route and try again |
| router.GET("/user/:name", wantHandle) |
| handle, params, _ := router.Lookup(http.MethodGet, "/user/gopher") |
| if handle == nil { |
| t.Fatal("Got no handle!") |
| } else { |
| handle(nil, nil, nil) |
| if !routed { |
| t.Fatal("Routing failed!") |
| } |
| } |
| if !reflect.DeepEqual(params, wantParams) { |
| t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) |
| } |
| routed = false |
| |
| // route without param |
| router.GET("/user", wantHandle) |
| handle, params, _ = router.Lookup(http.MethodGet, "/user") |
| if handle == nil { |
| t.Fatal("Got no handle!") |
| } else { |
| handle(nil, nil, nil) |
| if !routed { |
| t.Fatal("Routing failed!") |
| } |
| } |
| if params != nil { |
| t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) |
| } |
| |
| handle, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") |
| if handle != nil { |
| t.Fatalf("Got handle for unregistered pattern: %v", handle) |
| } |
| if !tsr { |
| t.Error("Got no TSR recommendation!") |
| } |
| |
| handle, _, tsr = router.Lookup(http.MethodGet, "/nope") |
| if handle != nil { |
| t.Fatalf("Got handle for unregistered pattern: %v", handle) |
| } |
| if tsr { |
| t.Error("Got wrong TSR recommendation!") |
| } |
| } |
| |
| func TestRouterParamsFromContext(t *testing.T) { |
| routed := false |
| |
| wantParams := Params{Param{"name", "gopher"}} |
| handlerFunc := func(_ http.ResponseWriter, req *http.Request) { |
| // get params from request context |
| params := ParamsFromContext(req.Context()) |
| |
| if !reflect.DeepEqual(params, wantParams) { |
| t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) |
| } |
| |
| routed = true |
| } |
| |
| var nilParams Params |
| handlerFuncNil := func(_ http.ResponseWriter, req *http.Request) { |
| // get params from request context |
| params := ParamsFromContext(req.Context()) |
| |
| if !reflect.DeepEqual(params, nilParams) { |
| t.Fatalf("Wrong parameter values: want %v, got %v", nilParams, params) |
| } |
| |
| routed = true |
| } |
| router := New() |
| router.HandlerFunc(http.MethodGet, "/user", handlerFuncNil) |
| router.HandlerFunc(http.MethodGet, "/user/:name", handlerFunc) |
| |
| w := new(mockResponseWriter) |
| r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) |
| router.ServeHTTP(w, r) |
| if !routed { |
| t.Fatal("Routing failed!") |
| } |
| |
| routed = false |
| r, _ = http.NewRequest(http.MethodGet, "/user", nil) |
| router.ServeHTTP(w, r) |
| if !routed { |
| t.Fatal("Routing failed!") |
| } |
| } |
| |
| type mockFileSystem struct { |
| opened bool |
| } |
| |
| func (mfs *mockFileSystem) Open(name string) (http.File, error) { |
| mfs.opened = true |
| return nil, errors.New("this is just a mock") |
| } |
| |
| func TestRouterServeFiles(t *testing.T) { |
| router := New() |
| mfs := &mockFileSystem{} |
| |
| recv := catchPanic(func() { |
| router.ServeFiles("/noFilepath", mfs) |
| }) |
| if recv == nil { |
| t.Fatal("registering path not ending with '*filepath' did not panic") |
| } |
| |
| router.ServeFiles("/*filepath", mfs) |
| w := new(mockResponseWriter) |
| r, _ := http.NewRequest(http.MethodGet, "/favicon.ico", nil) |
| router.ServeHTTP(w, r) |
| if !mfs.opened { |
| t.Error("serving file failed") |
| } |
| } |