konduktor/go/tests/integration/routing_test.go
Илья Глазунов bd0b381195 feat(caching): Implement cache hit detection and response header management
- Added functionality to mark responses as cache hits to prevent incorrect X-Cache headers.
- Introduced setCacheHitFlag function to traverse response writer wrappers and set cache hit flag.
- Updated cachingResponseWriter to manage cache hit state and adjust X-Cache header accordingly.
- Enhanced ProcessRequest and ProcessResponse methods to utilize new caching logic.

feat(extension): Introduce ResponseWriterWrapper and ResponseFinalizer interfaces

- Added ResponseWriterWrapper interface for extensions to wrap response writers.
- Introduced ResponseFinalizer interface for finalizing responses after processing.

refactor(manager): Improve response writer wrapping and finalization

- Updated Manager.Handler to wrap response writers through all enabled extensions.
- Implemented finalization of response writers after processing requests.

test(caching): Add comprehensive integration tests for caching behavior

- Created caching_test.go with tests for cache hit/miss, TTL expiration, pattern-based caching, and more.
- Ensured that caching logic works correctly for various scenarios including query strings and error responses.

test(routing): Add integration tests for routing behavior

- Created routing_test.go with tests for route priority, case sensitivity, default routes, and return directives.
- Verified that routing behaves as expected with multiple regex routes and named groups.
2025-12-12 01:03:32 +03:00

495 lines
13 KiB
Go

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<userId>\\d+)/posts/(?P<postId>\\d+)$": map[string]interface{}{
"proxy_pass": backend.URL() + "/api/v2/users/{userId}/posts/{postId}",
},
"~^/items/(?P<category>[a-z]+)/(?P<id>\\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"])
}
}