package integration import ( "encoding/json" "net/http" "testing" "github.com/konduktor/konduktor/internal/extension" ) // ============== Route Priority Tests ============== func TestRouting_ExactMatchPriority(t *testing.T) { // Exact match should have highest priority backend := StartBackend(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{ "path": r.URL.Path, "source": "default", }) }) defer backend.Close() logger := createTestLogger(t) routingExt, err := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ // Exact match - highest priority "=/api/status": map[string]interface{}{ "return": "200 exact-match", "content_type": "text/plain", }, // Regex that also matches /api/status "~^/api/.*": map[string]interface{}{ "proxy_pass": backend.URL(), }, "__default__": map[string]interface{}{ "proxy_pass": backend.URL(), }, }, }, logger) if err != nil { t.Fatalf("Failed to create routing extension: %v", err) } server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) // Test exact match route - should return static response resp, err := client.Get("/api/status", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() AssertStatus(t, resp, http.StatusOK) body := ReadBody(t, resp) if string(body) != "exact-match" { t.Errorf("Expected 'exact-match', got %q", string(body)) } // Regex route should be used for other /api/* paths resp2, err := client.Get("/api/other", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp2.Body.Close() AssertStatus(t, resp2, http.StatusOK) // Verify it went to backend if backend.RequestCount() != 1 { t.Errorf("Expected 1 backend request, got %d", backend.RequestCount()) } } // ============== Case Sensitivity Tests ============== func TestRouting_CaseSensitiveRegex(t *testing.T) { logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ // Case-sensitive regex (~) "~^/API/test$": map[string]interface{}{ "return": "200 case-sensitive", "content_type": "text/plain", }, "__default__": map[string]interface{}{ "return": "200 default", "content_type": "text/plain", }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) // Exact case match should work resp, err := client.Get("/API/test", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() body := ReadBody(t, resp) if string(body) != "case-sensitive" { t.Errorf("Expected 'case-sensitive' for /API/test, got %q", string(body)) } // Different case should NOT match resp2, err := client.Get("/api/test", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp2.Body.Close() body2 := ReadBody(t, resp2) if string(body2) != "default" { t.Errorf("Expected 'default' for /api/test (case mismatch), got %q", string(body2)) } } func TestRouting_CaseInsensitiveRegex(t *testing.T) { logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ // Case-insensitive regex (~*) "~*^/api/test$": map[string]interface{}{ "return": "200 case-insensitive", "content_type": "text/plain", }, "__default__": map[string]interface{}{ "return": "200 default", "content_type": "text/plain", }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) testCases := []struct { path string expected string }{ {"/api/test", "case-insensitive"}, {"/API/test", "case-insensitive"}, {"/Api/Test", "case-insensitive"}, {"/API/TEST", "case-insensitive"}, {"/api/other", "default"}, } for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { resp, err := client.Get(tc.path, nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() body := ReadBody(t, resp) if string(body) != tc.expected { t.Errorf("Expected %q for %s, got %q", tc.expected, tc.path, string(body)) } }) } } // ============== Default Route Tests ============== func TestRouting_DefaultRoute(t *testing.T) { backend := StartBackend(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{ "handler": "default", "path": r.URL.Path, }) }) defer backend.Close() logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "=/specific": map[string]interface{}{ "return": "200 specific", "content_type": "text/plain", }, "__default__": map[string]interface{}{ "proxy_pass": backend.URL(), }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) // Non-matching paths should go to default paths := []string{"/", "/random", "/path/to/resource", "/api/v1/users"} for _, path := range paths { t.Run(path, func(t *testing.T) { resp, err := client.Get(path, nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() AssertStatus(t, resp, http.StatusOK) var result map[string]string json.NewDecoder(resp.Body).Decode(&result) if result["handler"] != "default" { t.Errorf("Expected default handler, got %v", result["handler"]) } }) } } // ============== Return Directive Tests ============== func TestRouting_ReturnDirective(t *testing.T) { logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "=/health": map[string]interface{}{ "return": "200 OK", "content_type": "text/plain", }, "=/status": map[string]interface{}{ "return": "200 {\"status\": \"healthy\"}", "content_type": "application/json", }, "=/forbidden": map[string]interface{}{ "return": "404 Not Found", "content_type": "text/plain", }, "__default__": map[string]interface{}{ "return": "200 default", "content_type": "text/plain", }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) testCases := []struct { path string expectedStatus int expectedBody string contentType string }{ {"/health", 200, "OK", "text/plain"}, {"/status", 200, `{"status": "healthy"}`, "application/json"}, {"/forbidden", 404, "Not Found", "text/plain"}, } for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { resp, err := client.Get(tc.path, nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() AssertStatus(t, resp, tc.expectedStatus) AssertHeaderContains(t, resp, "Content-Type", tc.contentType) body := ReadBody(t, resp) if string(body) != tc.expectedBody { t.Errorf("Expected body %q, got %q", tc.expectedBody, string(body)) } }) } } // ============== Multiple Regex Routes Tests ============== func TestRouting_MultipleRegexRoutes(t *testing.T) { logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "~^/api/v1/.*": map[string]interface{}{ "return": "200 v1", "content_type": "text/plain", }, "~^/api/v2/.*": map[string]interface{}{ "return": "200 v2", "content_type": "text/plain", }, "~^/api/.*": map[string]interface{}{ "return": "200 api-generic", "content_type": "text/plain", }, "__default__": map[string]interface{}{ "return": "200 default", "content_type": "text/plain", }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) testCases := []struct { path string expected string }{ {"/api/v1/users", "v1"}, {"/api/v2/users", "v2"}, {"/api/v3/users", "api-generic"}, {"/other", "default"}, } for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { resp, err := client.Get(tc.path, nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() body := ReadBody(t, resp) if string(body) != tc.expected { t.Errorf("Expected %q for %s, got %q", tc.expected, tc.path, string(body)) } }) } } // ============== Regex with Named Groups ============== func TestRouting_RegexNamedGroups(t *testing.T) { backend := StartBackend(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{ "path": r.URL.Path, }) }) defer backend.Close() logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "~^/users/(?P\\d+)/posts/(?P\\d+)$": map[string]interface{}{ "proxy_pass": backend.URL() + "/api/v2/users/{userId}/posts/{postId}", }, "~^/items/(?P[a-z]+)/(?P\\d+)$": map[string]interface{}{ "proxy_pass": backend.URL() + "/catalog/{category}/item/{id}", }, "__default__": map[string]interface{}{ "proxy_pass": backend.URL(), }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) testCases := []struct { requestPath string expectedPath string }{ {"/users/123/posts/456", "/api/v2/users/123/posts/456"}, {"/items/electronics/789", "/catalog/electronics/item/789"}, } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { resp, err := client.Get(tc.requestPath, nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() AssertStatus(t, resp, http.StatusOK) lastReq := backend.LastRequest() if lastReq == nil { t.Fatal("No request received by backend") } if lastReq.Path != tc.expectedPath { t.Errorf("Expected backend path %s, got %s", tc.expectedPath, lastReq.Path) } }) } } // ============== No Matching Route Tests ============== func TestRouting_NoMatchingRoute(t *testing.T) { logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "=/specific": map[string]interface{}{ "return": "200 specific", "content_type": "text/plain", }, // No default route }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) // Request to non-matching path should return 404 resp, err := client.Get("/other", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() AssertStatus(t, resp, http.StatusNotFound) } // ============== Headers in Return Tests ============== func TestRouting_CustomHeaders(t *testing.T) { backend := StartBackend(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{ "x-custom-header": r.Header.Get("X-Custom-Header"), "x-api-version": r.Header.Get("X-API-Version"), }) }) defer backend.Close() logger := createTestLogger(t) routingExt, _ := extension.NewRoutingExtension(map[string]interface{}{ "regex_locations": map[string]interface{}{ "__default__": map[string]interface{}{ "proxy_pass": backend.URL(), "headers": []interface{}{ "X-Custom-Header: custom-value", "X-API-Version: v1", }, }, }, }, logger) server := StartTestServer(t, &ServerConfig{ Extensions: []extension.Extension{routingExt}, }) defer server.Close() client := NewHTTPClient(server.URL) resp, err := client.Get("/test", nil) if err != nil { t.Fatalf("Request failed: %v", err) } defer resp.Body.Close() var result map[string]string json.NewDecoder(resp.Body).Decode(&result) if result["x-custom-header"] != "custom-value" { t.Errorf("Expected X-Custom-Header=custom-value, got %v", result["x-custom-header"]) } if result["x-api-version"] != "v1" { t.Errorf("Expected X-API-Version=v1, got %v", result["x-api-version"]) } }