// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. // Revel Framework source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package revel import ( "fmt" "net/http" "net/url" "strings" "testing" ) // Data-driven tests that check that a given routes-file line translates into // the expected Route object. var routeTestCases = map[string]*Route{ "get / Application.Index": { Method: "GET", Path: "/", Action: "Application.Index", FixedParams: []string{}, }, "post /app/:id Application.SaveApp": { Method: "POST", Path: "/app/:id", Action: "Application.SaveApp", FixedParams: []string{}, }, "get /app/ Application.List": { Method: "GET", Path: "/app/", Action: "Application.List", FixedParams: []string{}, }, `get /app/:appId/ Application.Show`: { Method: "GET", Path: `/app/:appId/`, Action: "Application.Show", FixedParams: []string{}, }, `get /app-wild/*appId/ Application.WildShow`: { Method: "GET", Path: `/app-wild/*appId/`, Action: "Application.WildShow", FixedParams: []string{}, }, `GET /public/:filepath Static.Serve("public")`: { Method: "GET", Path: "/public/:filepath", Action: "Static.Serve", FixedParams: []string{ "public", }, }, `GET /javascript/:filepath Static.Serve("public/js")`: { Method: "GET", Path: "/javascript/:filepath", Action: "Static.Serve", FixedParams: []string{ "public", }, }, "* /apps/:id/:action Application.:action": { Method: "*", Path: "/apps/:id/:action", Action: "Application.:action", FixedParams: []string{}, }, "* /:controller/:action :controller.:action": { Method: "*", Path: "/:controller/:action", Action: ":controller.:action", FixedParams: []string{}, }, `GET / Application.Index("Test", "Test2")`: { Method: "GET", Path: "/", Action: "Application.Index", FixedParams: []string{ "Test", "Test2", }, }, } // Run the test cases above. func TestComputeRoute(t *testing.T) { for routeLine, expected := range routeTestCases { method, path, action, fixedArgs, found := parseRouteLine(routeLine) if !found { t.Error("Failed to parse route line:", routeLine) continue } actual := NewRoute(appModule, method, path, action, fixedArgs, "", 0) eq(t, "Method", actual.Method, expected.Method) eq(t, "Path", actual.Path, expected.Path) eq(t, "Action", actual.Action, expected.Action) if t.Failed() { t.Fatal("Failed on route:", routeLine) } } } // Router Tests const TestRoutes = ` # This is a comment GET / Application.Index GET /test/ Application.Index("Test", "Test2") GET /app/:id/ Application.Show GET /app-wild/*id/ Application.WildShow POST /app/:id Application.Save PATCH /app/:id/ Application.Update PROPFIND /app/:id Application.WebDevMethodPropFind MKCOL /app/:id Application.WebDevMethodMkCol COPY /app/:id Application.WebDevMethodCopy MOVE /app/:id Application.WebDevMethodMove PROPPATCH /app/:id Application.WebDevMethodPropPatch LOCK /app/:id Application.WebDevMethodLock UNLOCK /app/:id Application.WebDevMethodUnLock TRACE /app/:id Application.WebDevMethodTrace PURGE /app/:id Application.CacheMethodPurge GET /javascript/:filepath App\Static.Serve("public/js") GET /public/*filepath Static.Serve("public") * /:controller/:action :controller.:action GET /favicon.ico 404 ` var routeMatchTestCases = map[*http.Request]*RouteMatch{ { Method: "GET", URL: &url.URL{Path: "/"}, }: { ControllerName: "application", MethodName: "Index", FixedParams: []string{}, Params: map[string][]string{}, }, { Method: "GET", URL: &url.URL{Path: "/test/"}, }: { ControllerName: "application", MethodName: "Index", FixedParams: []string{"Test", "Test2"}, Params: map[string][]string{}, }, { Method: "GET", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "Show", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "PATCH", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "Update", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "POST", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "Save", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "GET", URL: &url.URL{Path: "/app/123/"}, }: { ControllerName: "application", MethodName: "Show", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "GET", URL: &url.URL{Path: "/public/css/style.css"}, }: { ControllerName: "static", MethodName: "Serve", FixedParams: []string{"public"}, Params: map[string][]string{"filepath": {"css/style.css"}}, }, { Method: "GET", URL: &url.URL{Path: "/javascript/sessvars.js"}, }: { ControllerName: "static", MethodName: "Serve", FixedParams: []string{"public/js"}, Params: map[string][]string{"filepath": {"sessvars.js"}}, }, { Method: "GET", URL: &url.URL{Path: "/Implicit/Route"}, }: { ControllerName: "implicit", MethodName: "Route", FixedParams: []string{}, Params: map[string][]string{ "METHOD": {"GET"}, "controller": {"Implicit"}, "action": {"Route"}, }, }, { Method: "GET", URL: &url.URL{Path: "/favicon.ico"}, }: { ControllerName: "", MethodName: "", Action: "404", FixedParams: []string{}, Params: map[string][]string{}, }, { Method: "POST", URL: &url.URL{Path: "/app/123"}, Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}}, }: { ControllerName: "application", MethodName: "Update", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "GET", URL: &url.URL{Path: "/app/123"}, Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}}, }: { ControllerName: "application", MethodName: "Show", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "PATCH", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "Update", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "PROPFIND", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodPropFind", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "MKCOL", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodMkCol", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "COPY", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodCopy", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "MOVE", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodMove", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "PROPPATCH", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodPropPatch", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "LOCK", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodLock", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "UNLOCK", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodUnLock", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "TRACE", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "WebDevMethodTrace", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, { Method: "PURGE", URL: &url.URL{Path: "/app/123"}, }: { ControllerName: "application", MethodName: "CacheMethodPurge", FixedParams: []string{}, Params: map[string][]string{"id": {"123"}}, }, } func TestRouteMatches(t *testing.T) { initControllers() BasePath = "/BasePath" router := NewRouter("") router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false) if err := router.updateTree(); err != nil { t.Errorf("updateTree failed: %s", err) } for req, expected := range routeMatchTestCases { t.Log("Routing:", req.Method, req.URL) context := NewGoContext(nil) context.Request.SetRequest(req) c := NewTestController(nil, req) actual := router.Route(c.Request) if !eq(t, "Found route", actual != nil, expected != nil) { continue } if expected.ControllerName != "" { eq(t, "ControllerName", actual.ControllerName, appModule.Namespace()+expected.ControllerName) } else { eq(t, "ControllerName", actual.ControllerName, expected.ControllerName) } eq(t, "MethodName", actual.MethodName, strings.ToLower(expected.MethodName)) eq(t, "len(Params)", len(actual.Params), len(expected.Params)) for key, actualValue := range actual.Params { eq(t, "Params "+key, actualValue[0], expected.Params[key][0]) } eq(t, "len(FixedParams)", len(actual.FixedParams), len(expected.FixedParams)) for i, actualValue := range actual.FixedParams { eq(t, "FixedParams", actualValue, expected.FixedParams[i]) } } } // Reverse Routing type ReverseRouteArgs struct { action string args map[string]string } var reverseRoutingTestCases = map[*ReverseRouteArgs]*ActionDefinition{ { action: "Application.Index", args: map[string]string{}, }: { URL: "/", Method: "GET", Star: false, Action: "Application.Index", }, { action: "Application.Show", args: map[string]string{"id": "123"}, }: { URL: "/app/123/", Method: "GET", Star: false, Action: "Application.Show", }, { action: "Implicit.Route", args: map[string]string{}, }: { URL: "/implicit/route", Method: "GET", Star: true, Action: "Implicit.Route", }, { action: "Application.Save", args: map[string]string{"id": "123", "c": "http://continue"}, }: { URL: "/app/123?c=http%3A%2F%2Fcontinue", Method: "POST", Star: false, Action: "Application.Save", }, { action: "Application.WildShow", args: map[string]string{"id": "123"}, }: { URL: "/app-wild/123/", Method: "GET", Star: false, Action: "Application.WildShow", }, { action: "Application.WebDevMethodPropFind", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "PROPFIND", Star: false, Action: "Application.WebDevMethodPropFind", }, { action: "Application.WebDevMethodMkCol", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "MKCOL", Star: false, Action: "Application.WebDevMethodMkCol", }, { action: "Application.WebDevMethodCopy", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "COPY", Star: false, Action: "Application.WebDevMethodCopy", }, { action: "Application.WebDevMethodMove", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "MOVE", Star: false, Action: "Application.WebDevMethodMove", }, { action: "Application.WebDevMethodPropPatch", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "PROPPATCH", Star: false, Action: "Application.WebDevMethodPropPatch", }, { action: "Application.WebDevMethodLock", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "LOCK", Star: false, Action: "Application.WebDevMethodLock", }, { action: "Application.WebDevMethodUnLock", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "UNLOCK", Star: false, Action: "Application.WebDevMethodUnLock", }, { action: "Application.WebDevMethodTrace", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "TRACE", Star: false, Action: "Application.WebDevMethodTrace", }, { action: "Application.CacheMethodPurge", args: map[string]string{"id": "123"}, }: { URL: "/app/123", Method: "PURGE", Star: false, Action: "Application.CacheMethodPurge", }, } type testController struct { *Controller } func initControllers() { registerControllers() } func TestReverseRouting(t *testing.T) { initControllers() router := NewRouter("") router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false) for routeArgs, expected := range reverseRoutingTestCases { actual := router.Reverse(routeArgs.action, routeArgs.args) if !eq(t, fmt.Sprintf("Found route %s %s", routeArgs.action, actual), actual != nil, expected != nil) { continue } eq(t, "Url", actual.URL, expected.URL) eq(t, "Method", actual.Method, expected.Method) eq(t, "Star", actual.Star, expected.Star) eq(t, "Action", actual.Action, expected.Action) } } func BenchmarkRouter(b *testing.B) { router := NewRouter("") router.Routes, _ = parseRoutes(nil, "", "", TestRoutes, false) if err := router.updateTree(); err != nil { b.Errorf("updateTree failed: %s", err) } b.ResetTimer() for i := 0; i < b.N/len(routeMatchTestCases); i++ { for req := range routeMatchTestCases { c := NewTestController(nil, req) r := router.Route(c.Request) if r == nil { b.Errorf("Request not found: %s", req.URL.Path) } } } } // The benchmark from github.com/ant0ine/go-urlrouter func BenchmarkLargeRouter(b *testing.B) { router := NewRouter("") routePaths := []string{ "/", "/signin", "/signout", "/profile", "/settings", "/upload/*file", } for i := 0; i < 10; i++ { for j := 0; j < 5; j++ { routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id/property%d", i, j)) } routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id", i)) routePaths = append(routePaths, fmt.Sprintf("/resource%d", i)) } routePaths = append(routePaths, "/:any") for _, p := range routePaths { router.Routes = append(router.Routes, NewRoute(appModule, "GET", p, "Controller.Action", "", "", 0)) } if err := router.updateTree(); err != nil { b.Errorf("updateTree failed: %s", err) } requestUrls := []string{ "http://example.org/", "http://example.org/resource9/123", "http://example.org/resource9/123/property1", "http://example.org/doesnotexist", } var reqs []*http.Request for _, url := range requestUrls { req, _ := http.NewRequest("GET", url, nil) reqs = append(reqs, req) } b.ResetTimer() for i := 0; i < b.N/len(reqs); i++ { for _, req := range reqs { c := NewTestController(nil, req) route := router.Route(c.Request) if route == nil { b.Errorf("Failed to route: %s", req.URL.Path) } } } } func BenchmarkRouterFilter(b *testing.B) { startFakeBookingApp() controllers := []*Controller{ NewTestController(nil, showRequest), NewTestController(nil, staticRequest), } for _, c := range controllers { c.Params = &Params{} ParseParams(c.Params, c.Request) } b.ResetTimer() for i := 0; i < b.N/len(controllers); i++ { for _, c := range controllers { RouterFilter(c, NilChain) } } } func TestOverrideMethodFilter(t *testing.T) { req, _ := http.NewRequest("POST", "/hotels/3", strings.NewReader("_method=put")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") c := NewTestController(nil, req) if HTTPMethodOverride(c, NilChain); c.Request.Method != "PUT" { t.Errorf("Expected to override current method '%s' in route, found '%s' instead", "", c.Request.Method) } } // Helpers func eq(t *testing.T, name string, a, b interface{}) bool { if a != b { t.Error(name, ": (actual)", a, " != ", b, "(expected)") return false } return true }