Router/router_test.go
2025-04-26 11:12:58 -05:00

595 lines
14 KiB
Go

package router
import (
"net/http"
"testing"
assert "git.sharkk.net/Go/Assert"
)
// Create a test handler that implements the Handler interface
func testHandler(t *testing.T, expectedParams []string) Handler {
return FuncHandler(func(params []string) {
assert.Equal(t, len(params), len(expectedParams))
for i, expected := range expectedParams {
assert.Equal(t, params[i], expected)
}
})
}
func TestRootPath(t *testing.T) {
r := New()
r.Get("/", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/")
assert.True(t, found)
h.Serve(params)
}
func TestStaticPath(t *testing.T) {
r := New()
r.Get("/users/all", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/users/all")
assert.True(t, found)
h.Serve(params)
}
func TestSingleParameter(t *testing.T) {
r := New()
r.Get("/users/[id]", testHandler(t, []string{"123"}))
h, params, found := r.Lookup("GET", "/users/123")
assert.True(t, found)
h.Serve(params)
}
func TestMultipleParameters(t *testing.T) {
r := New()
r.Get("/users/[id]/posts/[postId]", testHandler(t, []string{"123", "456"}))
h, params, found := r.Lookup("GET", "/users/123/posts/456")
assert.True(t, found)
h.Serve(params)
}
func TestNonExistentPath(t *testing.T) {
r := New()
r.Get("/users/[id]", testHandler(t, nil))
_, _, found := r.Lookup("GET", "/posts/123")
assert.False(t, found)
}
func TestWrongMethod(t *testing.T) {
r := New()
r.Get("/users/[id]", testHandler(t, nil))
_, _, found := r.Lookup("POST", "/users/123")
assert.False(t, found)
}
func TestTrailingSlash(t *testing.T) {
r := New()
r.Get("/users/[id]", testHandler(t, []string{"123"}))
h, params, found := r.Lookup("GET", "/users/123/")
assert.True(t, found)
h.Serve(params)
}
func TestDifferentMethods(t *testing.T) {
r := New()
r.Get("/test", testHandler(t, nil))
r.Post("/test", testHandler(t, nil))
r.Put("/test", testHandler(t, nil))
r.Patch("/test", testHandler(t, nil))
r.Delete("/test", testHandler(t, nil))
methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
for _, method := range methods {
t.Run(method, func(t *testing.T) {
_, _, found := r.Lookup(method, "/test")
assert.True(t, found)
})
}
}
func TestWildcardPath(t *testing.T) {
r := New()
t.Run("simple wildcard", func(t *testing.T) {
err := r.Get("/files/*path", testHandler(t, []string{"docs/report.pdf"}))
assert.Nil(t, err)
h, params, found := r.Lookup("GET", "/files/docs/report.pdf")
assert.True(t, found)
h.Serve(params)
})
t.Run("wildcard with empty path", func(t *testing.T) {
err := r.Get("/download/*filepath", testHandler(t, []string{""}))
assert.Nil(t, err)
h, params, found := r.Lookup("GET", "/download/")
assert.True(t, found)
h.Serve(params)
})
t.Run("wildcard with parameter", func(t *testing.T) {
err := r.Get("/users/[id]/*action", testHandler(t, []string{"123", "settings/profile/avatar"}))
assert.Nil(t, err)
h, params, found := r.Lookup("GET", "/users/123/settings/profile/avatar")
assert.True(t, found)
h.Serve(params)
})
t.Run("multiple wildcards not allowed", func(t *testing.T) {
err := r.Get("/api/*version/*path", testHandler(t, nil))
assert.NotNil(t, err)
})
t.Run("non-last wildcard not allowed", func(t *testing.T) {
err := r.Get("/api/*version/users", testHandler(t, nil))
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 FuncHandler(func(params []string) {
executed = true
next.Serve(params)
})
})
r.Get("/test", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/test")
assert.True(t, found)
h.Serve(params)
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 FuncHandler(func(params []string) {
order = append(order, 1)
next.Serve(params)
order = append(order, 4)
})
})
r.Use(func(next Handler) Handler {
return FuncHandler(func(params []string) {
order = append(order, 2)
next.Serve(params)
order = append(order, 3)
})
})
r.Get("/test", FuncHandler(func(params []string) {
order = append(order, 0)
}))
h, params, found := r.Lookup("GET", "/test")
assert.True(t, found)
h.Serve(params)
// 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 FuncHandler(func(params []string) {
executed = true
next.Serve(params)
})
}
r.WithMiddleware(middleware).Get("/test", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/test")
assert.True(t, found)
h.Serve(params)
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", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/api/users")
assert.True(t, found)
h.Serve(params)
})
t.Run("nested groups", func(t *testing.T) {
r := New()
// Create nested groups
api := r.Group("/api")
v1 := api.Group("/v1")
v1.Get("/users", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/api/v1/users")
assert.True(t, found)
h.Serve(params)
})
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 FuncHandler(func(params []string) {
executed = true
next.Serve(params)
})
})
api.Get("/users", testHandler(t, nil))
h, params, found := r.Lookup("GET", "/api/users")
assert.True(t, found)
h.Serve(params)
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 FuncHandler(func(params []string) {
order = append(order, 1)
next.Serve(params)
})
})
// Create nested group with additional middleware
v1 := api.Group("/v1")
v1.Use(func(next Handler) Handler {
return FuncHandler(func(params []string) {
order = append(order, 2)
next.Serve(params)
})
})
v1.Get("/users", FuncHandler(func(params []string) {
order = append(order, 3)
}))
h, params, found := r.Lookup("GET", "/api/v1/users")
assert.True(t, found)
h.Serve(params)
// 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 FuncHandler(func(params []string) {
order = append(order, 1)
next.Serve(params)
})
})
// Add route with specific middleware
api.WithMiddleware(func(next Handler) Handler {
return FuncHandler(func(params []string) {
order = append(order, 2)
next.Serve(params)
})
}).Get("/users", FuncHandler(func(params []string) {
order = append(order, 3)
}))
h, params, found := r.Lookup("GET", "/api/users")
assert.True(t, found)
h.Serve(params)
// 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
})
}
// Benchmarks
func BenchmarkRouterLookup(b *testing.B) {
r := New()
h := FuncHandler(func(params []string) {})
// Setup routes for benchmarking
r.Get("/", h)
r.Get("/users/all", h)
r.Get("/users/[id]", h)
r.Get("/users/[id]/posts/[postId]", h)
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()
h := FuncHandler(func(params []string) {})
r.Get("/users/[id]", h)
r.Get("/posts/[id]/comments", h)
r.Get("/products/[category]/[id]", h)
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()
h := FuncHandler(func(params []string) {})
// Setup routes for benchmarking
r.Get("/files/*path", h)
r.Get("/users/[id]/*action", h)
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 FuncHandler(func(params []string) {
next.Serve(params)
})
}
simpleHandler := FuncHandler(func(params []string) {})
b.Run("no_middleware", func(b *testing.B) {
r := New()
r.Get("/test", simpleHandler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(params)
}
})
b.Run("one_middleware", func(b *testing.B) {
r := New()
r.Use(passthrough)
r.Get("/test", simpleHandler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(params)
}
})
b.Run("five_middleware", func(b *testing.B) {
r := New()
for i := 0; i < 5; i++ {
r.Use(passthrough)
}
r.Get("/test", simpleHandler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(params)
}
})
}
func BenchmarkGroups(b *testing.B) {
simpleHandler := FuncHandler(func(params []string) {})
b.Run("flat_route", func(b *testing.B) {
r := New()
r.Get("/api/v1/users", simpleHandler)
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", simpleHandler)
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 FuncHandler(func(params []string) {
next.Serve(params)
})
})
v1 := api.Group("/v1")
v1.Get("/users", simpleHandler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/api/v1/users")
h.Serve(params)
}
})
}
func BenchmarkComparison(b *testing.B) {
// Custom router setup
customRouter := New()
customRouter.Get("/", FuncHandler(func(params []string) {}))
customRouter.Get("/users/all", FuncHandler(func(params []string) {}))
customRouter.Get("/users/[id]", FuncHandler(func(params []string) {}))
customRouter.Get("/users/[id]/posts/[postId]", FuncHandler(func(params []string) {}))
// Standard mux setup
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
mux.HandleFunc("/users/all", func(w http.ResponseWriter, r *http.Request) {})
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {}) // Best equivalent for dynamic routes
// 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) {
req, _ := http.NewRequest("GET", "/", nil)
for i := 0; i < b.N; i++ {
mux.ServeHTTP(nil, req)
}
})
})
// 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) {
req, _ := http.NewRequest("GET", "/users/all", nil)
for i := 0; i < b.N; i++ {
mux.ServeHTTP(nil, req)
}
})
})
// 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) {
req, _ := http.NewRequest("GET", "/users/123", nil)
for i := 0; i < b.N; i++ {
mux.ServeHTTP(nil, req)
}
})
})
// 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) {
req, _ := http.NewRequest("GET", "/nonexistent/path", nil)
for i := 0; i < b.N; i++ {
mux.ServeHTTP(nil, req)
}
})
})
}