- docs: SCOPE_CONTROL, CONTRIBUTING, README, .github templates - frontend: DeveloperDashboardView, Player components, MSW handlers, auth, reactQuerySync - backend: playback_analytics, playlist_service, testutils, integration README Excluded (artifacts): .auth, playwright-report, test-results, storybook_audit_detailed.json
232 lines
4.9 KiB
Go
232 lines
4.9 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) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
attempts := 0
|
|
|
|
config := &RetryConfig{
|
|
MaxAttempts: 5,
|
|
InitialDelay: 200 * time.Millisecond,
|
|
RetryableFunc: func(err error) bool {
|
|
return true
|
|
},
|
|
OnRetry: func(attempt int, err error) {
|
|
// Annuler le contexte dès le premier retry pour garantir l'annulation pendant le délai
|
|
if attempt == 1 {
|
|
cancel()
|
|
}
|
|
},
|
|
}
|
|
|
|
err := Retry(ctx, func() error {
|
|
attempts++
|
|
return errors.New("temporary error")
|
|
}, config)
|
|
|
|
assert.Error(t, err)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|