Files
openserp/core/server_test.go

1324 lines
45 KiB
Go
Raw Normal View History

2023-06-23 04:08:00 +03:00
package core
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
2023-06-23 04:08:00 +03:00
"testing"
"time"
2023-06-28 22:20:19 +03:00
"golang.org/x/time/rate"
2023-06-23 04:08:00 +03:00
)
type engineMock struct {
name string
initialized bool
limiter *rate.Limiter
searchFn func(context.Context, Query) ([]SearchResult, error)
imageFn func(context.Context, Query) ([]SearchResult, error)
2023-06-23 04:08:00 +03:00
mu sync.Mutex
searchCalls int
imageCalls int
2023-06-23 04:08:00 +03:00
}
func (e *engineMock) Name() string { return e.name }
func (e *engineMock) IsInitialized() bool {
return e.initialized
2023-06-23 04:08:00 +03:00
}
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
2023-06-23 04:08:00 +03:00
}
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
2023-06-23 04:08:00 +03:00
}
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
2023-07-23 04:37:01 +03:00
}
2026-04-01 17:38:13 +03:00
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)
}
2023-06-28 22:20:19 +03:00
}
2023-06-23 04:08:00 +03:00
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}
2023-06-23 04:08:00 +03:00
opts := DefaultServerOptions()
opts.AllowEndpointFallback = false
opts.Resilience.Retry.MaxRetries = 0
srv := NewServerWithOptions("127.0.0.1", 7072, opts, primary, fallback)
2023-06-23 04:08:00 +03:00
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)
}
}
2023-06-23 04:08:00 +03:00
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)
}
}
2026-04-01 17:38:13 +03:00
func TestRequestProxyOverrideDirectBeatsGlobal(t *testing.T) {
var googleProxy string
engine := &engineMock{
name: "google",
initialized: true,
searchFn: func(_ context.Context, q Query) ([]SearchResult, error) {
2026-04-01 17:38:13 +03:00
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) {
2026-04-01 17:38:13 +03:00
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)
}
}
2026-04-01 17:38:13 +03:00
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) {
2026-04-01 17:38:13 +03:00
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)
2023-06-23 04:08:00 +03:00
}
}
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}
2023-06-23 04:08:00 +03:00
opts := DefaultServerOptions()
opts.CORS = CORSConfig{
AllowOrigins: "https://client.local",
AllowMethods: "GET,OPTIONS",
AllowHeaders: "Authorization",
MaxAge: 600,
}
2023-06-23 04:08:00 +03:00
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)
}
}