package shadow import ( "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "aigateway.local/core/internal/gateway" "aigateway.local/core/internal/platform/config" ) func TestContractSignatureComparesShapeNotGeneratedValues(t *testing.T) { left := contractSignature(200, []byte(`{"id":"one","choices":[{"message":{"content":"answer one"}}]}`)) right := contractSignature(200, []byte(`{"id":"two","choices":[{"message":{"content":"answer two"}}]}`)) if left != right { t.Fatalf("expected equal shapes: %s != %s", left, right) } if left == contractSignature(500, []byte(`{"choices":[]}`)) { t.Fatal("status and schema mismatch must differ") } } func TestMiddlewareUsesDedicatedCredentialAndDoesNotDelayPrimary(t *testing.T) { delivered := make(chan string, 1) legacy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) delivered <- r.Header.Get("Authorization") + ":" + string(raw) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"legacy","choices":[{"message":{"content":"different"}}]}`)) })) defer legacy.Close() middleware := New(config.Shadow{BaseURL: legacy.URL, APIKey: "shadow-secret", SampleRate: 1, Timeout: time.Second, MaxBodyBytes: 1 << 20, MaxConcurrent: 2}, slog.Default()) primary := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"go","choices":[{"message":{"content":"primary"}}]}`)) }) request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"test","stream":false}`)) request.Header.Set("Authorization", "Bearer production-secret") request = request.WithContext(gateway.WithRequestID(request.Context(), "shadow-test")) response := httptest.NewRecorder() middleware.Wrap(primary).ServeHTTP(response, request) if response.Code != 200 { t.Fatalf("primary status %d", response.Code) } select { case value := <-delivered: if !strings.HasPrefix(value, "Bearer shadow-secret:") { t.Fatalf("shadow credential not replaced: %s", value) } case <-time.After(2 * time.Second): t.Fatal("shadow request was not delivered") } deadline := time.Now().Add(time.Second) for middleware.metrics.matched.Load()+middleware.metrics.mismatched.Load() == 0 && time.Now().Before(deadline) { time.Sleep(time.Millisecond) } if middleware.metrics.matched.Load() != 1 { t.Fatalf("expected shape match, metrics=%s", middleware.Prometheus()) } } func TestOversizedShadowBodyIsPreservedForPrimary(t *testing.T) { middleware := New(config.Shadow{BaseURL: "http://127.0.0.1:1", APIKey: "shadow", SampleRate: 1, Timeout: time.Second, MaxBodyBytes: 16, MaxConcurrent: 1}, slog.Default()) payload := `{"stream":false,"prompt":"this body is larger than the shadow limit"}` primary := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) if string(raw) != payload { t.Fatalf("primary body changed: %q", raw) } w.WriteHeader(http.StatusNoContent) }) request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(payload)) request = request.WithContext(gateway.WithRequestID(request.Context(), "large-shadow")) response := httptest.NewRecorder() middleware.Wrap(primary).ServeHTTP(response, request) if response.Code != http.StatusNoContent { t.Fatalf("status %d", response.Code) } }