package router import ( "net" "testing" assert "git.sharkk.net/Go/Assert" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttputil" ) // simpleHandler implements the Handler interface type testHandler struct { fn func(ctx Ctx, params []string) } func (h *testHandler) Serve(ctx Ctx, params []string) { h.fn(ctx, params) } // newHandler creates a simple Handler from a function func newHandler(fn func(ctx Ctx, params []string)) Handler { return &testHandler{fn: fn} } // performRequest is a helper function to test the router func performRequest(r *Router, method, path string) (*fasthttp.RequestCtx, bool) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(method) ctx.Request.SetRequestURI(path) handler, params, found := r.Lookup(method, path) if found { handler.Serve(ctx, params) } return ctx, found } func TestRootPath(t *testing.T) { r := New() r.Get("/", func(ctx Ctx, params []string) { // No-op for testing }) _, found := performRequest(r, "GET", "/") assert.True(t, found) } func TestStaticPath(t *testing.T) { r := New() r.Get("/users/all", func(ctx Ctx, params []string) { // No-op for testing }) _, found := performRequest(r, "GET", "/users/all") assert.True(t, found) } func TestSingleParameter(t *testing.T) { r := New() called := false r.Get("/users/[id]", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "123") }) _, found := performRequest(r, "GET", "/users/123") assert.True(t, found) assert.True(t, called) } func TestMultipleParameters(t *testing.T) { r := New() called := false r.Get("/users/[id]/posts/[postId]", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "123") assert.Equal(t, params[1], "456") }) _, found := performRequest(r, "GET", "/users/123/posts/456") assert.True(t, found) assert.True(t, called) } func TestNonExistentPath(t *testing.T) { r := New() r.Get("/users/[id]", func(ctx Ctx, params []string) { // No-op for testing }) _, found := performRequest(r, "GET", "/posts/123") assert.False(t, found) } func TestWrongMethod(t *testing.T) { r := New() r.Get("/users/[id]", func(ctx Ctx, params []string) { // No-op for testing }) _, found := performRequest(r, "POST", "/users/123") assert.False(t, found) } func TestTrailingSlash(t *testing.T) { r := New() called := false r.Get("/users/[id]", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "123") }) _, found := performRequest(r, "GET", "/users/123/") assert.True(t, found) assert.True(t, called) } func TestDifferentMethods(t *testing.T) { r := New() handler := func(ctx Ctx, params []string) {} r.Get("/test", handler) r.Post("/test", handler) r.Put("/test", handler) r.Patch("/test", handler) r.Delete("/test", handler) methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"} for _, method := range methods { t.Run(method, func(t *testing.T) { _, found := performRequest(r, method, "/test") assert.True(t, found) }) } } func TestWildcardPath(t *testing.T) { r := New() t.Run("simple wildcard", func(t *testing.T) { called := false err := r.Get("/files/*path", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "docs/report.pdf") }) assert.Nil(t, err) _, found := performRequest(r, "GET", "/files/docs/report.pdf") assert.True(t, found) assert.True(t, called) }) t.Run("wildcard with empty path", func(t *testing.T) { called := false err := r.Get("/download/*filepath", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "") }) assert.Nil(t, err) _, found := performRequest(r, "GET", "/download/") assert.True(t, found) assert.True(t, called) }) t.Run("wildcard with parameter", func(t *testing.T) { called := false err := r.Get("/users/[id]/*action", func(ctx Ctx, params []string) { called = true assert.Equal(t, params[0], "123") assert.Equal(t, params[1], "settings/profile/avatar") }) assert.Nil(t, err) _, found := performRequest(r, "GET", "/users/123/settings/profile/avatar") assert.True(t, found) assert.True(t, called) }) t.Run("multiple wildcards not allowed", func(t *testing.T) { err := r.Get("/api/*version/*path", func(ctx Ctx, params []string) {}) assert.NotNil(t, err) }) t.Run("non-last wildcard not allowed", func(t *testing.T) { err := r.Get("/api/*version/users", func(ctx Ctx, params []string) {}) assert.NotNil(t, err) }) } // Middleware Tests func TestMiddleware(t *testing.T) { t.Run("global middleware", func(t *testing.T) { r := New() // Track middleware execution executed := false r.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { executed = true next.Serve(ctx, params) }) }) r.Get("/test", func(ctx Ctx, params []string) {}) _, found := performRequest(r, "GET", "/test") assert.True(t, found) assert.True(t, executed) }) t.Run("multiple middleware", func(t *testing.T) { r := New() // Track middleware execution order order := []int{} r.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 1) next.Serve(ctx, params) order = append(order, 4) }) }) r.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 2) next.Serve(ctx, params) order = append(order, 3) }) }) r.Get("/test", func(ctx Ctx, params []string) { order = append(order, 0) }) _, found := performRequest(r, "GET", "/test") assert.True(t, found) // Check middleware execution order (first middleware wraps second) assert.Equal(t, len(order), 5) assert.Equal(t, order[0], 1) // First middleware enter assert.Equal(t, order[1], 2) // Second middleware enter assert.Equal(t, order[2], 0) // Handler assert.Equal(t, order[3], 3) // Second middleware exit assert.Equal(t, order[4], 4) // First middleware exit }) t.Run("route-specific middleware", func(t *testing.T) { r := New() executed := false middleware := func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { executed = true next.Serve(ctx, params) }) } r.WithMiddleware(middleware).Get("/test", func(ctx Ctx, params []string) {}) _, found := performRequest(r, "GET", "/test") assert.True(t, found) assert.True(t, executed) }) } // Group Tests func TestGroup(t *testing.T) { t.Run("simple group", func(t *testing.T) { r := New() // Create API group api := r.Group("/api") api.Get("/users", func(ctx Ctx, params []string) {}) _, found := performRequest(r, "GET", "/api/users") assert.True(t, found) }) t.Run("nested groups", func(t *testing.T) { r := New() // Create nested groups api := r.Group("/api") v1 := api.Group("/v1") v1.Get("/users", func(ctx Ctx, params []string) {}) _, found := performRequest(r, "GET", "/api/v1/users") assert.True(t, found) }) t.Run("group middleware", func(t *testing.T) { r := New() executed := false // Create group with middleware api := r.Group("/api") api.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { executed = true next.Serve(ctx, params) }) }) api.Get("/users", func(ctx Ctx, params []string) {}) _, found := performRequest(r, "GET", "/api/users") assert.True(t, found) assert.True(t, executed) }) t.Run("nested group middleware", func(t *testing.T) { r := New() order := []int{} // Create group with middleware api := r.Group("/api") api.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 1) next.Serve(ctx, params) }) }) // Create nested group with additional middleware v1 := api.Group("/v1") v1.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 2) next.Serve(ctx, params) }) }) v1.Get("/users", func(ctx Ctx, params []string) { order = append(order, 3) }) _, found := performRequest(r, "GET", "/api/v1/users") assert.True(t, found) // Check middleware execution order assert.Equal(t, len(order), 3) assert.Equal(t, order[0], 1) // First middleware (from api group) assert.Equal(t, order[1], 2) // Second middleware (from v1 group) assert.Equal(t, order[2], 3) // Handler }) t.Run("route-specific middleware in group", func(t *testing.T) { r := New() order := []int{} // Create group with middleware api := r.Group("/api") api.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 1) next.Serve(ctx, params) }) }) // Add route with specific middleware api.WithMiddleware(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { order = append(order, 2) next.Serve(ctx, params) }) }).Get("/users", func(ctx Ctx, params []string) { order = append(order, 3) }) _, found := performRequest(r, "GET", "/api/users") assert.True(t, found) // Check middleware execution order assert.Equal(t, len(order), 3) assert.Equal(t, order[0], 1) // Group middleware assert.Equal(t, order[1], 2) // Route-specific middleware assert.Equal(t, order[2], 3) // Handler }) } // Tests for standard handlers func TestStandardHandlers(t *testing.T) { r := New() handlerCalled := false standardHandler := func(ctx *fasthttp.RequestCtx) { handlerCalled = true } r.Get("/standard", StandardHandler(standardHandler)) _, found := performRequest(r, "GET", "/standard") assert.True(t, found) assert.True(t, handlerCalled) } // Test complete HTTP handler chain func TestHandlerChain(t *testing.T) { r := New() handlerCalled := false r.Get("/complete", func(ctx Ctx, params []string) { handlerCalled = true }) _, found := performRequest(r, "GET", "/complete") assert.True(t, found) assert.True(t, handlerCalled) } // Test advanced routes with multiple parameters func TestAdvancedRoutes(t *testing.T) { r := New() called := false r.Get("/api/[version]/users/[id]/profiles/[profile]", func(ctx Ctx, params []string) { called = true assert.Equal(t, len(params), 3) assert.Equal(t, params[0], "v1") assert.Equal(t, params[1], "123") assert.Equal(t, params[2], "basic") }) _, found := performRequest(r, "GET", "/api/v1/users/123/profiles/basic") assert.True(t, found) assert.True(t, called) wildcardCalled := false r.Get("/files/[type]", func(ctx Ctx, params []string) { wildcardCalled = true assert.Equal(t, params[0], "pdf") }) _, found = performRequest(r, "GET", "/files/pdf") assert.True(t, found) assert.True(t, wildcardCalled) } // Test 404 handling func TestNotFoundHandling(t *testing.T) { r := New() r.Get("/exists", func(ctx Ctx, params []string) { ctx.SetStatusCode(fasthttp.StatusOK) }) ln := fasthttputil.NewInmemoryListener() defer ln.Close() go fasthttp.Serve(ln, r.Handler()) req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://example.com/not-exists") req.Header.SetMethod("GET") client := &fasthttp.HostClient{ Addr: "example.com", Dial: func(addr string) (net.Conn, error) { return ln.Dial() }, } err := client.Do(req, resp) assert.Nil(t, err) assert.Equal(t, resp.StatusCode(), fasthttp.StatusNotFound) } // Benchmarks func BenchmarkRouterLookup(b *testing.B) { r := New() handler := func(ctx Ctx, params []string) {} // Setup routes for benchmarking r.Get("/", handler) r.Get("/users/all", handler) r.Get("/users/[id]", handler) r.Get("/users/[id]/posts/[postId]", handler) b.Run("root", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/") } }) b.Run("static", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/users/all") } }) b.Run("single_param", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/users/123") } }) b.Run("multi_param", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/users/123/posts/456") } }) b.Run("not_found", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/nonexistent/path") } }) } func BenchmarkParallelLookup(b *testing.B) { r := New() handler := func(ctx Ctx, params []string) {} r.Get("/users/[id]", handler) r.Get("/posts/[id]/comments", handler) r.Get("/products/[category]/[id]", handler) b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { switch i % 3 { case 0: r.Lookup("GET", "/users/123") case 1: r.Lookup("GET", "/posts/456/comments") case 2: r.Lookup("GET", "/products/electronics/789") } i++ } }) } func BenchmarkWildcardLookup(b *testing.B) { r := New() handler := func(ctx Ctx, params []string) {} // Setup routes for benchmarking r.Get("/files/*path", handler) r.Get("/users/[id]/*action", handler) b.Run("simple_wildcard", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/files/documents/reports/2024/q1.pdf") } }) b.Run("wildcard_with_param", func(b *testing.B) { for i := 0; i < b.N; i++ { r.Lookup("GET", "/users/123/settings/profile/avatar") } }) } func BenchmarkMiddleware(b *testing.B) { passthrough := func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { next.Serve(ctx, params) }) } b.Run("no_middleware", func(b *testing.B) { r := New() r.Get("/test", func(ctx Ctx, params []string) {}) dummyCtx := &fasthttp.RequestCtx{} b.ResetTimer() for i := 0; i < b.N; i++ { h, params, _ := r.Lookup("GET", "/test") h.Serve(dummyCtx, params) } }) b.Run("one_middleware", func(b *testing.B) { r := New() r.Use(passthrough) r.Get("/test", func(ctx Ctx, params []string) {}) dummyCtx := &fasthttp.RequestCtx{} b.ResetTimer() for i := 0; i < b.N; i++ { h, params, _ := r.Lookup("GET", "/test") h.Serve(dummyCtx, params) } }) b.Run("five_middleware", func(b *testing.B) { r := New() for i := 0; i < 5; i++ { r.Use(passthrough) } r.Get("/test", func(ctx Ctx, params []string) {}) dummyCtx := &fasthttp.RequestCtx{} b.ResetTimer() for i := 0; i < b.N; i++ { h, params, _ := r.Lookup("GET", "/test") h.Serve(dummyCtx, params) } }) } func BenchmarkGroups(b *testing.B) { handler := func(ctx Ctx, params []string) {} b.Run("flat_route", func(b *testing.B) { r := New() r.Get("/api/v1/users", handler) b.ResetTimer() for i := 0; i < b.N; i++ { r.Lookup("GET", "/api/v1/users") } }) b.Run("grouped_route", func(b *testing.B) { r := New() api := r.Group("/api") v1 := api.Group("/v1") v1.Get("/users", handler) b.ResetTimer() for i := 0; i < b.N; i++ { r.Lookup("GET", "/api/v1/users") } }) b.Run("grouped_route_with_middleware", func(b *testing.B) { r := New() api := r.Group("/api") api.Use(func(next Handler) Handler { return newHandler(func(ctx Ctx, params []string) { next.Serve(ctx, params) }) }) v1 := api.Group("/v1") v1.Get("/users", handler) dummyCtx := &fasthttp.RequestCtx{} b.ResetTimer() for i := 0; i < b.N; i++ { h, params, _ := r.Lookup("GET", "/api/v1/users") h.Serve(dummyCtx, params) } }) } func BenchmarkFasthttpServer(b *testing.B) { // Create a local listener ln := fasthttputil.NewInmemoryListener() defer ln.Close() r := New() r.Get("/users", func(ctx Ctx, params []string) {}) r.Get("/users/all", func(ctx Ctx, params []string) {}) r.Get("/users/[id]", func(ctx Ctx, params []string) {}) // Start the server go fasthttp.Serve(ln, r.Handler()) // Create a client client := &fasthttp.HostClient{ Addr: "example.com", Dial: func(addr string) (net.Conn, error) { return ln.Dial() }, } b.Run("root", func(b *testing.B) { req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://example.com/") req.Header.SetMethod("GET") b.ResetTimer() for i := 0; i < b.N; i++ { client.Do(req, resp) } }) b.Run("static_path", func(b *testing.B) { req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://example.com/users/all") req.Header.SetMethod("GET") b.ResetTimer() for i := 0; i < b.N; i++ { client.Do(req, resp) } }) b.Run("param_path", func(b *testing.B) { req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) req.SetRequestURI("http://example.com/users/123") req.Header.SetMethod("GET") b.ResetTimer() for i := 0; i < b.N; i++ { client.Do(req, resp) } }) } func BenchmarkComparison(b *testing.B) { // Custom router setup customRouter := New() handler := func(ctx Ctx, params []string) {} customRouter.Get("/", handler) customRouter.Get("/users/all", handler) customRouter.Get("/users/[id]", handler) customRouter.Get("/users/[id]/posts/[postId]", handler) // Simple fasthttp router setup (similar to http.ServeMux) routes := map[string]fasthttp.RequestHandler{ "/": func(ctx *fasthttp.RequestCtx) {}, "/users/all": func(ctx *fasthttp.RequestCtx) {}, "/users/": func(ctx *fasthttp.RequestCtx) {}, // Best equivalent for dynamic routes } muxHandler := func(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) if handler, ok := routes[path]; ok { handler(ctx) return } // Try prefix match (like http.ServeMux does) for pattern, handler := range routes { if len(pattern) > 0 && pattern[len(pattern)-1] == '/' && len(path) >= len(pattern) && path[:len(pattern)] == pattern { handler(ctx) return } } ctx.SetStatusCode(fasthttp.StatusNotFound) } // Root path b.Run("root_path", func(b *testing.B) { b.Run("custom", func(b *testing.B) { for i := 0; i < b.N; i++ { customRouter.Lookup("GET", "/") } }) b.Run("mux", func(b *testing.B) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/") for i := 0; i < b.N; i++ { muxHandler(ctx) } }) }) // Static path b.Run("static_path", func(b *testing.B) { b.Run("custom", func(b *testing.B) { for i := 0; i < b.N; i++ { customRouter.Lookup("GET", "/users/all") } }) b.Run("mux", func(b *testing.B) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/users/all") for i := 0; i < b.N; i++ { muxHandler(ctx) } }) }) // Dynamic path b.Run("dynamic_path", func(b *testing.B) { b.Run("custom", func(b *testing.B) { for i := 0; i < b.N; i++ { customRouter.Lookup("GET", "/users/123") } }) b.Run("mux", func(b *testing.B) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/users/123") for i := 0; i < b.N; i++ { muxHandler(ctx) } }) }) // Not found b.Run("not_found", func(b *testing.B) { b.Run("custom", func(b *testing.B) { for i := 0; i < b.N; i++ { customRouter.Lookup("GET", "/nonexistent/path") } }) b.Run("mux", func(b *testing.B) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod("GET") ctx.Request.SetRequestURI("/nonexistent/path") for i := 0; i < b.N; i++ { muxHandler(ctx) } }) }) }