veza/veza-backend-api/internal/recovery/retry_test.go

234 lines
5.5 KiB
Go

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