package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" "github.com/prometheus/client_golang/prometheus/testutil" ) // TestRetryValidation429WithRetryAfter tests that 429 with Retry-After header // honors the delay before retrying, then returns success to the client. func TestRetryValidation429WithRetryAfter(t *testing.T) { maxRetries := 2 var upstreamCallCount atomic.Int32 // Create upstream that returns 429 twice, then success upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) if count <= 2 { w.Header().Set("Retry-After", "1") // 1 second wait w.WriteHeader(http.StatusTooManyRequests) return } // Success after retries w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "id": "msg_test123", "type": "message", "role": "assistant", "content": []map[string]string{{"type": "text", "text": "Success"}}, }) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) // Create test request req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Record start time to verify retry-after delay start := time.Now() // Call the proxy handler handler.ServeHTTP(w, req) duration := time.Since(start) // Verify response - read from recorder body, not response body body := w.Body.String() if !json.Valid([]byte(body)) { t.Errorf("Response body is not valid JSON: %s", body) } if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } // Verify upstream call count (1 initial + 2 retries = 3 total) expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } // Verify total duration is at least 2 seconds (Retry-After: 1 twice) if duration < 2*time.Second { t.Logf("WARNING: Duration %v is less than expected 2s", duration) } t.Logf("PASS: %d upstream calls in %v", actualCalls, duration) } // TestRetryValidation429NoHeader tests that 429 without Retry-After header // uses exponential backoff and eventually surfaces the 429. func TestRetryValidation429NoHeader(t *testing.T) { maxRetries := 1 // Small for fast test var upstreamCallCount atomic.Int32 // Create upstream that always returns 429 without Retry-After upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.WriteHeader(http.StatusTooManyRequests) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Record start time start := time.Now() handler.ServeHTTP(w, req) duration := time.Since(start) // Verify 429 is returned to client if w.Code != http.StatusTooManyRequests { t.Errorf("Expected status 429, got %d", w.Code) } // Verify upstream call count (1 initial + 1 retry = 2 total with maxRetries=1) expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } // Verify exponential backoff: first retry waits 1s if duration < 1*time.Second { t.Logf("WARNING: Duration %v is less than expected 1s", duration) } t.Logf("PASS: %d upstream calls in %v", actualCalls, duration) } // TestRetryValidation422NoRetry tests that 422 responses are NOT retried. func TestRetryValidation422NoRetry(t *testing.T) { maxRetries := 5 // High to ensure no retry happens var upstreamCallCount atomic.Int32 // Create upstream that returns 422 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) t.Logf("Upstream call #%d", count) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(map[string]string{ "error": "unprocessable entity", "type": "invalid_request_error", }) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 422 is returned to client if w.Code != http.StatusUnprocessableEntity { t.Errorf("Expected status 422, got %d", w.Code) } body := w.Body.String() t.Logf("Response body: %s", body) // Verify exactly 1 upstream call (no retry) actualCalls := upstreamCallCount.Load() if actualCalls != 1 { t.Errorf("Expected 1 upstream call (no retry), got %d", actualCalls) } t.Logf("PASS: 422 not retried, %d upstream call", actualCalls) } // TestRetryValidationEmptyJSONBodyRetry tests empty JSON body on 200 triggers retry // and eventually returns 502 after MAX_RETRIES. func TestRetryValidationEmptyJSONBodyRetry(t *testing.T) { maxRetries := 1 // Small for fast test var upstreamCallCount atomic.Int32 // Create upstream that returns 200 with empty body upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) w.WriteHeader(http.StatusOK) // Empty body - no data written t.Logf("Upstream call #%d - returning empty body", count) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 502 is returned to client (empty body after retries) if w.Code != http.StatusBadGateway { t.Errorf("Expected status 502, got %d", w.Code) } // Verify upstream call count (1 initial + 1 retry = 2 total with maxRetries=1) expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: empty body triggered retry, %d upstream calls, returned 502", actualCalls) } // TestRetryValidationInvalidJSONBodyRetry tests invalid JSON body on 200 triggers retry // and eventually returns 502 after MAX_RETRIES. func TestRetryValidationInvalidJSONBodyRetry(t *testing.T) { maxRetries := 1 var upstreamCallCount atomic.Int32 // Create upstream that returns 200 with invalid JSON upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) w.WriteHeader(http.StatusOK) w.Write([]byte("{invalid json")) t.Logf("Upstream call #%d - returning invalid JSON", count) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 502 is returned to client if w.Code != http.StatusBadGateway { t.Errorf("Expected status 502, got %d", w.Code) } // Verify upstream call count expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: invalid JSON triggered retry, %d upstream calls, returned 502", actualCalls) } // TestRetryValidationEmptyStreamingRetry tests empty streaming response // triggers retry and eventually returns 502 after MAX_RETRIES. func TestRetryValidationEmptyStreamingRetry(t *testing.T) { maxRetries := 1 var upstreamCallCount atomic.Int32 // Create upstream that returns 200 with empty streaming response upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) // No data written - empty streaming response t.Logf("Upstream call #%d - returning empty streaming", count) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 502 is returned to client if w.Code != http.StatusBadGateway { t.Errorf("Expected status 502, got %d", w.Code) } // Verify upstream call count expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: empty streaming triggered retry, %d upstream calls, returned 502", actualCalls) } // TestRetryValidation500NoRetry tests that 500 responses are NOT retried. func TestRetryValidation500NoRetry(t *testing.T) { maxRetries := 5 var upstreamCallCount atomic.Int32 // Create upstream that returns 500 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.WriteHeader(http.StatusInternalServerError) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 500 is returned to client if w.Code != http.StatusInternalServerError { t.Errorf("Expected status 500, got %d", w.Code) } // Verify exactly 1 upstream call (no retry) actualCalls := upstreamCallCount.Load() if actualCalls != 1 { t.Errorf("Expected 1 upstream call (no retry), got %d", actualCalls) } t.Logf("PASS: 500 not retried, %d upstream call", actualCalls) } // TestRetryValidation404NoRetry tests that 404 responses are NOT retried. func TestRetryValidation404NoRetry(t *testing.T) { maxRetries := 5 var upstreamCallCount atomic.Int32 // Create upstream that returns 404 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.WriteHeader(http.StatusNotFound) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify 404 is returned to client if w.Code != http.StatusNotFound { t.Errorf("Expected status 404, got %d", w.Code) } // Verify exactly 1 upstream call (no retry) actualCalls := upstreamCallCount.Load() if actualCalls != 1 { t.Errorf("Expected 1 upstream call (no retry), got %d", actualCalls) } t.Logf("PASS: 404 not retried, %d upstream call", actualCalls) } // TestRetryValidationSuccessNoRetry tests successful request passes through correctly. func TestRetryValidationSuccessNoRetry(t *testing.T) { maxRetries := 3 var upstreamCallCount atomic.Int32 // Create upstream that returns success immediately upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "id": "msg_test123", "type": "message", "role": "assistant", "content": []map[string]string{{"type": "text", "text": "Hello!"}}, "usage": map[string]int{"input_tokens": 10, "output_tokens": 5}, }) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify success is returned to client if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } body := w.Body.String() if !json.Valid([]byte(body)) { t.Error("Response body is not valid JSON") } // Verify exactly 1 upstream call (success, no retry) actualCalls := upstreamCallCount.Load() if actualCalls != 1 { t.Errorf("Expected 1 upstream call (success, no retry), got %d", actualCalls) } t.Logf("PASS: success returned, %d upstream call", actualCalls) } // TestRetryValidationMetrics429 tests that 429 retries increment metrics correctly. func TestRetryValidationMetrics429(t *testing.T) { maxRetries := 2 var upstreamCallCount atomic.Int32 // Reset metrics before test retryAttempts.Reset() upstreamErrors.Reset() // Create upstream that returns 429 twice, then success upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) if count <= 2 { w.Header().Set("Retry-After", "1") w.WriteHeader(http.StatusTooManyRequests) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{"id": "msg_123"}) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify retry attempts metric - should have 2 retries with reason="429" retryMetric := retryAttempts.WithLabelValues("429", "test") retryCount := testutil.ToFloat64(retryMetric) if retryCount != float64(2) { t.Errorf("Expected 2 retry attempts, got %f", retryCount) } // Verify total upstream calls actualCalls := upstreamCallCount.Load() expectedCalls := int32(3) // 1 initial + 2 retries if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: 429 metrics verified, %d upstream calls, %f retries", actualCalls, retryCount) } // TestRetryValidationMetrics422 tests that 422 increments error metric but NOT retry metric. func TestRetryValidationMetrics422(t *testing.T) { maxRetries := 5 var upstreamCallCount atomic.Int32 // Reset metrics before test retryAttempts.Reset() upstreamErrors.Reset() upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(map[string]string{"error": "unprocessable"}) })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify upstream errors metric - should have 1 error with error_type="422" errorMetric := upstreamErrors.WithLabelValues("422", "test") errorCount := testutil.ToFloat64(errorMetric) if errorCount != 1 { t.Errorf("Expected 1 upstream error (422), got %f", errorCount) } // Verify NO retry attempts for 422 retryMetric := retryAttempts.WithLabelValues("422", "test") retryCount := testutil.ToFloat64(retryMetric) if retryCount != 0 { t.Errorf("Expected 0 retry attempts for 422 (no retry), got %f", retryCount) } // Verify exactly 1 upstream call actualCalls := upstreamCallCount.Load() if actualCalls != 1 { t.Errorf("Expected 1 upstream call (no retry), got %d", actualCalls) } t.Logf("PASS: 422 error recorded, no retry attempts, 1 upstream call") } // TestRetryValidationMetricsEmptyBody tests that empty body retries increment metrics correctly. func TestRetryValidationMetricsEmptyBody(t *testing.T) { maxRetries := 1 var upstreamCallCount atomic.Int32 // Reset metrics before test retryAttempts.Reset() upstreamErrors.Reset() upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.WriteHeader(http.StatusOK) // Empty body })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createNonStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify upstream errors metric - should have 2 errors with error_type="truncated_response" // (one for each failed attempt before the last one) errorMetric := upstreamErrors.WithLabelValues("truncated_response", "test") errorCount := testutil.ToFloat64(errorMetric) if errorCount != 2 { t.Errorf("Expected 2 upstream errors (truncated_response), got %f", errorCount) } // Verify retry attempts metric - should have 1 retry with reason="truncated_response" retryMetric := retryAttempts.WithLabelValues("truncated_response", "test") retryCount := testutil.ToFloat64(retryMetric) if retryCount != 1 { t.Errorf("Expected 1 retry attempt (truncated_response), got %f", retryCount) } // Verify upstream call count (1 initial + 1 retry = 2 total) expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: truncated_response errors=%f, retries=%f, upstream calls=%d", errorCount, retryCount, actualCalls) } // TestRetryValidationMetricsEmptyStreaming tests that empty streaming retries increment metrics correctly. func TestRetryValidationMetricsEmptyStreaming(t *testing.T) { maxRetries := 1 var upstreamCallCount atomic.Int32 // Reset metrics before test retryAttempts.Reset() upstreamErrors.Reset() upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamCallCount.Add(1) w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) // Empty streaming response })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, maxRetries) req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(createStreamingRequestBody())) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) // Verify upstream errors metric - should have 2 errors with error_type="empty_streaming" errorMetric := upstreamErrors.WithLabelValues("empty_streaming", "test") errorCount := testutil.ToFloat64(errorMetric) if errorCount != 2 { t.Errorf("Expected 2 upstream errors (empty_streaming), got %f", errorCount) } // Verify retry attempts metric - should have 1 retry with reason="empty_streaming" retryMetric := retryAttempts.WithLabelValues("empty_streaming", "test") retryCount := testutil.ToFloat64(retryMetric) if retryCount != 1 { t.Errorf("Expected 1 retry attempt (empty_streaming), got %f", retryCount) } // Verify upstream call count expectedCalls := int32(maxRetries + 1) actualCalls := upstreamCallCount.Load() if actualCalls != expectedCalls { t.Errorf("Expected %d upstream calls, got %d", expectedCalls, actualCalls) } t.Logf("PASS: empty_streaming errors=%f, retries=%f, upstream calls=%d", errorCount, retryCount, actualCalls) } // TestRetryValidationErrorClassificationTable tests all rows from the plan.md error classification table. func TestRetryValidationErrorClassificationTable(t *testing.T) { testCases := []struct { name string maxRetries int scenario string expectedCalls int32 // Expected upstream call count expectedStatusCode int shouldRetry bool description string }{ { name: "429_with_retry_after", maxRetries: 2, scenario: "429-retry-after-success", expectedCalls: 3, // 1 initial + 2 retries expectedStatusCode: 200, shouldRetry: true, description: "429 with Retry-After should honor delay and retry", }, { name: "429_no_header", maxRetries: 1, scenario: "429-always", expectedCalls: 2, // 1 initial + 1 retry expectedStatusCode: 429, shouldRetry: true, description: "429 without header should use exponential backoff", }, { name: "422_no_retry", maxRetries: 5, scenario: "422-always", expectedCalls: 1, // No retry expectedStatusCode: 422, shouldRetry: false, description: "422 should not be retried", }, { name: "empty_json_body", maxRetries: 1, scenario: "200-empty", expectedCalls: 2, // 1 initial + 1 retry expectedStatusCode: 502, shouldRetry: true, description: "Empty JSON body should retry, return 502 after max retries", }, { name: "invalid_json_body", maxRetries: 1, scenario: "200-invalid-json", expectedCalls: 2, // 1 initial + 1 retry expectedStatusCode: 502, shouldRetry: true, description: "Invalid JSON body should retry, return 502 after max retries", }, { name: "empty_streaming", maxRetries: 1, scenario: "200-empty-streaming", expectedCalls: 2, // 1 initial + 1 retry expectedStatusCode: 502, shouldRetry: true, description: "Empty streaming should retry, return 502 after max retries", }, { name: "500_no_retry", maxRetries: 5, scenario: "500-always", expectedCalls: 1, // No retry expectedStatusCode: 500, shouldRetry: false, description: "500 should not be retried", }, { name: "404_no_retry", maxRetries: 5, scenario: "404-always", expectedCalls: 1, // No retry expectedStatusCode: 404, shouldRetry: false, description: "404 should not be retried", }, { name: "success", maxRetries: 3, scenario: "200-success", expectedCalls: 1, // Success, no retry expectedStatusCode: 200, shouldRetry: false, description: "Success should not trigger retry", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Log(tc.description) var upstreamCallCount atomic.Int32 // Create upstream based on scenario upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := upstreamCallCount.Add(1) switch tc.scenario { case "429-retry-after-success": if count <= 2 { w.Header().Set("Retry-After", "1") w.WriteHeader(http.StatusTooManyRequests) } else { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{"id": "msg_123"}) } case "429-always": w.WriteHeader(http.StatusTooManyRequests) case "422-always": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(map[string]string{"error": "unprocessable"}) case "200-empty": w.WriteHeader(http.StatusOK) // Empty body case "200-invalid-json": w.WriteHeader(http.StatusOK) w.Write([]byte("{invalid json")) case "200-empty-streaming": w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) // Empty streaming case "500-always": w.WriteHeader(http.StatusInternalServerError) case "404-always": w.WriteHeader(http.StatusNotFound) case "200-success": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{"id": "msg_123"}) } })) defer upstream.Close() handler := createTestProxyHandler(t, upstream.URL, tc.maxRetries) // Use streaming request body for streaming scenario var reqBody string if tc.scenario == "200-empty-streaming" { reqBody = createStreamingRequestBody() } else { reqBody = createNonStreamingRequestBody() } req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != tc.expectedStatusCode { t.Errorf("Expected status %d, got %d", tc.expectedStatusCode, w.Code) } actualCalls := upstreamCallCount.Load() if actualCalls != tc.expectedCalls { t.Errorf("Expected %d upstream calls, got %d", tc.expectedCalls, actualCalls) } t.Logf("PASS: %d upstream calls, status %d", actualCalls, w.Code) }) } } // TestRetryValidationExponentialBackoffDuration tests that exponential backoff delays // are calculated correctly: 1s, 2s, 4s, 8s, etc. func TestRetryValidationExponentialBackoffDuration(t *testing.T) { testCases := []struct { attempt int expectedDuration time.Duration }{ {1, 1 * time.Second}, {2, 2 * time.Second}, {3, 4 * time.Second}, {4, 8 * time.Second}, {5, 16 * time.Second}, } for _, tc := range testCases { t.Run(fmt.Sprintf("attempt_%d", tc.attempt), func(t *testing.T) { // Calculate backoff: 1 << (attempt - 1) seconds backoff := time.Duration(1< expectedCalls*2 { t.Logf("WARNING: Upstream call count outside expected range: got %d, expected ~%d", totalCalls, expectedCalls) } t.Logf("PASS: %d concurrent requests, %d total upstream calls", numConcurrent, totalCalls) }