forked from aegis/pyserveX
- 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.
495 lines
13 KiB
Go
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"])
|
|
}
|
|
}
|