- Conflit SQLx résolu (alignement sur version 0.7) - build.rs configurés pour protoc dans chat/stream servers - API Prometheus migrée vers HistogramOpts - Traits Display/Debug corrigés (String au lieu de &dyn Display) - API TOTP corrigée (totp-rs 5.4 avec Secret::Encoded) - Layers tracing-subscriber corrigés (types conditionnels) - VezaError/VezaResult exportés dans lib.rs - TransactionProvider simplifié (retour void au lieu de Box<dyn>) - VezaConfig contraint Serialize pour to_json() Files: veza-common/Cargo.toml, veza-common/src/*.rs, veza-chat-server/Cargo.toml, veza-chat-server/build.rs, veza-stream-server/Cargo.toml, veza-stream-server/build.rs, VEZA_ROADMAP.json Hours: 8 estimated, 3 actual
181 lines
4.2 KiB
Go
181 lines
4.2 KiB
Go
package recovery
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestRetryRecoveryStrategy(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger, _ := zap.NewDevelopment()
|
|
attempts := 0
|
|
|
|
fn := func() error {
|
|
attempts++
|
|
if attempts < 2 {
|
|
return errors.New("temporary error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
config := &RetryConfig{
|
|
MaxAttempts: 3,
|
|
InitialDelay: 10 * time.Millisecond,
|
|
}
|
|
|
|
strategy := NewRetryRecoveryStrategy(fn, config, logger)
|
|
|
|
assert.True(t, strategy.CanRecover(errors.New("timeout")))
|
|
assert.False(t, strategy.CanRecover(nil))
|
|
|
|
err := strategy.Recover(ctx, errors.New("temporary error"))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, attempts)
|
|
}
|
|
|
|
func TestFallbackRecoveryStrategy(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger, _ := zap.NewDevelopment()
|
|
fallbackCalled := false
|
|
|
|
fallbackFn := func() error {
|
|
fallbackCalled = true
|
|
return nil
|
|
}
|
|
|
|
strategy := NewFallbackRecoveryStrategy(fallbackFn, logger)
|
|
|
|
assert.True(t, strategy.CanRecover(errors.New("any error")))
|
|
|
|
err := strategy.Recover(ctx, errors.New("original error"))
|
|
assert.NoError(t, err)
|
|
assert.True(t, fallbackCalled)
|
|
}
|
|
|
|
func TestCircuitBreakerRecoveryStrategy(t *testing.T) {
|
|
// Utiliser un contexte avec timeout court pour éviter d'attendre 30s
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
logger, _ := zap.NewDevelopment()
|
|
circuitOpen := true
|
|
|
|
checkFn := func() bool {
|
|
return circuitOpen
|
|
}
|
|
|
|
strategy := NewCircuitBreakerRecoveryStrategy(checkFn, logger)
|
|
|
|
assert.True(t, strategy.CanRecover(errors.New("circuit breaker error")))
|
|
|
|
// Simuler la fermeture du circuit breaker après un court délai
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
circuitOpen = false
|
|
}()
|
|
|
|
err := strategy.Recover(ctx, errors.New("circuit breaker open"))
|
|
// Le timeout du contexte devrait se produire avant le timeout de la stratégie
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "timeout")
|
|
}
|
|
|
|
func TestCompositeRecoveryStrategy(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger, _ := zap.NewDevelopment()
|
|
attempts := 0
|
|
|
|
retryFn := func() error {
|
|
attempts++
|
|
if attempts < 2 {
|
|
return errors.New("temporary error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
config := &RetryConfig{
|
|
MaxAttempts: 3,
|
|
InitialDelay: 10 * time.Millisecond,
|
|
RetryableFunc: func(err error) bool {
|
|
return true // Toujours retryable pour ce test
|
|
},
|
|
}
|
|
|
|
retryStrategy := NewRetryRecoveryStrategy(retryFn, config, logger)
|
|
composite := NewCompositeRecoveryStrategy([]ErrorRecoveryStrategy{retryStrategy}, logger)
|
|
|
|
// Utiliser une erreur qui est détectée comme retryable par IsRetryableError
|
|
testErr := errors.New("timeout error")
|
|
assert.True(t, composite.CanRecover(testErr))
|
|
|
|
err := composite.Recover(ctx, testErr)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, attempts)
|
|
}
|
|
|
|
func TestRecoverWithStrategies(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger, _ := zap.NewDevelopment()
|
|
fallbackCalled := false
|
|
|
|
retryFn := func() error {
|
|
return errors.New("retry failed")
|
|
}
|
|
|
|
retryStrategy := NewRetryRecoveryStrategy(retryFn, nil, logger)
|
|
fallbackStrategy := NewFallbackRecoveryStrategy(func() error {
|
|
fallbackCalled = true
|
|
return nil
|
|
}, logger)
|
|
|
|
strategies := []ErrorRecoveryStrategy{retryStrategy, fallbackStrategy}
|
|
|
|
err := RecoverWithStrategies(ctx, errors.New("original error"), strategies, logger)
|
|
assert.NoError(t, err)
|
|
assert.True(t, fallbackCalled)
|
|
}
|
|
|
|
func TestIsTemporaryError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
expected bool
|
|
}{
|
|
{"timeout", errors.New("timeout"), true},
|
|
{"connection refused", errors.New("connection refused"), true},
|
|
{"permanent", errors.New("invalid input"), false},
|
|
{"nil", nil, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := IsTemporaryError(tt.err)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsPermanentError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
expected bool
|
|
}{
|
|
{"timeout", errors.New("timeout"), false},
|
|
{"permanent", errors.New("invalid input"), true},
|
|
{"nil", nil, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := IsPermanentError(tt.err)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|