package core import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "golang.org/x/time/rate" ) type engineMock struct { name string initialized bool limiter *rate.Limiter searchFn func(context.Context, Query) ([]SearchResult, error) imageFn func(context.Context, Query) ([]SearchResult, error) mu sync.Mutex searchCalls int imageCalls int } func (e *engineMock) Name() string { return e.name } func (e *engineMock) IsInitialized() bool { return e.initialized } func (e *engineMock) GetRateLimiter() *rate.Limiter { return e.limiter } func (e *engineMock) Search(ctx context.Context, q Query) ([]SearchResult, error) { e.mu.Lock() e.searchCalls++ e.mu.Unlock() if e.searchFn != nil { return e.searchFn(ctx, q) } return []SearchResult{{Rank: 1, URL: "https://example.com/" + e.name, Title: e.name}}, nil } func (e *engineMock) SearchImage(ctx context.Context, q Query) ([]SearchResult, error) { e.mu.Lock() e.imageCalls++ e.mu.Unlock() if e.imageFn != nil { return e.imageFn(ctx, q) } return []SearchResult{{Rank: 1, URL: "https://img.example.com/" + e.name, Title: e.name}}, nil } func request(t *testing.T, s *Server, path string) *http.Response { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) resp, err := s.app.Test(req, -1) if err != nil { t.Fatalf("request failed for %s: %v", path, err) } return resp } func requestWithHeader(t *testing.T, s *Server, path string, header string, value string) *http.Response { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) req.Header.Set(header, value) resp, err := s.app.Test(req, -1) if err != nil { t.Fatalf("request failed for %s: %v", path, err) } return resp } func TestOpenAPISpecEndpoint(t *testing.T) { engine := &engineMock{name: "google", initialized: true} srv := NewServerWithOptions("127.0.0.1", 7107, DefaultServerOptions(), engine) resp := request(t, srv, "/openapi.yaml") if resp.StatusCode != http.StatusOK { t.Fatalf("expected /openapi.yaml to return 200, got %d", resp.StatusCode) } if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "application/yaml") { t.Fatalf("expected YAML content-type, got %q", got) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read /openapi.yaml body: %v", err) } if !strings.Contains(string(body), "openapi: 3.0.3") { t.Fatalf("expected OpenAPI version marker in body") } } func TestDocsEndpointServesSwaggerUI(t *testing.T) { engine := &engineMock{name: "google", initialized: true} srv := NewServerWithOptions("127.0.0.1", 7108, DefaultServerOptions(), engine) resp := request(t, srv, "/docs") if resp.StatusCode != http.StatusOK { t.Fatalf("expected /docs to return 200, got %d", resp.StatusCode) } if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "text/html") { t.Fatalf("expected HTML content-type, got %q", got) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read /docs body: %v", err) } content := string(body) if !strings.Contains(content, "SwaggerUIBundle") { t.Fatalf("expected SwaggerUI bundle script on docs page") } if !strings.Contains(content, "/openapi.yaml") { t.Fatalf("expected docs page to reference /openapi.yaml") } } func TestInvalidQueryParametersReturnJSONError(t *testing.T) { engine := &engineMock{name: "google", initialized: true} srv := NewServerWithOptions("127.0.0.1", 7104, DefaultServerOptions(), engine) tests := []struct { name string path string message string }{ { name: "invalid limit", path: "/google/search?text=golang&limit=abc", message: "invalid syntax", }, { name: "negative start", path: "/google/search?text=golang&start=-1", message: "start must be >= 0", }, { name: "invalid filter flag", path: "/google/search?text=golang&filter=notabool", message: "invalid syntax", }, { name: "invalid answers flag on mega endpoint", path: "/mega/search?text=golang&answers=notabool", message: "invalid syntax", }, { name: "empty text query", path: "/google/search?text=", message: "Query cannot be empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp := request(t, srv, tt.path) if resp.StatusCode != http.StatusInternalServerError { t.Fatalf("expected 500 for invalid query params, got %d", resp.StatusCode) } var payload JSONErrorResponse if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode error response: %v", err) } if payload.Code != http.StatusInternalServerError { t.Fatalf("expected code=500, got %d", payload.Code) } if payload.Error != "server_error" { t.Fatalf("expected error=server_error, got %q", payload.Error) } if payload.Message == "" { t.Fatal("expected error message to be present") } if tt.message != "" && !strings.Contains(payload.Message, tt.message) { t.Fatalf("expected message to contain %q, got %q", tt.message, payload.Message) } }) } } func TestMegaEnginesEndpointResponseFormat(t *testing.T) { google := &engineMock{name: "google", initialized: true} yandex := &engineMock{name: "yandex", initialized: false} srv := NewServerWithOptions("127.0.0.1", 7105, DefaultServerOptions(), google, yandex) // Prime circuit breaker stats so circuit_state is populated for both engines. _ = request(t, srv, "/google/search?text=golang") _ = request(t, srv, "/yandex/search?text=golang") resp := request(t, srv, "/mega/engines") if resp.StatusCode != http.StatusOK { t.Fatalf("expected /mega/engines to return 200, got %d", resp.StatusCode) } var payload map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode /mega/engines response: %v", err) } total, ok := payload["total"].(float64) if !ok { t.Fatalf("expected numeric total field, got %T", payload["total"]) } if total != 2 { t.Fatalf("expected total=2, got %v", total) } engines, ok := payload["engines"].([]interface{}) if !ok { t.Fatalf("expected engines array, got %T", payload["engines"]) } if len(engines) != 2 { t.Fatalf("expected 2 engines in payload, got %d", len(engines)) } byName := map[string]map[string]interface{}{} for _, entry := range engines { engineData, ok := entry.(map[string]interface{}) if !ok { t.Fatalf("expected engine object, got %T", entry) } name, _ := engineData["name"].(string) if name == "" { t.Fatalf("expected non-empty engine name, got %#v", engineData["name"]) } if _, ok := engineData["initialized"].(bool); !ok { t.Fatalf("expected initialized bool for engine %s, got %T", name, engineData["initialized"]) } state, ok := engineData["circuit_state"].(string) if !ok || state == "" { t.Fatalf("expected non-empty circuit_state for engine %s, got %#v", name, engineData["circuit_state"]) } byName[name] = engineData } if _, ok := byName["google"]; !ok { t.Fatal("expected google engine in response") } if _, ok := byName["yandex"]; !ok { t.Fatal("expected yandex engine in response") } } func TestStatsEndpointStructure(t *testing.T) { engine := &engineMock{name: "google", initialized: true} srv := NewServerWithOptions("127.0.0.1", 7106, DefaultServerOptions(), engine) // Ensure circuit breaker stats are initialized. _ = request(t, srv, "/google/search?text=golang") resp := request(t, srv, "/stats") if resp.StatusCode != http.StatusOK { t.Fatalf("expected /stats to return 200, got %d", resp.StatusCode) } var payload map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode /stats response: %v", err) } cache, ok := payload["cache"].(map[string]interface{}) if !ok { t.Fatalf("expected cache object, got %T", payload["cache"]) } if _, ok := cache["status"].(bool); !ok { t.Fatalf("expected cache.status bool, got %T", cache["status"]) } if _, ok := cache["entries"].(float64); !ok { t.Fatalf("expected cache.entries number, got %T", cache["entries"]) } if _, ok := cache["hits"].(float64); !ok { t.Fatalf("expected cache.hits number, got %T", cache["hits"]) } if _, ok := cache["misses"].(float64); !ok { t.Fatalf("expected cache.misses number, got %T", cache["misses"]) } if _, ok := cache["bypasses"].(float64); !ok { t.Fatalf("expected cache.bypasses number, got %T", cache["bypasses"]) } proxy, ok := payload["proxy"].(map[string]interface{}) if !ok { t.Fatalf("expected proxy object, got %T", payload["proxy"]) } if _, ok := proxy["configured_count"].(float64); !ok { t.Fatalf("expected proxy.configured_count number, got %T", proxy["configured_count"]) } if _, ok := proxy["healthy_count"].(float64); !ok { t.Fatalf("expected proxy.healthy_count number, got %T", proxy["healthy_count"]) } if _, ok := proxy["unhealthy_count"].(float64); !ok { t.Fatalf("expected proxy.unhealthy_count number, got %T", proxy["unhealthy_count"]) } if _, ok := proxy["tags"].(map[string]interface{}); !ok { t.Fatalf("expected proxy.tags object, got %T", proxy["tags"]) } if _, ok := proxy["entries"].([]interface{}); !ok { t.Fatalf("expected proxy.entries array, got %T", proxy["entries"]) } engines, ok := proxy["engines"].(map[string]interface{}) if !ok { t.Fatalf("expected proxy.engines object, got %T", proxy["engines"]) } googleStats, ok := engines["google"].(map[string]interface{}) if !ok { t.Fatalf("expected proxy.engines.google object, got %T", engines["google"]) } if got := googleStats["selected_proxy"]; got != "direct" { t.Fatalf("expected proxy.engines.google.selected_proxy=direct, got %#v", got) } breakers, ok := payload["circuit_breakers"].([]interface{}) if !ok { t.Fatalf("expected circuit_breakers array, got %T", payload["circuit_breakers"]) } if len(breakers) == 0 { t.Fatal("expected at least one circuit breaker entry") } first, ok := breakers[0].(map[string]interface{}) if !ok { t.Fatalf("expected circuit breaker object, got %T", breakers[0]) } if _, ok := first["engine"].(string); !ok { t.Fatalf("expected circuit_breakers[0].engine string, got %T", first["engine"]) } if _, ok := first["state"].(string); !ok { t.Fatalf("expected circuit_breakers[0].state string, got %T", first["state"]) } if _, ok := first["failure_count"].(float64); !ok { t.Fatalf("expected circuit_breakers[0].failure_count number, got %T", first["failure_count"]) } } func TestHealthEndpointStatusSemantics(t *testing.T) { ready := &engineMock{name: "google", initialized: true, limiter: rate.NewLimiter(rate.Every(time.Second), 1)} notReady := &engineMock{name: "yandex", initialized: false, limiter: rate.NewLimiter(rate.Every(time.Second), 1)} srv := NewServerWithOptions("127.0.0.1", 7070, DefaultServerOptions(), ready, notReady) resp := request(t, srv, "/health") if resp.StatusCode != http.StatusOK { t.Fatalf("expected degraded health to return 200, got %d", resp.StatusCode) } var health HealthStatus if err := json.NewDecoder(resp.Body).Decode(&health); err != nil { t.Fatalf("decode health response: %v", err) } if health.Status != "degraded" { t.Fatalf("expected degraded status, got %s", health.Status) } down := &engineMock{name: "google", initialized: false, limiter: rate.NewLimiter(rate.Every(time.Second), 1)} srvUnhealthy := NewServerWithOptions("127.0.0.1", 7071, DefaultServerOptions(), down) resp = request(t, srvUnhealthy, "/health") if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected unhealthy health to return 503, got %d", resp.StatusCode) } } func TestDedicatedEndpointNoFallbackByDefault(t *testing.T) { primary := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return nil, errors.New("primary failed") }, } fallback := &engineMock{name: "yandex", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = false opts.Resilience.Retry.MaxRetries = 0 srv := NewServerWithOptions("127.0.0.1", 7072, opts, primary, fallback) resp := request(t, srv, "/google/search?text=golang") if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected 503 when primary fails and fallback disabled, got %d", resp.StatusCode) } if got := resp.Header.Get("X-Fallback-Engine"); got != "" { t.Fatalf("unexpected fallback header: %s", got) } if fallback.searchCalls != 0 { t.Fatalf("fallback engine should not be called, got %d calls", fallback.searchCalls) } } func TestDedicatedEndpointCachesSearchResults(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = false opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7073, opts, engine) first := request(t, srv, "/google/search?text=golang") if first.StatusCode != http.StatusOK { t.Fatalf("expected first request to succeed, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "MISS" { t.Fatalf("expected cache MISS on first request, got %q", got) } second := request(t, srv, "/google/search?text=golang") if second.StatusCode != http.StatusOK { t.Fatalf("expected second request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "HIT" { t.Fatalf("expected cache HIT on second request, got %q", got) } if engine.searchCalls != 1 { t.Fatalf("expected engine to be called once, got %d", engine.searchCalls) } } func TestDedicatedEndpointCachesImageResults(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = false opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7074, opts, engine) first := request(t, srv, "/google/image?text=golang") if first.StatusCode != http.StatusOK { t.Fatalf("expected first image request to succeed, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "MISS" { t.Fatalf("expected cache MISS on first image request, got %q", got) } second := request(t, srv, "/google/image?text=golang") if second.StatusCode != http.StatusOK { t.Fatalf("expected second image request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "HIT" { t.Fatalf("expected cache HIT on second image request, got %q", got) } if engine.imageCalls != 1 { t.Fatalf("expected image engine to be called once, got %d", engine.imageCalls) } } func TestDedicatedEndpointFallbackBypassesCache(t *testing.T) { primary := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return nil, errors.New("primary failed") }, } fallback := &engineMock{name: "yandex", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = true opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7078, opts, primary, fallback) first := request(t, srv, "/google/search?text=golang") if first.StatusCode != http.StatusOK { t.Fatalf("expected first fallback request to succeed, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "BYPASS" { t.Fatalf("expected bypass on fallback response, got %q", got) } if got := first.Header.Get("X-Fallback-Engine"); got != "yandex" { t.Fatalf("expected fallback engine header, got %q", got) } second := request(t, srv, "/google/search?text=golang") if second.StatusCode != http.StatusOK { t.Fatalf("expected repeated fallback request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "BYPASS" { t.Fatalf("expected repeated fallback request to bypass cache, got %q", got) } if got := second.Header.Get("X-Fallback-Engine"); got != "yandex" { t.Fatalf("expected fallback engine header on repeated request, got %q", got) } if primary.searchCalls != 2 || fallback.searchCalls != 2 { t.Fatalf("expected repeated live fallback calls, got primary=%d fallback=%d", primary.searchCalls, fallback.searchCalls) } } func TestDedicatedEndpointFallbackDoesNotBypassProxyPolicy(t *testing.T) { primary := &engineMock{name: "google", initialized: true} fallback := &engineMock{name: "yandex", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = true opts.Resilience.Retry.MaxRetries = 0 opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{}, }, EnginePolicies: map[string]string{"google": "missing"}, } srv := NewServerWithOptions("127.0.0.1", 7103, opts, primary, fallback) resp := request(t, srv, "/google/search?text=golang") if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected fail-closed 503 when primary proxy policy cannot be satisfied, got %d", resp.StatusCode) } if got := resp.Header.Get("X-Fallback-Engine"); got != "" { t.Fatalf("unexpected fallback header when proxy policy fails closed: %q", got) } if fallback.searchCalls != 0 { t.Fatalf("fallback engine should not be called when proxy policy fails closed, got %d calls", fallback.searchCalls) } } func TestMegaSearchCachesWholeQueryWithEngineOrderNormalization(t *testing.T) { google := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return []SearchResult{{Rank: 1, URL: "https://example.com/shared", Title: "shared"}}, nil }, } yandex := &engineMock{ name: "yandex", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return []SearchResult{{Rank: 1, URL: "https://example.com/shared", Title: "shared"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7083, opts, google, yandex) first := request(t, srv, "/mega/search?text=golang&engines=yandex,google") if first.StatusCode != http.StatusOK { t.Fatalf("expected first mega request to succeed, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "MISS" { t.Fatalf("expected cache MISS on first mega request, got %q", got) } second := request(t, srv, "/mega/search?text=golang&engines=google,yandex") if second.StatusCode != http.StatusOK { t.Fatalf("expected second mega request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "HIT" { t.Fatalf("expected cache HIT on reordered mega request, got %q", got) } if google.searchCalls != 1 || yandex.searchCalls != 1 { t.Fatalf("expected one live mega call per engine before cache hit, got google=%d yandex=%d", google.searchCalls, yandex.searchCalls) } } func TestMegaImageCachesWholeQueryWithEngineOrderNormalization(t *testing.T) { google := &engineMock{ name: "google", initialized: true, imageFn: func(_ context.Context, q Query) ([]SearchResult, error) { return []SearchResult{{Rank: 1, URL: "https://img.example.com/shared", Title: "shared"}}, nil }, } yandex := &engineMock{ name: "yandex", initialized: true, imageFn: func(_ context.Context, q Query) ([]SearchResult, error) { return []SearchResult{{Rank: 1, URL: "https://img.example.com/shared", Title: "shared"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7084, opts, google, yandex) first := request(t, srv, "/mega/image?text=golang+logo&engines=yandex,google") if first.StatusCode != http.StatusOK { t.Fatalf("expected first mega image request to succeed, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "MISS" { t.Fatalf("expected cache MISS on first mega image request, got %q", got) } second := request(t, srv, "/mega/image?text=golang+logo&engines=google,yandex") if second.StatusCode != http.StatusOK { t.Fatalf("expected second mega image request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "HIT" { t.Fatalf("expected cache HIT on reordered mega image request, got %q", got) } if google.imageCalls != 1 || yandex.imageCalls != 1 { t.Fatalf("expected one live mega image call per engine before cache hit, got google=%d yandex=%d", google.imageCalls, yandex.imageCalls) } } func TestMegaSearchDeduplicatesRepeatedEngineNames(t *testing.T) { google := &engineMock{name: "google", initialized: true} yandex := &engineMock{name: "yandex", initialized: true} opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 0 srv := NewServerWithOptions("127.0.0.1", 7085, opts, google, yandex) resp := request(t, srv, "/mega/search?text=golang&engines=google,google,yandex,google") if resp.StatusCode != http.StatusOK { t.Fatalf("expected mega request to succeed, got %d", resp.StatusCode) } if google.searchCalls != 1 || yandex.searchCalls != 1 { t.Fatalf("expected deduplicated engine execution, got google=%d yandex=%d", google.searchCalls, yandex.searchCalls) } } func TestMegaSearchCachesForHealthySubsetWhenOneCircuitIsOpen(t *testing.T) { google := &engineMock{name: "google", initialized: true} bing := &engineMock{ name: "bing", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return nil, errors.New("bing failed") }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 0 opts.Resilience.CircuitBreaker.FailureThreshold = 1 opts.Resilience.CircuitBreaker.RecoveryTimeout = 5 * time.Minute opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7086, opts, google, bing) first := request(t, srv, "/mega/search?text=golang&engines=google,bing") if first.StatusCode != http.StatusOK { t.Fatalf("expected first mega request to succeed with partial results, got %d", first.StatusCode) } if got := first.Header.Get("X-Cache"); got != "MISS" { t.Fatalf("expected first response to populate cache, got %q", got) } second := request(t, srv, "/mega/search?text=golang&engines=google,bing") if second.StatusCode != http.StatusOK { t.Fatalf("expected second mega request to succeed, got %d", second.StatusCode) } if got := second.Header.Get("X-Cache"); got != "HIT" { t.Fatalf("expected second response to hit subset cache, got %q", got) } if google.searchCalls != 1 || bing.searchCalls != 1 { t.Fatalf("expected subset cache hit to avoid new engine calls, got google=%d bing=%d", google.searchCalls, bing.searchCalls) } } func TestResilienceStatsContainsRetryInWhenCircuitOpen(t *testing.T) { primary := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return nil, errors.New("forced failure") }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 0 opts.Resilience.CircuitBreaker.FailureThreshold = 1 opts.Resilience.CircuitBreaker.RecoveryTimeout = 5 * time.Minute srv := NewServerWithOptions("127.0.0.1", 7075, opts, primary) _ = request(t, srv, "/google/search?text=golang") statsResp := request(t, srv, "/stats/cb") if statsResp.StatusCode != http.StatusOK { t.Fatalf("expected stats endpoint to return 200, got %d", statsResp.StatusCode) } var stats map[string]interface{} if err := json.NewDecoder(statsResp.Body).Decode(&stats); err != nil { t.Fatalf("decode stats response: %v", err) } breakers, ok := stats["circuit_breakers"].([]interface{}) if !ok || len(breakers) == 0 { t.Fatalf("expected at least one circuit breaker entry, got %#v", stats["circuit_breakers"]) } first := breakers[0].(map[string]interface{}) if state, _ := first["state"].(string); state != "open" { t.Fatalf("expected circuit to be open, got %q", state) } retryIn, ok := first["retry_in"].(float64) if !ok { t.Fatalf("expected retry_in number in JSON response, got %T", first["retry_in"]) } if retryIn <= 0 { t.Fatalf("expected retry_in to be present when circuit is open, got %v", first["retry_in"]) } } func TestStatsEndpointsContract(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() srv := NewServerWithOptions("127.0.0.1", 7090, opts, engine) for _, path := range []string{"/stats", "/stats/cache", "/stats/proxy", "/stats/cb"} { resp := request(t, srv, path) if resp.StatusCode != http.StatusOK { t.Fatalf("expected %s to return 200, got %d", path, resp.StatusCode) } } for _, oldPath := range []string{"/cache/stats", "/resilience/stats"} { resp := request(t, srv, oldPath) if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected %s to return 404, got %d", oldPath, resp.StatusCode) } } } func TestStatsProxyV2Payload(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{ {URL: "http://user:pass@proxy1:8080", Tags: []string{"default", "us"}}, {URL: "http://proxy2:8080", Tags: []string{"default"}}, }, Health: ProxiesHealthConfig{FailureThreshold: 1}, }, EnginePolicies: map[string]string{"google": "default"}, } srv := NewServerWithOptions("127.0.0.1", 7092, opts, engine) resp := request(t, srv, "/stats/proxy") if resp.StatusCode != http.StatusOK { t.Fatalf("expected /stats/proxy to return 200, got %d", resp.StatusCode) } var payload map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode /stats/proxy response: %v", err) } if got := payload["configured_count"].(float64); got != 2 { t.Fatalf("expected configured_count=2, got %v", got) } if got := payload["healthy_count"].(float64); got != 2 { t.Fatalf("expected healthy_count=2, got %v", got) } if got := payload["unhealthy_count"].(float64); got != 0 { t.Fatalf("expected unhealthy_count=0, got %v", got) } if _, exists := payload["defaults"]; exists { t.Fatalf("defaults must not be exposed in proxy stats payload") } entries := payload["entries"].([]interface{}) first := entries[0].(map[string]interface{}) if got := first["proxy"].(string); got == "http://user:pass@proxy1:8080" { t.Fatalf("expected masked proxy, got %q", got) } if _, exists := payload["runtime"]; exists { t.Fatalf("runtime must not be exposed in V2 stats payload") } if _, exists := payload["source"]; exists { t.Fatalf("source must not be exposed in V2 stats payload") } engines := payload["engines"].(map[string]interface{}) google := engines["google"].(map[string]interface{}) if _, exists := google["mode"]; exists { t.Fatalf("engine mode must not be exposed in proxy stats payload") } if got := google["tag"]; got != "default" { t.Fatalf("expected engine tag default, got %#v", got) } if got := google["selected_proxy"]; got != "pooled" { t.Fatalf("expected pooled engine proxy stats, got %#v", got) } } func TestResilientRawProxyPoolRotatesOnRetry(t *testing.T) { var attemptedProxies []string engine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { attemptedProxies = append(attemptedProxies, q.ProxyURL) if q.ProxyURL == "http://bad-proxy:8080" { return nil, errors.New("proxy failed") } return []SearchResult{{Rank: 1, URL: "https://example.com/success", Title: "ok"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 1 opts.Resilience.Retry.InitialBackoff = 0 opts.Resilience.Retry.MaxBackoff = 0 opts.Resilience.Retry.BackoffFactor = 1 opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{ {URL: "http://bad-proxy:8080", Tags: []string{"default"}}, {URL: "http://good-proxy:8080", Tags: []string{"default"}}, }, Health: ProxiesHealthConfig{FailureThreshold: 1}, }, EnginePolicies: map[string]string{"google": "default"}, } srv := NewServerWithOptions("127.0.0.1", 7091, opts, engine) resp := request(t, srv, "/google/search?text=golang") if resp.StatusCode != http.StatusOK { t.Fatalf("expected search to recover through rotated proxy, got %d", resp.StatusCode) } if len(attemptedProxies) != 2 { t.Fatalf("expected 2 attempts, got %d", len(attemptedProxies)) } if attemptedProxies[0] != "http://bad-proxy:8080" || attemptedProxies[1] != "http://good-proxy:8080" { t.Fatalf("unexpected proxy rotation order: %#v", attemptedProxies) } statsResp := request(t, srv, "/stats/proxy") var stats map[string]interface{} if err := json.NewDecoder(statsResp.Body).Decode(&stats); err != nil { t.Fatalf("decode stats response: %v", err) } if got := stats["healthy_count"].(float64); got != 1 { t.Fatalf("expected healthy_count=1, got %v", got) } if got := stats["unhealthy_count"].(float64); got != 1 { t.Fatalf("expected unhealthy_count=1, got %v", got) } } func TestProxyHeadersDirectAndTagPool(t *testing.T) { directEngine := &engineMock{name: "google", initialized: true} directSrv := NewServerWithOptions("127.0.0.1", 7093, DefaultServerOptions(), directEngine) directResp := request(t, directSrv, "/google/search?text=golang") if directResp.StatusCode != http.StatusOK { t.Fatalf("expected direct request to succeed, got %d", directResp.StatusCode) } if got := directResp.Header.Get("X-Proxy-Mode"); got != ProxyModeOff { t.Fatalf("expected X-Proxy-Mode=%s, got %q", ProxyModeOff, got) } if got := directResp.Header.Get("X-Proxy-Tag"); got != "" { t.Fatalf("expected empty X-Proxy-Tag in off mode, got %q", got) } if got := directResp.Header.Get("X-Proxy-Used"); got != "direct" { t.Fatalf("expected X-Proxy-Used=direct, got %q", got) } proxiedEngine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{ {URL: "http://proxy1:8080", Tags: []string{"default"}}, }, }, EnginePolicies: map[string]string{"google": "default"}, } proxiedSrv := NewServerWithOptions("127.0.0.1", 7094, opts, proxiedEngine) proxiedResp := request(t, proxiedSrv, "/google/search?text=golang") if proxiedResp.StatusCode != http.StatusOK { t.Fatalf("expected proxied request to succeed, got %d", proxiedResp.StatusCode) } if got := proxiedResp.Header.Get("X-Proxy-Mode"); got != ProxyModeTagPool { t.Fatalf("expected X-Proxy-Mode=%s, got %q", ProxyModeTagPool, got) } if got := proxiedResp.Header.Get("X-Proxy-Tag"); got != "default" { t.Fatalf("expected X-Proxy-Tag=default, got %q", got) } if got := proxiedResp.Header.Get("X-Proxy-Used"); got != "http://proxy1:8080" { t.Fatalf("expected masked selected proxy, got %q", got) } } func TestGlobalProxyForcesAllEnginesRaw(t *testing.T) { var googleProxy string var yandexProxy string googleEngine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { googleProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } yandexEngine := &engineMock{ name: "yandex", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { yandexProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/yandex", Title: "yandex"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Global: "http://global-proxy:8080", }, EnginePolicies: map[string]string{ "google": "us", }, } srv := NewServerWithOptions("127.0.0.1", 7097, opts, googleEngine, yandexEngine) if resp := request(t, srv, "/google/search?text=golang"); resp.StatusCode != http.StatusOK { t.Fatalf("expected google request to succeed, got %d", resp.StatusCode) } if resp := request(t, srv, "/yandex/search?text=golang"); resp.StatusCode != http.StatusOK { t.Fatalf("expected yandex request to succeed, got %d", resp.StatusCode) } if googleProxy != "http://global-proxy:8080" { t.Fatalf("expected google to use global proxy, got %q", googleProxy) } if yandexProxy != "http://global-proxy:8080" { t.Fatalf("expected yandex to use global proxy, got %q", yandexProxy) } } func TestRequestProxyOverrideDirectBeatsGlobal(t *testing.T) { var googleProxy string engine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { googleProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Global: "http://global-proxy:8080", }, } srv := NewServerWithOptions("127.0.0.1", 7099, opts, engine) resp := requestWithHeader(t, srv, "/google/search?text=golang", "X-Use-Proxy", "direct") if resp.StatusCode != http.StatusOK { t.Fatalf("expected direct override request to succeed, got %d", resp.StatusCode) } if googleProxy != "" { t.Fatalf("expected direct override to disable proxy, got %q", googleProxy) } if got := resp.Header.Get("X-Proxy-Mode"); got != ProxyModeOff { t.Fatalf("expected X-Proxy-Mode=%s, got %q", ProxyModeOff, got) } if got := resp.Header.Get("X-Proxy-Used"); got != "direct" { t.Fatalf("expected X-Proxy-Used=direct, got %q", got) } } func TestRequestProxyOverrideTagBeatsGlobal(t *testing.T) { var googleProxy string engine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { googleProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Global: "http://global-proxy:8080", Entries: []ProxyEntryConfig{ {URL: "http://proxy-us:8080", Tags: []string{"us"}}, }, }, } srv := NewServerWithOptions("127.0.0.1", 7100, opts, engine) resp := requestWithHeader(t, srv, "/google/search?text=golang", "X-Use-Proxy", "us") if resp.StatusCode != http.StatusOK { t.Fatalf("expected tagged override request to succeed, got %d", resp.StatusCode) } if googleProxy != "http://proxy-us:8080" { t.Fatalf("expected tagged override to use pool proxy, got %q", googleProxy) } if got := resp.Header.Get("X-Proxy-Tag"); got != "us" { t.Fatalf("expected X-Proxy-Tag=us, got %q", got) } if got := resp.Header.Get("X-Proxy-Used"); got != "http://proxy-us:8080" { t.Fatalf("expected X-Proxy-Used to reflect override proxy, got %q", got) } } func TestBrowserProxyPoolRotatesPerRequest(t *testing.T) { var attemptedProxies []string engine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { attemptedProxies = append(attemptedProxies, q.ProxyURL) return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeBrowser, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{ {URL: "http://proxy1:8080", Tags: []string{"default"}}, {URL: "http://proxy2:8080", Tags: []string{"default"}}, }, }, EnginePolicies: map[string]string{"google": "default"}, } srv := NewServerWithOptions("127.0.0.1", 7098, opts, engine) if resp := request(t, srv, "/google/search?text=golang"); resp.StatusCode != http.StatusOK { t.Fatalf("expected first browser-style request to succeed, got %d", resp.StatusCode) } if resp := request(t, srv, "/google/search?text=golang+2"); resp.StatusCode != http.StatusOK { t.Fatalf("expected second browser-style request to succeed, got %d", resp.StatusCode) } if len(attemptedProxies) != 2 { t.Fatalf("expected 2 browser-style proxy attempts, got %d", len(attemptedProxies)) } if attemptedProxies[0] != "http://proxy1:8080" || attemptedProxies[1] != "http://proxy2:8080" { t.Fatalf("expected browser proxy rotation order, got %#v", attemptedProxies) } } func TestRequestProxyOverrideMissingTagFailsClosed(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Global: "http://global-proxy:8080", }, } srv := NewServerWithOptions("127.0.0.1", 7101, opts, engine) resp := requestWithHeader(t, srv, "/google/search?text=golang", "X-Use-Proxy", "missing") if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected missing tag override to fail closed, got %d", resp.StatusCode) } if got := resp.Header.Get("X-Proxy-Mode"); got != ProxyModeTagPool { t.Fatalf("expected X-Proxy-Mode=%s on missing tag response, got %q", ProxyModeTagPool, got) } if got := resp.Header.Get("X-Proxy-Tag"); got != "missing" { t.Fatalf("expected X-Proxy-Tag=missing, got %q", got) } } func TestMegaProxyOverrideHeaderBeatsGlobal(t *testing.T) { var googleProxy string engine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { googleProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Global: "http://global-proxy:8080", Entries: []ProxyEntryConfig{ {URL: "http://proxy-us:8080", Tags: []string{"us"}}, }, }, } srv := NewServerWithOptions("127.0.0.1", 7102, opts, engine) resp := requestWithHeader(t, srv, "/mega/search?text=golang&engines=google", "X-Use-Proxy", "us") if resp.StatusCode != http.StatusOK { t.Fatalf("expected mega override request to succeed, got %d", resp.StatusCode) } if googleProxy != "http://proxy-us:8080" { t.Fatalf("expected mega override to use pool proxy, got %q", googleProxy) } if got := resp.Header.Get("X-Proxy-Tag"); got != "us" { t.Fatalf("expected mega X-Proxy-Tag=us, got %q", got) } } func TestProxyFailClosedWhenNoHealthyProxy(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{}, }, EnginePolicies: map[string]string{"google": "missing"}, } srv := NewServerWithOptions("127.0.0.1", 7095, opts, engine) resp := request(t, srv, "/google/search?text=golang") if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected fail-closed 503 when no healthy proxy exists, got %d", resp.StatusCode) } if got := resp.Header.Get("X-Proxy-Mode"); got != ProxyModeTagPool { t.Fatalf("expected X-Proxy-Mode=%s on fail-closed response, got %q", ProxyModeTagPool, got) } } func TestEngineOverrideProxyBehaviorRaw(t *testing.T) { var googleProxy string var yandexProxy string googleEngine := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { googleProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } yandexEngine := &engineMock{ name: "yandex", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { yandexProxy = q.ProxyURL return []SearchResult{{Rank: 1, URL: "https://example.com/yandex", Title: "yandex"}}, nil }, } opts := DefaultServerOptions() opts.Resilience.Proxy = ProxyConfig{ Runtime: ProxyRuntimeRaw, Proxies: ProxiesConfig{ Entries: []ProxyEntryConfig{ {URL: "http://proxy-us:8080", Tags: []string{"us"}}, }, }, EnginePolicies: map[string]string{"google": "us"}, } srv := NewServerWithOptions("127.0.0.1", 7096, opts, googleEngine, yandexEngine) if resp := request(t, srv, "/google/search?text=golang"); resp.StatusCode != http.StatusOK { t.Fatalf("expected google request to succeed, got %d", resp.StatusCode) } if resp := request(t, srv, "/yandex/search?text=golang"); resp.StatusCode != http.StatusOK { t.Fatalf("expected yandex request to succeed, got %d", resp.StatusCode) } if googleProxy == "" { t.Fatalf("expected proxied google request, got empty proxy") } if yandexProxy != "" { t.Fatalf("expected direct yandex request, got proxy %q", yandexProxy) } } func TestRetryAppliesRateLimiterOnEachAttempt(t *testing.T) { engine := &engineMock{ name: "google", initialized: true, limiter: rate.NewLimiter(rate.Every(120*time.Millisecond), 1), searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { return nil, errors.New("always fail") }, } opts := DefaultServerOptions() opts.Resilience.Retry.MaxRetries = 2 opts.Resilience.Retry.InitialBackoff = 0 opts.Resilience.Retry.MaxBackoff = 0 opts.Resilience.Retry.BackoffFactor = 1 srv := NewServerWithOptions("127.0.0.1", 7076, opts, engine) start := time.Now() resp := request(t, srv, "/google/search?text=golang") elapsed := time.Since(start) if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected failure response, got %d", resp.StatusCode) } if engine.searchCalls != 3 { t.Fatalf("expected 3 attempts (1 + 2 retries), got %d", engine.searchCalls) } if elapsed < 200*time.Millisecond { t.Fatalf("expected limiter to delay retries, elapsed only %s", elapsed) } } func TestCacheStatsDisabled(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.CacheTTL = 0 opts.CacheMaxSize = 0 srv := NewServerWithOptions("127.0.0.1", 7079, opts, engine) resp := request(t, srv, "/stats/cache") if resp.StatusCode != http.StatusOK { t.Fatalf("expected disabled cache stats to return 200, got %d", resp.StatusCode) } var payload map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode cache stats: %v", err) } if payload["status"] != false { t.Fatalf("expected disabled status, got %#v", payload) } } func TestCacheStatsReflectActivity(t *testing.T) { primary := &engineMock{ name: "google", initialized: true, searchFn: func(_ context.Context, q Query) ([]SearchResult, error) { if q.Text == "fallback" { return nil, errors.New("fallback path") } return []SearchResult{{Rank: 1, URL: "https://example.com/google", Title: "google"}}, nil }, } fallback := &engineMock{name: "yandex", initialized: true} opts := DefaultServerOptions() opts.AllowEndpointFallback = true opts.Resilience.Retry.MaxRetries = 0 opts.CacheTTL = time.Minute opts.CacheMaxSize = 10 srv := NewServerWithOptions("127.0.0.1", 7080, opts, primary, fallback) _ = request(t, srv, "/google/search?text=golang") _ = request(t, srv, "/google/search?text=golang") _ = request(t, srv, "/google/search?text=fallback") resp := request(t, srv, "/stats/cache") if resp.StatusCode != http.StatusOK { t.Fatalf("expected cache stats endpoint to return 200, got %d", resp.StatusCode) } var payload map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { t.Fatalf("decode cache stats response: %v", err) } if got := payload["status"].(bool); got != true { t.Fatalf("expected enabled status, got %v", got) } if got := payload["entries"].(float64); got != 1 { t.Fatalf("expected 1 cache entry, got %v", got) } if got := payload["hits"].(float64); got != 1 { t.Fatalf("expected 1 cache hit, got %v", got) } if got := payload["misses"].(float64); got != 2 { t.Fatalf("expected 2 cache misses, got %v", got) } if got := payload["bypasses"].(float64); got != 1 { t.Fatalf("expected 1 bypass, got %v", got) } } // Server-level CORS tests are intentionally smoke-level: // they verify middleware registration and option wiring, not header semantics. func TestServerOptions_WiresCustomCORSConfig(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.CORS = CORSConfig{ AllowOrigins: "https://client.local", AllowMethods: "GET,OPTIONS", AllowHeaders: "Authorization", MaxAge: 600, } srv := NewServerWithOptions("127.0.0.1", 7081, opts, engine) resp := request(t, srv, "/health") if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://client.local" { t.Fatalf("unexpected allow-origin: %q", got) } } func TestServerOptions_DisableCORSMiddleware(t *testing.T) { engine := &engineMock{name: "google", initialized: true} opts := DefaultServerOptions() opts.EnableCORS = false srv := NewServerWithOptions("127.0.0.1", 7082, opts, engine) resp := request(t, srv, "/health") if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" { t.Fatalf("expected CORS headers to be absent when disabled, got allow-origin=%q", got) } }