package recovery import ( "context" "errors" "strings" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap" ) func TestRetry_Success(t *testing.T) { ctx := context.Background() attempts := 0 err := Retry(ctx, func() error { attempts++ return nil }, nil) assert.NoError(t, err) assert.Equal(t, 1, attempts) } func TestRetry_SuccessAfterRetries(t *testing.T) { ctx := context.Background() attempts := 0 config := &RetryConfig{ MaxAttempts: 3, InitialDelay: 10 * time.Millisecond, MaxDelay: 100 * time.Millisecond, Multiplier: 2.0, } err := Retry(ctx, func() error { attempts++ if attempts < 3 { return errors.New("temporary error") } return nil }, config) assert.NoError(t, err) assert.Equal(t, 3, attempts) } func TestRetry_MaxAttemptsReached(t *testing.T) { ctx := context.Background() attempts := 0 config := &RetryConfig{ MaxAttempts: 3, InitialDelay: 10 * time.Millisecond, MaxDelay: 100 * time.Millisecond, } err := Retry(ctx, func() error { attempts++ return errors.New("persistent error") }, config) assert.Error(t, err) assert.Contains(t, err.Error(), "max attempts") assert.Equal(t, 3, attempts) } func TestRetry_ContextCancellation(t *testing.T) { // Utiliser un contexte avec timeout pour garantir l'annulation pendant le délai d'attente // Le timeout doit être suffisant pour que fn() soit appelé au moins une fois, // mais pas trop long pour que le contexte soit annulé pendant le délai d'attente ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) defer cancel() attempts := 0 config := &RetryConfig{ MaxAttempts: 5, // Réduire le nombre de tentatives pour garantir que le contexte soit annulé à temps InitialDelay: 200 * time.Millisecond, // Délai plus long que le timeout du contexte pour garantir l'annulation RetryableFunc: func(err error) bool { return true // Toujours retryable pour ce test }, } err := Retry(ctx, func() error { attempts++ // Ajouter un petit délai pour ralentir le test et garantir que le contexte soit annulé pendant l'attente time.Sleep(5 * time.Millisecond) return errors.New("temporary error") }, config) assert.Error(t, err) // L'erreur peut être "context cancelled" ou "context cancelled during retry" assert.True(t, strings.Contains(err.Error(), "context cancelled") || strings.Contains(err.Error(), "context cancelled during retry"), "Error should contain 'context cancelled': %s", err.Error()) assert.Greater(t, attempts, 0) // Devrait avoir fait au moins un appel } func TestRetry_NonRetryableError(t *testing.T) { ctx := context.Background() attempts := 0 permanentErr := errors.New("permanent error") config := &RetryConfig{ MaxAttempts: 3, RetryableFunc: func(err error) bool { return err != permanentErr }, } err := Retry(ctx, func() error { attempts++ return permanentErr }, config) assert.Error(t, err) assert.Equal(t, 1, attempts) // Ne devrait retryer qu'une fois } func TestRetryWithResult_Success(t *testing.T) { ctx := context.Background() attempts := 0 result, err := RetryWithResult(ctx, func() (int, error) { attempts++ return 42, nil }, nil) assert.NoError(t, err) assert.Equal(t, 42, result) assert.Equal(t, 1, attempts) } func TestRetryWithResult_Retry(t *testing.T) { ctx := context.Background() attempts := 0 config := &RetryConfig{ MaxAttempts: 3, InitialDelay: 10 * time.Millisecond, } result, err := RetryWithResult(ctx, func() (int, error) { attempts++ if attempts < 2 { return 0, errors.New("temporary error") } return 100, nil }, config) assert.NoError(t, err) assert.Equal(t, 100, result) assert.Equal(t, 2, attempts) } func TestRetryWithLogger(t *testing.T) { ctx := context.Background() logger, _ := zap.NewDevelopment() attempts := 0 config := &RetryConfig{ MaxAttempts: 3, InitialDelay: 10 * time.Millisecond, } err := RetryWithLogger(ctx, logger, func() error { attempts++ if attempts < 2 { return errors.New("temporary error") } return nil }, config) assert.NoError(t, err) assert.Equal(t, 2, attempts) } func TestIsRetryableError(t *testing.T) { tests := []struct { name string err error expected bool }{ {"timeout", errors.New("timeout"), true}, {"connection refused", errors.New("connection refused"), true}, {"server error", errors.New("server error 500"), true}, {"permanent", errors.New("invalid input"), false}, {"nil", nil, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsRetryableError(tt.err) assert.Equal(t, tt.expected, result) }) } } func TestCalculateDelay(t *testing.T) { config := &RetryConfig{ InitialDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, Multiplier: 2.0, Jitter: false, } // Test exponential backoff delay1 := calculateDelay(1, config) assert.Equal(t, 100*time.Millisecond, delay1) delay2 := calculateDelay(2, config) assert.Equal(t, 200*time.Millisecond, delay2) delay3 := calculateDelay(3, config) assert.Equal(t, 400*time.Millisecond, delay3) // Test max delay delay10 := calculateDelay(10, config) assert.LessOrEqual(t, delay10, config.MaxDelay) } func TestDefaultRetryConfig(t *testing.T) { config := DefaultRetryConfig() assert.NotNil(t, config) assert.Equal(t, 3, config.MaxAttempts) assert.Equal(t, 100*time.Millisecond, config.InitialDelay) assert.Equal(t, 5*time.Second, config.MaxDelay) assert.Equal(t, 2.0, config.Multiplier) assert.True(t, config.Jitter) }