package routing import ( "path/filepath" "testing" ) // ============== Router Initialization Tests ============== func TestRouter_Initialization(t *testing.T) { router := NewRouter() if router.StaticDir() != "./static" { t.Errorf("Expected static dir ./static, got %s", router.StaticDir()) } if len(router.Routes()) != 0 { t.Error("Expected empty routes") } if len(router.ExactRoutes()) != 0 { t.Error("Expected empty exact routes") } if router.DefaultRoute() != nil { t.Error("Expected nil default route") } } func TestRouter_CustomStaticDir(t *testing.T) { router := NewRouter(WithStaticDir("/custom/path")) if router.StaticDir() != "/custom/path" { t.Errorf("Expected static dir /custom/path, got %s", router.StaticDir()) } } // ============== Route Adding Tests ============== func TestRouter_AddExactRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"return": "200 OK"} router.AddRoute("=/health", config) exactRoutes := router.ExactRoutes() if _, ok := exactRoutes["/health"]; !ok { t.Error("Expected /health in exact routes") } } func TestRouter_AddDefaultRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"spa_fallback": true, "root": "./static"} router.AddRoute("__default__", config) if router.DefaultRoute() == nil { t.Error("Expected default route to be set") } } func TestRouter_AddRegexRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"root": "./static"} router.AddRoute("~^/api/", config) if len(router.Routes()) != 1 { t.Errorf("Expected 1 regex route, got %d", len(router.Routes())) } } func TestRouter_AddCaseInsensitiveRegexRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"root": "./static", "cache_control": "public, max-age=3600"} router.AddRoute("~*\\.(css|js)$", config) if len(router.Routes()) != 1 { t.Errorf("Expected 1 regex route, got %d", len(router.Routes())) } if router.Routes()[0].CaseSensitive { t.Error("Expected case-insensitive route") } } func TestRouter_InvalidRegexPattern(t *testing.T) { router := NewRouter() config := map[string]interface{}{"root": "./static"} // Invalid regex - unmatched bracket router.AddRoute("~^/api/[invalid", config) // Should not add invalid pattern if len(router.Routes()) != 0 { t.Error("Should not add invalid regex pattern") } } // ============== Route Matching Tests ============== func TestRouter_MatchExactRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"return": "200 OK"} router.AddRoute("=/health", config) match := router.Match("/health") if match == nil { t.Fatal("Expected match for /health") } if match.Config["return"] != "200 OK" { t.Error("Expected return config") } if len(match.Params) != 0 { t.Error("Expected empty params for exact match") } } func TestRouter_MatchExactRouteNoMatch(t *testing.T) { router := NewRouter() config := map[string]interface{}{"return": "200 OK"} router.AddRoute("=/health", config) match := router.Match("/healthcheck") if match != nil { t.Error("Exact route should not match /healthcheck") } } func TestRouter_MatchRegexRoute(t *testing.T) { router := NewRouter() config := map[string]interface{}{"proxy_pass": "http://localhost:9001"} router.AddRoute("~^/api/v\\d+/", config) match := router.Match("/api/v1/users") if match == nil { t.Fatal("Expected match for /api/v1/users") } if match.Config["proxy_pass"] != "http://localhost:9001" { t.Error("Expected proxy_pass config") } } func TestRouter_MatchRegexRouteWithGroups(t *testing.T) { router := NewRouter() config := map[string]interface{}{"proxy_pass": "http://localhost:9001"} router.AddRoute("~^/api/v(?P\\d+)/", config) match := router.Match("/api/v2/data") if match == nil { t.Fatal("Expected match for /api/v2/data") } if match.Params["version"] != "2" { t.Errorf("Expected version=2, got %s", match.Params["version"]) } } func TestRouter_MatchCaseInsensitiveRegex(t *testing.T) { router := NewRouter() config := map[string]interface{}{"root": "./static", "cache_control": "public, max-age=3600"} router.AddRoute("~*\\.(CSS|JS)$", config) // Should match lowercase match1 := router.Match("/styles/main.css") if match1 == nil { t.Error("Should match lowercase .css") } // Should match uppercase match2 := router.Match("/scripts/app.JS") if match2 == nil { t.Error("Should match uppercase .JS") } } func TestRouter_MatchCaseSensitiveRegex(t *testing.T) { router := NewRouter() config := map[string]interface{}{"root": "./static"} router.AddRoute("~\\.(css)$", config) // Should match lowercase match1 := router.Match("/styles/main.css") if match1 == nil { t.Error("Should match lowercase .css") } // Should NOT match uppercase match2 := router.Match("/styles/main.CSS") if match2 != nil { t.Error("Should not match uppercase .CSS for case-sensitive regex") } } func TestRouter_MatchDefaultRoute(t *testing.T) { router := NewRouter() router.AddRoute("=/health", map[string]interface{}{"return": "200 OK"}) router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true}) match := router.Match("/unknown/path") if match == nil { t.Fatal("Expected default route match") } if match.Config["spa_fallback"] != true { t.Error("Expected spa_fallback config from default route") } } // ============== Priority Tests ============== func TestRouter_PriorityExactOverRegex(t *testing.T) { router := NewRouter() router.AddRoute("=/api/status", map[string]interface{}{"return": "200 Exact"}) router.AddRoute("~^/api/", map[string]interface{}{"proxy_pass": "http://localhost:9001"}) match := router.Match("/api/status") if match == nil { t.Fatal("Expected match") } if match.Config["return"] != "200 Exact" { t.Error("Exact match should have priority over regex") } } func TestRouter_PriorityRegexOverDefault(t *testing.T) { router := NewRouter() router.AddRoute("~^/api/", map[string]interface{}{"proxy_pass": "http://localhost:9001"}) router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true}) match := router.Match("/api/v1/users") if match == nil { t.Fatal("Expected match") } if match.Config["proxy_pass"] != "http://localhost:9001" { t.Error("Regex match should have priority over default") } } // ============== CreateRouterFromConfig Tests ============== func TestCreateRouterFromConfig(t *testing.T) { config := map[string]interface{}{ "regex_locations": map[string]interface{}{ "=/health": map[string]interface{}{ "return": "200 OK", "content_type": "text/plain", }, "~^/api/": map[string]interface{}{ "proxy_pass": "http://localhost:9001", }, "__default__": map[string]interface{}{ "spa_fallback": true, "root": "./static", }, }, } router := CreateRouterFromConfig(config) // Check exact route if _, ok := router.ExactRoutes()["/health"]; !ok { t.Error("Expected /health exact route") } // Check regex route if len(router.Routes()) != 1 { t.Errorf("Expected 1 regex route, got %d", len(router.Routes())) } // Check default route if router.DefaultRoute() == nil { t.Error("Expected default route") } } // ============== Static Dir Path Tests ============== func TestRouter_StaticDirPath(t *testing.T) { router := NewRouter(WithStaticDir("/var/www/html")) expected, _ := filepath.Abs("/var/www/html") actual, _ := filepath.Abs(router.StaticDir()) if actual != expected { t.Errorf("Expected static dir %s, got %s", expected, actual) } } // ============== Concurrent Access Tests ============== func TestRouter_ConcurrentAccess(t *testing.T) { router := NewRouter() // Add routes concurrently done := make(chan bool, 10) for i := 0; i < 10; i++ { go func(n int) { router.AddRoute("~^/api/v"+string(rune('0'+n))+"/", map[string]interface{}{ "proxy_pass": "http://localhost:900" + string(rune('0'+n)), }) done <- true }(i) } // Wait for all goroutines for i := 0; i < 10; i++ { <-done } // Match routes concurrently for i := 0; i < 10; i++ { go func(n int) { router.Match("/api/v" + string(rune('0'+n)) + "/users") done <- true }(i) } for i := 0; i < 10; i++ { <-done } } // ============== Benchmarks ============== func BenchmarkRouter_MatchExact(b *testing.B) { router := NewRouter() router.AddRoute("=/health", map[string]interface{}{"return": "200 OK"}) b.ResetTimer() for i := 0; i < b.N; i++ { router.Match("/health") } } func BenchmarkRouter_MatchRegex(b *testing.B) { router := NewRouter() router.AddRoute("~^/api/v(?P\\d+)/", map[string]interface{}{"proxy_pass": "http://localhost:9001"}) b.ResetTimer() for i := 0; i < b.N; i++ { router.Match("/api/v1/users/123") } } func BenchmarkRouter_MatchWithManyRoutes(b *testing.B) { router := NewRouter() // Add many routes for i := 0; i < 50; i++ { router.AddRoute("~^/api/v"+string(rune('0'+i%10))+"/service"+string(rune('0'+i/10))+"/", map[string]interface{}{"proxy_pass": "http://localhost:9001"}) } router.AddRoute("__default__", map[string]interface{}{"spa_fallback": true}) b.ResetTimer() for i := 0; i < b.N; i++ { router.Match("/api/v5/service3/users/123") } }