- Created DataExportService for comprehensive user data export (GDPR compliance) - Exports all user data: profile, settings, tracks, playlists, comments, likes, analytics, federated identities, roles - Added ExportUserData method to retrieve all user data from database - Added ExportUserDataAsJSON method to export as downloadable JSON file - Added endpoint GET /api/v1/users/me/export that returns JSON file download - Comprehensive unit tests for export service - Proper error handling and logging Phase: PHASE-6 Priority: P2 Progress: 118/267 (44.19%)
226 lines
4.7 KiB
Go
226 lines
4.7 KiB
Go
package recovery
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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: 10,
|
|
InitialDelay: 50 * time.Millisecond,
|
|
}
|
|
|
|
// Annuler le contexte dans une goroutine après un court délai
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
err := Retry(ctx, func() error {
|
|
attempts++
|
|
return errors.New("temporary error")
|
|
}, config)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "context cancelled")
|
|
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)
|
|
}
|
|
|