diff --git a/matcher.go b/matcher.go index f44c789..0acda38 100644 --- a/matcher.go +++ b/matcher.go @@ -49,7 +49,7 @@ func bySchemas(schemas ...string) (matcher, error) { }, nil } -func byHeaders(headers map[string]string) (matcher, error) { +func byHeaders(headers map[string]string) matcher { return func(r *http.Request) (bool, *node) { if r == nil || len(headers) > len(r.Header) { @@ -61,10 +61,10 @@ func byHeaders(headers map[string]string) (matcher, error) { } } return true, nil - }, nil + } } -func byQueryParameters(params map[string]string) (matcher, error) { +func byQueryParameters(params map[string]string) matcher { return func(r *http.Request) (bool, *node) { if r == nil || len(params) > len(r.URL.Query()) { @@ -76,10 +76,10 @@ func byQueryParameters(params map[string]string) (matcher, error) { } } return true, nil - }, nil + } } -func byCustomMatcher(custom func(r *http.Request) bool) (matcher, error) { +func byCustomMatcher(custom func(r *http.Request) bool) matcher { return func(r *http.Request) (bool, *node) { if r == nil { @@ -87,5 +87,5 @@ func byCustomMatcher(custom func(r *http.Request) bool) (matcher, error) { } return custom(r), nil - }, nil + } } diff --git a/matcher_test.go b/matcher_test.go index 081f534..65aae28 100644 --- a/matcher_test.go +++ b/matcher_test.go @@ -54,6 +54,13 @@ func Test_byHost_ReturnsErrorWhenMalformedHost(t *testing.T) { assertNotNil(t, err) } +func Test_bySchemas_ReturnsErrorWhenInvalidSchemaFormat(t *testing.T) { + s := "htt{" + + _, err := bySchemas(s) + assertNotNil(t, err) +} + func Test_byHeaders_ReturnsFalseWhenInsufficientHeaders(t *testing.T) { req, _ := http.NewRequest("GET", "/", nil) @@ -61,7 +68,7 @@ func Test_byHeaders_ReturnsFalseWhenInsufficientHeaders(t *testing.T) { "key1": "value1", } - m, _ := byHeaders(headers) + m := byHeaders(headers) matches, _ := m(req) assertFalse(t, matches) } @@ -75,7 +82,7 @@ func Test_byHeaders_ReturnsFalseWhenHeaderDoNotMach(t *testing.T) { "key2": "value2", } - m, _ := byHeaders(headers) + m := byHeaders(headers) matches, _ := m(req) assertFalse(t, matches) } @@ -89,31 +96,31 @@ func Test_byHeaders_ReturnsTrueWhenHeadersMatch(t *testing.T) { "key2": "value2", } - m, _ := byHeaders(headers) + m := byHeaders(headers) matches, _ := m(req) assertTrue(t, matches) } -func Test_byHeaders_ReturnsFalseWhenInsufficientQueryParams(t *testing.T) { +func Test_byQueryParameters_ReturnsFalseWhenInsufficientQueryParams(t *testing.T) { req, _ := http.NewRequest("GET", "/", nil) params := map[string]string{ "key1": "value1", } - m, _ := byQueryParameters(params) + m := byQueryParameters(params) matches, _ := m(req) assertFalse(t, matches) } -func Test_byHeaders_ReturnsFalseWhenQueryParamsDoNotMach(t *testing.T) { +func Test_byQueryParameters_ReturnsFalseWhenQueryParamsDoNotMach(t *testing.T) { req, _ := http.NewRequest("GET", "/?key1=value1&key2=invalid", nil) params := map[string]string{ "key1": "value1", "key2": "value2", } - m, _ := byQueryParameters(params) + m := byQueryParameters(params) matches, _ := m(req) assertFalse(t, matches) } @@ -125,7 +132,7 @@ func Test_byQueryParameters_ReturnsTrueWhenQueryParamsMatch(t *testing.T) { "key2": "value2", } - m, _ := byQueryParameters(params) + m := byQueryParameters(params) matches, _ := m(req) assertTrue(t, matches) } @@ -141,11 +148,11 @@ func Test_byCustomMatcher_UsesCustomFunction(t *testing.T) { return false } - m, _ := byCustomMatcher(matcherTrue) + m := byCustomMatcher(matcherTrue) matches, _ := m(req) assertTrue(t, matches) - m, _ = byCustomMatcher(matcherFalse) + m = byCustomMatcher(matcherFalse) matches, _ = m(req) assertFalse(t, matches) } diff --git a/router.go b/router.go index ae5b71e..f99ad2c 100644 --- a/router.go +++ b/router.go @@ -77,6 +77,7 @@ func buildURLParameters(leaf *node, path string, offset int, paramsCount uint) U return paramsBag } +// RouterConfig is a structure to set the router configuration type RouterConfig struct { EnableAutoMethodHead bool EnableAutoMethodOptions bool @@ -146,6 +147,7 @@ func (r *Router) As(asName string) *Router { return r } +// MatchingOptions is a structure to define a route name and extend the matching options type MatchingOptions struct { Name string Host string @@ -155,6 +157,7 @@ type MatchingOptions struct { Custom func(r *http.Request) bool } +// NewMatchingOptions returns the MatchingOptions structure func NewMatchingOptions() MatchingOptions { return MatchingOptions{ Name: "", @@ -211,26 +214,17 @@ func (r *Router) Register(verb, path string, handler http.HandlerFunc, options . } if len(options[0].Headers) > 0 { - matcherByHeaders, err := byHeaders(options[0].Headers) - if err != nil { - return err - } + matcherByHeaders := byHeaders(options[0].Headers) leaf.matchers = append(leaf.matchers, matcherByHeaders) } if len(options[0].QueryParams) > 0 { - matcherByQueryParams, err := byQueryParameters(options[0].QueryParams) - if err != nil { - return err - } + matcherByQueryParams := byQueryParameters(options[0].QueryParams) leaf.matchers = append(leaf.matchers, matcherByQueryParams) } if options[0].Custom != nil { - matcherByCustomFunc, err := byCustomMatcher(options[0].Custom) - if err != nil { - return err - } + matcherByCustomFunc := byCustomMatcher(options[0].Custom) leaf.matchers = append(leaf.matchers, matcherByCustomFunc) } } @@ -377,21 +371,19 @@ func (r *Router) Prefix(path string, router *Router) error { return nil } +// StaticFiles will serve files from a directory under a prefix path func (r *Router) StaticFiles(prefix, dir string) error { return r.Register("GET", prefix+"/{name:.*}", func(writer http.ResponseWriter, request *http.Request) { urlParams := GetURLParameters(request) - name, err := urlParams.GetByName("name") - if err != nil { - writer.WriteHeader(404) - return - } + name, _ := urlParams.GetByName("name") request.URL.Path = name http.FileServer(http.Dir(dir)).ServeHTTP(writer, request) }) } +// Redirect will redirect a path to an url func (r *Router) Redirect(path, url string, code ...int) error { return r.Register(http.MethodGet, path, getRedirectHandler(url, code...)) } diff --git a/router_test.go b/router_test.go index 0facc47..e689fce 100644 --- a/router_test.go +++ b/router_test.go @@ -221,6 +221,31 @@ func TestRouter_AllVerbs(t *testing.T) { assertPathFound(t, router, "TRACE", path) } +func TestRouter_Any(t *testing.T) { + path := "/path1" + + router := Router{} + _ = router.Any(path, testHandlerFunc) + + assertPathFound(t, router, "GET", path) + assertPathFound(t, router, "HEAD", path) + assertPathFound(t, router, "POST", path) + assertPathFound(t, router, "PUT", path) + assertPathFound(t, router, "PATCH", path) + assertPathFound(t, router, "DELETE", path) + assertPathFound(t, router, "CONNECT", path) + assertPathFound(t, router, "OPTIONS", path) + assertPathFound(t, router, "TRACE", path) +} + +func TestRouter_Any_ReturnsErrorIfInvalidRoute(t *testing.T) { + path := "/path1{" + + router := Router{} + err := router.Any(path, testHandlerFunc) + assertNotNil(t, err) +} + func TestGetURLParameters_ReturnsEmptyBagIfNoContextValueExists(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, "/dummy", nil) @@ -324,6 +349,24 @@ func TestRouter_As_AssignsRouteNames(t *testing.T) { assertRouteNameHasHandler(t, mainRouter, http.MethodGet, "/api/users/profile", "users.profile") } +func TestRouter_Prefix_ReturnsErrorIfInvalidPath(t *testing.T) { + mainRouter := Router{} + secondRouter := Router{} + + err := mainRouter.Prefix("path{", &secondRouter) + assertNotNil(t, err) +} + +func TestRouter_Prefix_CreateTreeWhenStillNotCreated(t *testing.T) { + mainRouter := Router{} + secondRouter := Router{} + assertNil(t, mainRouter.trees) + + _ = mainRouter.Prefix("/path", &secondRouter) + + assertNotNil(t, mainRouter.trees) +} + func TestRouter_MatchingOptions_AssignsRouteNames(t *testing.T) { mainRouter := Router{} @@ -591,6 +634,62 @@ func TestRouter_GenerateURL_GenerateValidRoutes(t *testing.T) { assertRouteIsGenerated(t, mainRouter, "date_1", "/posts/10/2020-05-05", map[string]string{"id": "10", "date": "2020-05-05"}) } +func TestRouter_GenerateURL_ReturnsErrorWhenRouteNameNotFound(t *testing.T) { + mainRouter := Router{} + url := "/path1" + name := "name1" + _ = mainRouter.Register(http.MethodGet, url, testHandlerFunc, MatchingOptions{ + Name: name, + }) + + bag := URLParameterBag{} + route, err := mainRouter.GenerateURL("path2", bag) + if err == nil { + t.Errorf("route %s is generated", name) + } + if route == url { + t.Errorf("route %s is valid", url) + } +} + +func TestRouter_GenerateURL_ReturnsErrorWhenParamNameNotFound(t *testing.T) { + mainRouter := Router{} + url := "/path1/{name:[a-z]+}/path2" + name := "path1" + _ = mainRouter.Register(http.MethodGet, url, testHandlerFunc, MatchingOptions{ + Name: name, + }) + + bag := URLParameterBag{} + bag.add("id", "john") + route, err := mainRouter.GenerateURL(name, bag) + if err == nil { + t.Errorf("route %s is generated", name) + } + if route == url { + t.Errorf("route %s is valid", url) + } +} + +func TestRouter_GenerateURL_ReturnsErrorWhenRegularExpressionNotMatches(t *testing.T) { + mainRouter := Router{} + url := "/path1/{name:[a-z]+}" + name := "path1" + _ = mainRouter.Register(http.MethodGet, url, testHandlerFunc, MatchingOptions{ + Name: name, + }) + + bag := URLParameterBag{} + bag.add("name", "1234") + route, err := mainRouter.GenerateURL(name, bag) + if err == nil { + t.Errorf("route %s is generated", name) + } + if route == url { + t.Errorf("route %s is valid", url) + } +} + func TestRouter_StaticFiles_ServerStaticFileFromDir(t *testing.T) { mainRouter := Router{} @@ -634,6 +733,26 @@ func TestRouter_Register_GeneratesValidRouteNames(t *testing.T) { assertRouteIsGenerated(t, mainRouter, "date", "/2020-05-05", map[string]string{"date": "2020-05-05"}) } +func TestRouter_PrioritizeByWeight_StillMatchesRoutes(t *testing.T) { + mainRouter := Router{} + + _ = mainRouter.Register(http.MethodGet, "/", testHandlerFunc) + _ = mainRouter.Register(http.MethodGet, "/with/slash", testHandlerFunc, MatchingOptions{Name: "/w/s"}) + _ = mainRouter.Register(http.MethodGet, "/path1", testHandlerFunc, MatchingOptions{Name: "path"}) + _ = mainRouter.Register(http.MethodGet, "/path1/{id}/{name:[a-z]{1,5}}", testHandlerFunc) + _ = mainRouter.Register(http.MethodGet, "/path1/{file:.*}", testHandlerFunc, MatchingOptions{Name: "path"}) + _ = mainRouter.Register(http.MethodGet, "/{date:[0-9]{4}-[0-9]{2}-[0-9]{2}}", testHandlerFunc) + + mainRouter.PrioritizeByWeight() + + assertPathFound(t, mainRouter, "GET", "/") + assertPathFound(t, mainRouter, "GET", "/with/slash") + assertPathFound(t, mainRouter, "GET", "/path1") + assertPathFound(t, mainRouter, "GET", "/path1/1/name") + assertPathFound(t, mainRouter, "GET", "/path1/some/path/to/file") + assertPathFound(t, mainRouter, "GET", "/2021-01-31") +} + func TestRouter_NewRouter_WithDefaultConfig(t *testing.T) { mainRouter := NewRouter() @@ -710,6 +829,22 @@ func TestRouter_Register_CanOverrideRouteHandler(t *testing.T) { assertStringEqual(t, "dummy", getResponse.Body.String()) } +func TestRouter_Register_ReturnsErrorIfInvalidPath(t *testing.T) { + mainRouter := NewRouter() + + err := mainRouter.Register(http.MethodGet, "/some{", testHandlerFunc) + + assertNotNil(t, err) +} + +func TestRouter_Register_ReturnsErrorIfInvalidBySchemasMatcher(t *testing.T) { + mainRouter := NewRouter() + + err := mainRouter.Register(http.MethodGet, "/some", testHandlerFunc, MatchingOptions{Schemas: []string{"http{"}}) + + assertNotNil(t, err) +} + func TestRouter_NewRouter_WithMethodNotAllowedResponseEnabled(t *testing.T) { mainRouter := NewRouter(RouterConfig{ EnableMethodNotAllowedResponse: true, @@ -851,6 +986,18 @@ func TestRouter_Load_FailsWhenSchemaIsInvalid(t *testing.T) { } } + +func TestRouter_Load_FailsWhenRouteIsInvalid(t *testing.T) { + AddHandler(testHandlerFunc, "users.Handler") + + router := NewRouter() + loader := sliceLoader{ + RouteDef{Name: "get.users", Method: "GET", Schema: "/users{", Handler: "users.Handler"}, + } + err := router.Load(&loader) + assertNotNil(t, err) +} + func assertEqual(t *testing.T, expected, value int) { if expected != value { t.Errorf("%v is not equal to %v", expected, value) diff --git a/tree_test.go b/tree_test.go index dbf287f..628f754 100644 --- a/tree_test.go +++ b/tree_test.go @@ -205,6 +205,16 @@ func TestTree_Insert_PrioritisesStaticPaths(t *testing.T) { assertNodeDynamic(t, tree.root.child.sibling.sibling, "name", "", true, tree.root) } +func TestCreateTreeFromChunks_ReturnsNilIfEmptyChunks(t *testing.T) { + + chunks := []chunk{} + + root, leaf := createTreeFromChunks(chunks) + + assertNil(t, root) + assertNil(t, leaf) +} + func TestCreateTreeFromChunks(t *testing.T) { chunks := []chunk{ @@ -247,6 +257,7 @@ func TestTree_OptimizeByWeight_PrioritisesHeavierPathsAllStatic(t *testing.T) { parseAndInsertSchema(tree, "/path3", "3") parseAndInsertSchema(tree, "/path3/name", "name") parseAndInsertSchema(tree, "/path3/phone", "phone") + parseAndInsertSchema(tree, "/path3/{name:[a-z]+}/phone", "phoneName") assertNodeStatic(t, tree.root, "/", false, nil) assertNodeStatic(t, tree.root.child, "data", true, tree.root) @@ -258,6 +269,7 @@ func TestTree_OptimizeByWeight_PrioritisesHeavierPathsAllStatic(t *testing.T) { assertNodeStatic(t, tree.root.child.sibling.child.sibling.sibling.child, "/", false, tree.root.child.sibling.child.sibling.sibling) assertNodeStatic(t, tree.root.child.sibling.child.sibling.sibling.child.child, "name", true, tree.root.child.sibling.child.sibling.sibling.child) assertNodeStatic(t, tree.root.child.sibling.child.sibling.sibling.child.child.sibling, "phone", true, tree.root.child.sibling.child.sibling.sibling.child) + assertNodeDynamic(t, tree.root.child.sibling.child.sibling.sibling.child.child.sibling.sibling, "name", "^[a-z]+$",false, tree.root.child.sibling.child.sibling.sibling.child.child.sibling.parent) _ = calcWeight(tree.root) tree.root = sortByWeight(tree.root)