diff --git a/echo.go b/echo.go index ccd83db03..c43d26e20 100644 --- a/echo.go +++ b/echo.go @@ -153,6 +153,8 @@ const ( PROPFIND = "PROPFIND" // REPORT Method can be used to get information about a resource, see rfc 3253 REPORT = "REPORT" + // RouteNotFound is special method type for routes handling "route not found" (404) cases + RouteNotFound = "echo_route_not_found" ) // Headers @@ -408,6 +410,16 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo return e.Add(http.MethodTrace, path, h, m...) } +// RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases) +// for current request URL. +// Path supports static and named/any parameters just like other http method is defined. Generally path is ended with +// wildcard/match-any character (`/*`, `/download/*` etc). +// +// Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` +func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo { + return e.Add(RouteNotFound, path, h, m...) +} + // Any registers a new route for all supported HTTP methods and path with matching handler // in the router with optional route-level middleware. Panics on error. func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) Routes { diff --git a/echo_test.go b/echo_test.go index 67e5483b0..b75bd2532 100644 --- a/echo_test.go +++ b/echo_test.go @@ -918,6 +918,70 @@ func TestEchoGroup(t *testing.T) { assert.Equal(t, "023", buf.String()) } +func TestEcho_RouteNotFound(t *testing.T) { + var testCases = []struct { + name string + whenURL string + expectRoute interface{} + expectCode int + }{ + { + name: "404, route to static not found handler /a/c/xx", + whenURL: "/a/c/xx", + expectRoute: "GET /a/c/xx", + expectCode: http.StatusNotFound, + }, + { + name: "404, route to path param not found handler /a/:file", + whenURL: "/a/echo.exe", + expectRoute: "GET /a/:file", + expectCode: http.StatusNotFound, + }, + { + name: "404, route to any not found handler /*", + whenURL: "/b/echo.exe", + expectRoute: "GET /*", + expectCode: http.StatusNotFound, + }, + { + name: "200, route /a/c/df to /a/c/df", + whenURL: "/a/c/df", + expectRoute: "GET /a/c/df", + expectCode: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + + okHandler := func(c Context) error { + return c.String(http.StatusOK, c.Request().Method+" "+c.Path()) + } + notFoundHandler := func(c Context) error { + return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path()) + } + + e.GET("/", okHandler) + e.GET("/a/c/df", okHandler) + e.GET("/a/b*", okHandler) + e.PUT("/*", okHandler) + + e.RouteNotFound("/a/c/xx", notFoundHandler) // static + e.RouteNotFound("/a/:file", notFoundHandler) // param + e.RouteNotFound("/*", notFoundHandler) // any + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + assert.Equal(t, tc.expectCode, rec.Code) + assert.Equal(t, tc.expectRoute, rec.Body.String()) + }) + } +} + func TestEchoNotFound(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/files", nil) diff --git a/group.go b/group.go index 4f04a73a2..b9df5af9a 100644 --- a/group.go +++ b/group.go @@ -157,6 +157,13 @@ func (g *Group) File(path, file string, middleware ...MiddlewareFunc) RouteInfo return g.Add(http.MethodGet, path, handler, middleware...) } +// RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group. +// +// Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` +func (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo { + return g.Add(RouteNotFound, path, h, m...) +} + // Add implements `Echo#Add()` for sub-routes within the Group. Panics on error. func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo { ri, err := g.AddRoute(Route{ diff --git a/group_test.go b/group_test.go index 3914c0bd8..bd2157260 100644 --- a/group_test.go +++ b/group_test.go @@ -303,6 +303,71 @@ func TestGroup_TRACE(t *testing.T) { assert.Equal(t, `OK`, body) } +func TestGroup_RouteNotFound(t *testing.T) { + var testCases = []struct { + name string + whenURL string + expectRoute interface{} + expectCode int + }{ + { + name: "404, route to static not found handler /group/a/c/xx", + whenURL: "/group/a/c/xx", + expectRoute: "GET /group/a/c/xx", + expectCode: http.StatusNotFound, + }, + { + name: "404, route to path param not found handler /group/a/:file", + whenURL: "/group/a/echo.exe", + expectRoute: "GET /group/a/:file", + expectCode: http.StatusNotFound, + }, + { + name: "404, route to any not found handler /group/*", + whenURL: "/group/b/echo.exe", + expectRoute: "GET /group/*", + expectCode: http.StatusNotFound, + }, + { + name: "200, route /group/a/c/df to /group/a/c/df", + whenURL: "/group/a/c/df", + expectRoute: "GET /group/a/c/df", + expectCode: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + g := e.Group("/group") + + okHandler := func(c Context) error { + return c.String(http.StatusOK, c.Request().Method+" "+c.Path()) + } + notFoundHandler := func(c Context) error { + return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path()) + } + + g.GET("/", okHandler) + g.GET("/a/c/df", okHandler) + g.GET("/a/b*", okHandler) + g.PUT("/*", okHandler) + + g.RouteNotFound("/a/c/xx", notFoundHandler) // static + g.RouteNotFound("/a/:file", notFoundHandler) // param + g.RouteNotFound("/*", notFoundHandler) // any + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + assert.Equal(t, tc.expectCode, rec.Code) + assert.Equal(t, tc.expectRoute, rec.Body.String()) + }) + } +} + func TestGroup_Any(t *testing.T) { e := New() diff --git a/middleware/basic_auth.go b/middleware/basic_auth.go index 8caf848d9..3071eedb3 100644 --- a/middleware/basic_auth.go +++ b/middleware/basic_auth.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/base64" "errors" - "fmt" + "net/http" "strconv" "strings" @@ -72,9 +72,11 @@ func (config BasicAuthConfig) ToMiddleware() (echo.MiddlewareFunc, error) { continue } + // Invalid base64 shouldn't be treated as error + // instead should be treated as invalid client input b, errDecode := base64.StdEncoding.DecodeString(auth[l+1:]) if errDecode != nil { - lastError = echo.ErrUnauthorized.WithInternal(fmt.Errorf("invalid basic auth value: %w", errDecode)) + lastError = echo.NewHTTPError(http.StatusBadRequest).WithInternal(errDecode) continue } idx := bytes.IndexByte(b, ':') diff --git a/middleware/basic_auth_test.go b/middleware/basic_auth_test.go index 67640efaa..3d69ae84d 100644 --- a/middleware/basic_auth_test.go +++ b/middleware/basic_auth_test.go @@ -56,7 +56,7 @@ func TestBasicAuth(t *testing.T) { name: "nok, not base64 Authorization header", givenConfig: defaultConfig, whenAuth: []string{strings.ToUpper(basic) + " NOT_BASE64"}, - expectErr: "code=401, message=Unauthorized, internal=invalid basic auth value: illegal base64 data at input byte 3", + expectErr: "code=400, message=Bad Request, internal=illegal base64 data at input byte 3", }, { name: "nok, missing Authorization header", diff --git a/middleware/logger.go b/middleware/logger.go index bd2d3d932..0e525e749 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -22,6 +22,8 @@ type LoggerConfig struct { // Tags to construct the logger format. // // - time_unix + // - time_unix_milli + // - time_unix_micro // - time_unix_nano // - time_rfc3339 // - time_rfc3339_nano @@ -119,6 +121,10 @@ func (config LoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) { switch tag { case "time_unix": return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10)) + case "time_unix_milli": + return buf.WriteString(strconv.FormatInt(time.Now().UnixMilli(), 10)) + case "time_unix_micro": + return buf.WriteString(strconv.FormatInt(time.Now().UnixMicro(), 10)) case "time_unix_nano": return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10)) case "time_rfc3339": diff --git a/middleware/logger_test.go b/middleware/logger_test.go index 2f1230dda..455520f98 100644 --- a/middleware/logger_test.go +++ b/middleware/logger_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strconv" "strings" "testing" "time" @@ -172,6 +173,52 @@ func TestLoggerCustomTimestamp(t *testing.T) { assert.Error(t, err) } +func TestLoggerTemplateWithTimeUnixMilli(t *testing.T) { + buf := new(bytes.Buffer) + + e := echo.New() + e.Use(LoggerWithConfig(LoggerConfig{ + Format: `${time_unix_milli}`, + Output: buf, + })) + + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "OK") + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + unixMillis, err := strconv.ParseInt(buf.String(), 10, 64) + assert.NoError(t, err) + assert.WithinDuration(t, time.Unix(unixMillis/1000, 0), time.Now(), 3*time.Second) +} + +func TestLoggerTemplateWithTimeUnixMicro(t *testing.T) { + buf := new(bytes.Buffer) + + e := echo.New() + e.Use(LoggerWithConfig(LoggerConfig{ + Format: `${time_unix_micro}`, + Output: buf, + })) + + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "OK") + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + unixMicros, err := strconv.ParseInt(buf.String(), 10, 64) + assert.NoError(t, err) + assert.WithinDuration(t, time.Unix(unixMicros/1000000, 0), time.Now(), 3*time.Second) +} + func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) { e := echo.New() diff --git a/router.go b/router.go index bee6c2bd0..42d3ec900 100644 --- a/router.go +++ b/router.go @@ -214,18 +214,22 @@ type routeMethod struct { } type routeMethods struct { - connect *routeMethod - delete *routeMethod - get *routeMethod - head *routeMethod - options *routeMethod - patch *routeMethod - post *routeMethod - propfind *routeMethod - put *routeMethod - trace *routeMethod - report *routeMethod - anyOther map[string]*routeMethod + connect *routeMethod + delete *routeMethod + get *routeMethod + head *routeMethod + options *routeMethod + patch *routeMethod + post *routeMethod + propfind *routeMethod + put *routeMethod + trace *routeMethod + report *routeMethod + anyOther map[string]*routeMethod + + // notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases + notFoundHandler *routeMethod + allowHeader string } @@ -253,6 +257,9 @@ func (m *routeMethods) set(method string, r *routeMethod) { m.trace = r case REPORT: m.report = r + case RouteNotFound: + m.notFoundHandler = r + return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed default: if m.anyOther == nil { m.anyOther = make(map[string]*routeMethod) @@ -357,6 +364,7 @@ func (m *routeMethods) isHandler() bool { m.trace != nil || m.report != nil || len(m.anyOther) != 0 + // RouteNotFound/404 is not considered as a handler } // Routes returns all registered routes @@ -615,7 +623,12 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho } currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil } else if lcpLen < prefixLen { - // Split node + // Split node into two before we insert new node. + // This happens when we are inserting path that is submatch of any existing inserted paths. + // For example, we have node `/test` and now are about to insert `/te/*`. In that case + // 1. overlapping part is `/te` that is used as parent node + // 2. `st` is part from existing node that is not matching - it gets its own node (child to `/te`) + // 3. `/*` is the new part we are about to insert (child to `/te`) n := newNode( currentNode.kind, currentNode.prefix[lcpLen:], @@ -739,10 +752,8 @@ func (n *node) findStaticChild(l byte) *node { } func (n *node) findChildWithLabel(l byte) *node { - for _, c := range n.staticChildren { - if c.label == l { - return c - } + if c := n.findStaticChild(l); c != nil { + return c } if l == paramLabel { return n.paramChild @@ -755,11 +766,7 @@ func (n *node) findChildWithLabel(l byte) *node { func (n *node) setHandler(method string, r *routeMethod) { n.methods.set(method, r) - if r != nil && r.handler != nil { - n.isHandler = true - } else { - n.isHandler = n.methods.isHandler() - } + n.isHandler = n.methods.isHandler() } // Note: notFoundRouteInfo exists to avoid allocations when setting 404 RouteInfo to Context @@ -904,7 +911,7 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc { // No matching prefix, let's backtrack to the first possible alternative node of the decision path nk, ok := backtrackToNextNodeKind(staticKind) if !ok { - break // No other possibilities on the decision path + break // No other possibilities on the decision path, handler will be whatever context is reset to. } else if nk == paramKind { goto Param // NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently @@ -920,15 +927,21 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc { search = search[lcpLen:] searchIndex = searchIndex + lcpLen - // Finish routing if no remaining search and we are on a node with handler and matching method type - if search == "" && currentNode.isHandler { - // check if current node has handler registered for http method we are looking for. we store currentNode as - // best matching in case we do no find no more routes matching this path+method - if previousBestMatchNode == nil { - previousBestMatchNode = currentNode - } - if rMethod := currentNode.methods.find(req.Method); rMethod != nil { - matchedRouteMethod = rMethod + // Finish routing if is no request path remaining to search + if search == "" { + // in case of node that is handler we have exact method type match or something for 405 to use + if currentNode.isHandler { + // check if current node has handler registered for http method we are looking for. we store currentNode as + // best matching in case we do no find no more routes matching this path+method + if previousBestMatchNode == nil { + previousBestMatchNode = currentNode + } + if h := currentNode.methods.find(req.Method); h != nil { + matchedRouteMethod = h + break + } + } else if currentNode.methods.notFoundHandler != nil { + matchedRouteMethod = currentNode.methods.notFoundHandler break } } @@ -948,7 +961,8 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc { i := 0 l := len(search) if currentNode.isLeaf { - // when param node does not have any children then param node should act similarly to any node - consider all remaining search as match + // when param node does not have any children (path param is last piece of route path) then param node should + // act similarly to any node - consider all remaining search as match i = l } else { for ; i < l && search[i] != '/'; i++ { @@ -973,13 +987,16 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc { searchIndex += +len(search) search = "" - // check if current node has handler registered for http method we are looking for. we store currentNode as - // best matching in case we do no find no more routes matching this path+method + if rMethod := currentNode.methods.find(req.Method); rMethod != nil { + matchedRouteMethod = rMethod + break + } + // we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405 if previousBestMatchNode == nil { previousBestMatchNode = currentNode } - if rMethod := currentNode.methods.find(req.Method); rMethod != nil { - matchedRouteMethod = rMethod + if currentNode.methods.notFoundHandler != nil { + matchedRouteMethod = currentNode.methods.notFoundHandler break } } @@ -1021,7 +1038,13 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc { rPath = currentNode.originalPath rInfo = notFoundRouteInfo - if currentNode.isHandler { + if currentNode.methods.notFoundHandler != nil { + matchedRouteMethod = currentNode.methods.notFoundHandler + + rInfo = matchedRouteMethod.routeInfo + rPath = matchedRouteMethod.path + rHandler = matchedRouteMethod.handler + } else if currentNode.isHandler { rInfo = methodNotAllowedRouteInfo c.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader) diff --git a/router_test.go b/router_test.go index eed4016f9..83d3b1b2d 100644 --- a/router_test.go +++ b/router_test.go @@ -3190,6 +3190,232 @@ func TestDefaultRouter_OptionsMethodHandler(t *testing.T) { assert.Equal(t, "not empty", body) } +func TestRouter_RouteWhenNotFoundRouteWithNodeSplitting(t *testing.T) { + e := New() + r := e.router + + hf := func(c Context) error { + return c.String(http.StatusOK, c.RouteInfo().Name()) + } + r.Add(Route{Method: http.MethodGet, Path: "/test*", Handler: hf, Name: "0"}) + r.Add(Route{Method: RouteNotFound, Path: "/test*", Handler: hf, Name: "1"}) + r.Add(Route{Method: RouteNotFound, Path: "/test", Handler: hf, Name: "2"}) + + // Tree before: + // 1 `/` + // 1.1 `*` (any) ID=1 + // 1.2 `test` (static) ID=2 + // 1.2.1 `*` (any) ID=0 + + // node with path `test` has routeNotFound handler from previous Add call. Now when we insert `/te/st*` into router tree + // This means that node `test` is split into `te` and `st` nodes and new node `/st*` is inserted. + // On that split `/test` routeNotFound handler must not be lost. + r.Add(Route{Method: http.MethodGet, Path: "/te/st*", Handler: hf, Name: "3"}) + // Tree after: + // 1 `/` + // 1.1 `*` (any) ID=1 + // 1.2 `te` (static) + // 1.2.1 `st` (static) ID=2 + // 1.2.1.1 `*` (any) ID=0 + // 1.2.2 `/st` (static) + // 1.2.2.1 `*` (any) ID=3 + + _, body := request(http.MethodPut, "/test", e) + + assert.Equal(t, "2", body) +} + +func TestRouter_RouteWhenNotFoundRouteAnyKind(t *testing.T) { + var testCases = []struct { + name string + whenURL string + expectRoute interface{} + expectID int + expectParam map[string]string + }{ + { + name: "route not existent /xx to not found handler /*", + whenURL: "/xx", + expectRoute: "/*", + expectID: 4, + expectParam: map[string]string{"*": "xx"}, + }, + { + name: "route not existent /a/xx to not found handler /a/*", + whenURL: "/a/xx", + expectRoute: "/a/*", + expectID: 5, + expectParam: map[string]string{"*": "xx"}, + }, + { + name: "route not existent /a/c/dxxx to not found handler /a/c/d*", + whenURL: "/a/c/dxxx", + expectRoute: "/a/c/d*", + expectID: 6, + expectParam: map[string]string{"*": "xxx"}, + }, + { + name: "route /a/c/df to /a/c/df", + whenURL: "/a/c/df", + expectRoute: "/a/c/df", + expectID: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + e.contextPathParamAllocSize = 1 + r := e.router + + r.Add(Route{Method: http.MethodGet, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"}) + r.Add(Route{Method: http.MethodGet, Path: "/a/c/df", Handler: handlerHelper("ID", 1), Name: "1"}) + r.Add(Route{Method: http.MethodGet, Path: "/a/b*", Handler: handlerHelper("ID", 2), Name: "2"}) + r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 3), Name: "3"}) + + r.Add(Route{Method: RouteNotFound, Path: "/a/c/d*", Handler: handlerHelper("ID", 6), Name: "6"}) + r.Add(Route{Method: RouteNotFound, Path: "/a/*", Handler: handlerHelper("ID", 5), Name: "5"}) + r.Add(Route{Method: RouteNotFound, Path: "/*", Handler: handlerHelper("ID", 4), Name: "4"}) + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + c := e.NewContext(req, nil).(*DefaultContext) + + handler := r.Route(c) + handler(c) + + testValue, _ := c.Get("ID").(int) + assert.Equal(t, tc.expectID, testValue) + assert.Equal(t, tc.expectRoute, c.Path()) + for param, expectedValue := range tc.expectParam { + assert.Equal(t, expectedValue, c.PathParam(param)) + } + checkUnusedParamValues(t, c, tc.expectParam) + }) + } +} + +func TestRouter_RouteWhenNotFoundRouteParamKind(t *testing.T) { + var testCases = []struct { + name string + whenURL string + expectRoute interface{} + expectID int + expectParam map[string]string + }{ + { + name: "route not existent /xx to not found handler /:file", + whenURL: "/xx", + expectRoute: "/:file", + expectID: 4, + expectParam: map[string]string{"file": "xx"}, + }, + { + name: "route not existent /a/xx to not found handler /a/:file", + whenURL: "/a/xx", + expectRoute: "/a/:file", + expectID: 5, + expectParam: map[string]string{"file": "xx"}, + }, + { + name: "route not existent /a/c/dxxx to not found handler /a/c/d:file", + whenURL: "/a/c/dxxx", + expectRoute: "/a/c/d:file", + expectID: 6, + expectParam: map[string]string{"file": "xxx"}, + }, + { + name: "route /a/c/df to /a/c/df", + whenURL: "/a/c/df", + expectRoute: "/a/c/df", + expectID: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + e.contextPathParamAllocSize = 1 + r := e.router + + r.Add(Route{Method: http.MethodGet, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"}) + r.Add(Route{Method: http.MethodGet, Path: "/a/c/df", Handler: handlerHelper("ID", 1), Name: "1"}) + r.Add(Route{Method: http.MethodGet, Path: "/a/b*", Handler: handlerHelper("ID", 2), Name: "2"}) + r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 3), Name: "3"}) + + r.Add(Route{Method: RouteNotFound, Path: "/a/c/d:file", Handler: handlerHelper("ID", 6), Name: "6"}) + r.Add(Route{Method: RouteNotFound, Path: "/a/:file", Handler: handlerHelper("ID", 5), Name: "5"}) + r.Add(Route{Method: RouteNotFound, Path: "/:file", Handler: handlerHelper("ID", 4), Name: "4"}) + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + c := e.NewContext(req, nil).(*DefaultContext) + + handler := r.Route(c) + handler(c) + + testValue, _ := c.Get("ID").(int) + assert.Equal(t, tc.expectID, testValue) + assert.Equal(t, tc.expectRoute, c.Path()) + for param, expectedValue := range tc.expectParam { + assert.Equal(t, expectedValue, c.PathParam(param)) + } + checkUnusedParamValues(t, c, tc.expectParam) + }) + } +} + +func TestRouter_RouteWhenNotFoundRouteStaticKind(t *testing.T) { + // note: static not found handler is quite silly thing to have but we still support it + var testCases = []struct { + name string + whenURL string + expectRoute interface{} + expectID int + expectParam map[string]string + }{ + { + name: "route not existent / to not found handler /", + whenURL: "/", + expectRoute: "/", + expectID: 3, + expectParam: map[string]string{}, + }, + { + name: "route /a to /a", + whenURL: "/a", + expectRoute: "/a", + expectID: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + e.contextPathParamAllocSize = 1 + r := e.router + + r.Add(Route{Method: http.MethodPut, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"}) + r.Add(Route{Method: http.MethodGet, Path: "/a", Handler: handlerHelper("ID", 1), Name: "1"}) + r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 2), Name: "2"}) + + r.Add(Route{Method: RouteNotFound, Path: "/", Handler: handlerHelper("ID", 3), Name: "3"}) + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + c := e.NewContext(req, nil).(*DefaultContext) + + handler := r.Route(c) + handler(c) + + testValue, _ := c.Get("ID").(int) + assert.Equal(t, tc.expectID, testValue) + assert.Equal(t, tc.expectRoute, c.Path()) + for param, expectedValue := range tc.expectParam { + assert.Equal(t, expectedValue, c.PathParam(param)) + } + checkUnusedParamValues(t, c, tc.expectParam) + }) + } +} + type mySimpleRouter struct { route Route }