package resilience import ( "context" "io" "net/http" "strings" "testing" "time" ) func TestRetryOnSuccess(t *testing.T) { cfg := &RetryConfig{MaxAttempts: 3} attempts := 0 resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) { attempts++ return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("ok")), }, nil }) if err != nil { t.Fatalf("unexpected error: %v", err) } if attempts != 1 { t.Errorf("expected 1 attempt on success, got %d", attempts) } if resp.StatusCode != 200 { t.Errorf("expected status 200, got %d", resp.StatusCode) } resp.Body.Close() } func TestRetryOn5xx(t *testing.T) { cfg := &RetryConfig{ MaxAttempts: 3, InitialBackoff: 10 * time.Millisecond, MaxBackoff: 50 * time.Millisecond, BackoffMultiplier: 2.0, } attempts := 0 resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) { attempts++ if attempt < 2 { // First two attempts return 503 return &http.Response{ StatusCode: 503, Body: io.NopCloser(strings.NewReader("unavailable")), }, nil } // Third attempt succeeds return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("ok")), }, nil }) if err != nil { t.Fatalf("unexpected error: %v", err) } if attempts != 3 { t.Errorf("expected 3 attempts (2 retries), got %d", attempts) } if resp.StatusCode != 200 { t.Errorf("expected status 200, got %d", resp.StatusCode) } resp.Body.Close() } func TestRetryExhaustion(t *testing.T) { cfg := &RetryConfig{ MaxAttempts: 2, InitialBackoff: 10 * time.Millisecond, MaxBackoff: 50 * time.Millisecond, } attempts := 0 resp, err := DoRetry(context.Background(), cfg, func(ctx context.Context, attempt int) (*http.Response, error) { attempts++ // Always return 503 return &http.Response{ StatusCode: 503, Body: io.NopCloser(strings.NewReader("unavailable")), }, nil }) if err != nil { t.Fatalf("unexpected error: %v", err) } if attempts != 2 { t.Errorf("expected 2 attempts (max), got %d", attempts) } if resp.StatusCode != 503 { t.Errorf("expected status 503, got %d", resp.StatusCode) } resp.Body.Close() } func TestRetryWithContext(t *testing.T) { cfg := &RetryConfig{MaxAttempts: 10} attempts := 0 ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Cancel after a short delay go func() { time.Sleep(50 * time.Millisecond) cancel() }() resp, err := DoRetry(ctx, cfg, func(ctx context.Context, attempt int) (*http.Response, error) { attempts++ time.Sleep(30 * time.Millisecond) return &http.Response{ StatusCode: 503, Body: io.NopCloser(strings.NewReader("unavailable")), }, nil }) if err != context.Canceled { t.Errorf("expected context.Canceled error, got: %v", err) } if resp != nil { resp.Body.Close() } // Should have fewer than all attempts due to cancellation if attempts >= 10 { t.Errorf("expected fewer than 10 attempts due to cancellation, got %d", attempts) } } func TestRetryPolicyShouldRetry(t *testing.T) { policy := &RetryPolicy{Retryable: true} resp503 := &http.Response{StatusCode: 503} if !policy.ShouldRetry(resp503) { t.Errorf("expected to retry on 503") } resp200 := &http.Response{StatusCode: 200} if policy.ShouldRetry(resp200) { t.Errorf("expected not to retry on 200") } resp404 := &http.Response{StatusCode: 404} if policy.ShouldRetry(resp404) { t.Errorf("expected not to retry on 404") } policyNoRetry := &RetryPolicy{Retryable: false} if policyNoRetry.ShouldRetry(resp503) { t.Errorf("expected not to retry when retryable=false") } }