Files
homelab-frontend/internal/resilience/retry_test.go
T

159 lines
3.7 KiB
Go
Raw Normal View History

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")
}
}