From ca7b437f3dc99a9f83f946d923becc3820b8734f Mon Sep 17 00:00:00 2001 From: De Ville Weppenaar Date: Wed, 17 Apr 2024 16:28:23 -0700 Subject: [PATCH] contrib/dimfeld/httptreemux.v5: fix route and name for 30X redirects The httptreemux router has a redirect behaviour that is invoked when a request URL and matched route only differs in a trailing slash. The default behaviour is to respond with a 301 (moved permanently) to redirect the client to the exact path of the matched handler. We previously patched one of the two scenarios where this occurs in #2332. The changes in this commit addresses the other scenario for the 301 redirect, but also the 307 and 308 redirect options. Several tests were included to cover the various scenarios. We also standardize the resource name in the event that there is a trailing slash missmatch between request URL and matched handler. We always truncate the trailing slash in the resource name. Fixes #2663 --- contrib/dimfeld/httptreemux.v5/httptreemux.go | 52 +- .../httptreemux.v5/httptreemux_test.go | 1174 ++++++++++++++++- 2 files changed, 1198 insertions(+), 28 deletions(-) diff --git a/contrib/dimfeld/httptreemux.v5/httptreemux.go b/contrib/dimfeld/httptreemux.v5/httptreemux.go index 6169ac4b7e..512b94561a 100644 --- a/contrib/dimfeld/httptreemux.v5/httptreemux.go +++ b/contrib/dimfeld/httptreemux.v5/httptreemux.go @@ -112,18 +112,26 @@ func getRoute(router *httptreemux.TreeMux, w http.ResponseWriter, req *http.Requ if !found { return "", false } + routeLen := len(route) + trailingSlash := route[routeLen-1] == '/' && routeLen > 1 - // Check for redirecting route due to trailing slash for parameters. - // The redirecting behaviour originates from httptreemux router. - if lr.StatusCode == http.StatusMovedPermanently && strings.HasSuffix(route, "/") { + // Retry the population of lookup result parameters. + // If the initial attempt to populate the parameters fails, clone the request and modify the URI and URL Path. + // Depending on whether the route has a trailing slash or not, it will either add or remove the trailing slash and retry the lookup. + if routerRedirectEnabled(router) && isSupportedRedirectStatus(lr.StatusCode) && lr.Params == nil { rReq := req.Clone(req.Context()) - rReq.RequestURI = strings.TrimSuffix(rReq.RequestURI, "/") - rReq.URL.Path = strings.TrimSuffix(rReq.URL.Path, "/") - - lr, found = router.Lookup(w, rReq) - if !found { - return "", false + if trailingSlash { + // if the route has a trailing slash, remove it + rReq.RequestURI = strings.TrimSuffix(rReq.RequestURI, "/") + rReq.URL.Path = strings.TrimSuffix(rReq.URL.Path, "/") + } else { + // if the route does not have a trailing slash, add one + rReq.RequestURI = rReq.RequestURI + "/" + rReq.URL.Path = rReq.URL.Path + "/" } + // no need to check found again + // we already matched a route and we are only trying to populate the lookup result params + lr, _ = router.Lookup(w, rReq) } for k, v := range lr.Params { @@ -139,5 +147,31 @@ func getRoute(router *httptreemux.TreeMux, w http.ResponseWriter, req *http.Requ newP = "/:" + k route = strings.Replace(route, oldP, newP, 1) } + + // remove trailing slash from route to standardize returned value + // the router does not allow you to register two matching routes with the only difference being a trailing slash + // this only affects the resulting returned value and not the actual request URL set on tag http.url + if trailingSlash { + route = strings.TrimSuffix(route, "/") + } + return route, true } + +// isSupportedRedirectStatus checks if the given HTTP status code is a supported redirect status. +// It returns true if the status code is either StatusMovedPermanently, StatusTemporaryRedirect, or StatusPermanentRedirect. +// Otherwise, it returns false. +func isSupportedRedirectStatus(status int) bool { + return status == http.StatusMovedPermanently || + status == http.StatusTemporaryRedirect || + status == http.StatusPermanentRedirect +} + +// routerRedirectEnabled checks if the redirection is enabled on the router. +// It returns true if either RedirectCleanPath or RedirectTrailingSlash is enabled, +// and the RedirectBehavior is not set to UseHandler. Otherwise, it returns false. +// This function is used to determine whether to perform redirections based on the router's configuration. +func routerRedirectEnabled(router *httptreemux.TreeMux) bool { + return (router.RedirectCleanPath || router.RedirectTrailingSlash) && + router.RedirectBehavior != httptreemux.UseHandler +} diff --git a/contrib/dimfeld/httptreemux.v5/httptreemux_test.go b/contrib/dimfeld/httptreemux.v5/httptreemux_test.go index 017f2feedd..d9970917c9 100644 --- a/contrib/dimfeld/httptreemux.v5/httptreemux_test.go +++ b/contrib/dimfeld/httptreemux.v5/httptreemux_test.go @@ -238,18 +238,777 @@ func TestNamingSchema(t *testing.T) { namingschematest.NewHTTPServerTest(genSpans, "http.router")(t) } -func TestTrailingSlashRoutes(t *testing.T) { - t.Run("unknown if no handler matches", func(t *testing.T) { +func TestTrailingSlashRoutesWithBehaviorRedirect301(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { assert := assert.New(t) mt := mocktracer.Start() defer mt.Stop() - url := "/unknown/" - r := httptest.NewRequest(http.MethodGet, url, nil) + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect301 // default + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorRedirect307(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect307 + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorRedirect308(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect308 + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorUseHandler(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.UseHandler + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() - router().ServeHTTP(w, r) - assert.Equal(404, w.Code) - assert.Equal("404 page not found\n", w.Body.String()) + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") spans := mt.FinishedSpans() assert.Equal(1, len(spans)) @@ -260,23 +1019,31 @@ func TestTrailingSlashRoutes(t *testing.T) { assert.Equal("GET unknown", s.Tag(ext.ResourceName)) assert.Equal("404", s.Tag(ext.HTTPCode)) assert.Equal("GET", s.Tag(ext.HTTPMethod)) - assert.Equal("http://example.com/unknown/", s.Tag(ext.HTTPURL)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) assert.Equal("testvalue", s.Tag("testkey")) assert.Nil(s.Tag(ext.Error)) assert.NotContains(s.Tags(), ext.HTTPRoute) }) - t.Run("parametrizes URL with trailing slash", func(t *testing.T) { + t.Run("GET /api/:parameter", func(t *testing.T) { assert := assert.New(t) mt := mocktracer.Start() defer mt.Stop() - url := "/api/paramvalue/" + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramvalue/" // with trailing slash r := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() - router().ServeHTTP(w, r) - assert.Equal(301, w.Code) - assert.Contains(w.Body.String(), "Moved Permanently") + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") spans := mt.FinishedSpans() assert.Equal(1, len(spans)) @@ -284,14 +1051,384 @@ func TestTrailingSlashRoutes(t *testing.T) { s := spans[0] assert.Equal("http.request", s.OperationName()) assert.Equal("my-service", s.Tag(ext.ServiceName)) - assert.Equal("GET /api/:parameter/", s.Tag(ext.ResourceName)) - assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) assert.Equal("GET", s.Tag(ext.HTTPMethod)) assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) assert.Equal("testvalue", s.Tag("testkey")) assert.Nil(s.Tag(ext.Error)) assert.Contains(s.Tags(), ext.HTTPRoute) }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestIsSupportedRedirectStatus(t *testing.T) { + tests := []struct { + name string + status int + want bool + }{ + { + name: "Test with status 301", + status: 301, + want: true, + }, + { + name: "Test with status 302", + status: 302, + want: false, + }, + { + name: "Test with status 303", + status: 303, + want: false, + }, + { + name: "Test with status 307", + status: 307, + want: true, + }, + { + name: "Test with status 308", + status: 308, + want: true, + }, + { + name: "Test with status 400", + status: 400, + want: false, + }, + { + name: "Test with status 0", + status: 0, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isSupportedRedirectStatus(tt.status); got != tt.want { + t.Errorf("isSupportedRedirectStatus() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRouterRedirectEnabled(t *testing.T) { + tests := []struct { + name string + cleanPath bool + trailingSlash bool + redirectBehaviour httptreemux.RedirectBehavior + + want bool + }{ + // httptreemux.Redirect301 + { + name: "Test Redirect301 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect301, + want: false, + }, + // httptreemux.Redirect307 + { + name: "Test Redirect307 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect307, + want: false, + }, + // httptreemux.Redirect308 + { + name: "Test Redirect308 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect308, + want: false, + }, + // httptreemux.UseHandler + { + name: "Test UseHandler with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router := httptreemux.New() + router.RedirectCleanPath = tt.cleanPath + router.RedirectTrailingSlash = tt.trailingSlash + router.RedirectBehavior = tt.redirectBehaviour + + if got := routerRedirectEnabled(router); got != tt.want { + t.Errorf("routerRedirectEnabled() = %v, want %v", got, tt.want) + } + }) + } } func router() http.Handler { @@ -302,7 +1439,9 @@ func router() http.Handler { router.GET("/200", handler200) router.GET("/500", handler500) - router.GET("/api/:parameter", handlerDummy) + + router.GET("/api/:parameter", handler200) + router.GET("/api/:param1/:param2/:param3", handler200) return router } @@ -314,6 +1453,3 @@ func handler200(w http.ResponseWriter, _ *http.Request, _ map[string]string) { func handler500(w http.ResponseWriter, _ *http.Request, _ map[string]string) { http.Error(w, "500!", http.StatusInternalServerError) } -func handlerDummy(w http.ResponseWriter, _ *http.Request, _ map[string]string) { - w.WriteHeader(http.StatusAccepted) -}