package services import ( "context" "net/http" "net/http/httptest" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) // TestStreamService_StartProcessing_RetryOnFailure vérifie que le retry fonctionne quand le serveur échoue temporairement // MOD-P1-RES-002: Test pour vérifier le mécanisme de retry func TestStreamService_StartProcessing_RetryOnFailure(t *testing.T) { // Mock server qui échoue 2 fois puis réussit attemptCount := int32(0) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempt := atomic.AddInt32(&attemptCount, 1) if attempt <= 2 { // Échouer les 2 premières tentatives w.WriteHeader(http.StatusInternalServerError) } else { // Réussir à la 3ème tentative w.WriteHeader(http.StatusOK) } })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) trackID := uuid.New() err := service.StartProcessing(context.Background(), trackID, "/path/to/file") // Vérifier que la requête a finalement réussi assert.NoError(t, err, "Request should succeed after retries") assert.Equal(t, int32(3), attemptCount, "Should have made 3 attempts (2 failures + 1 success)") } // TestStreamService_StartProcessing_MaxRetriesExceeded vérifie que le service échoue après max retries // MOD-P1-RES-002: Test pour vérifier que le retry s'arrête après maxRetries func TestStreamService_StartProcessing_MaxRetriesExceeded(t *testing.T) { // Mock server qui échoue toujours attemptCount := int32(0) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&attemptCount, 1) w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) trackID := uuid.New() err := service.StartProcessing(context.Background(), trackID, "/path/to/file") // Vérifier que la requête échoue après max retries assert.Error(t, err, "Request should fail after max retries") assert.Contains(t, err.Error(), "after 3 attempts", "Error should mention max retries") assert.Equal(t, int32(3), attemptCount, "Should have made exactly 3 attempts") } // TestStreamService_StartProcessing_RetryOnNetworkError vérifie que le retry fonctionne pour les erreurs réseau // MOD-P1-RES-002: Test pour vérifier le retry sur erreurs réseau (client.Do error) func TestStreamService_StartProcessing_RetryOnNetworkError(t *testing.T) { // Mock server qui ferme la connexion immédiatement (simule erreur réseau) attemptCount := int32(0) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempt := atomic.AddInt32(&attemptCount, 1) if attempt <= 2 { // Fermer la connexion immédiatement (simule erreur réseau) hj, ok := w.(http.Hijacker) if ok { conn, _, _ := hj.Hijack() conn.Close() } return } // Réussir à la 3ème tentative w.WriteHeader(http.StatusOK) })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) trackID := uuid.New() err := service.StartProcessing(context.Background(), trackID, "/path/to/file") // Vérifier que la requête a finalement réussi après retries assert.NoError(t, err, "Request should succeed after retries on network error") assert.Equal(t, int32(3), attemptCount, "Should have made 3 attempts") } // TestStreamService_StartProcessing_ContextTimeout vérifie que le contexte annule correctement // MOD-P1-RES-002: Test pour vérifier le respect du contexte timeout func TestStreamService_StartProcessing_ContextTimeout(t *testing.T) { // Mock server qui prend trop de temps (plus que le timeout du contexte) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Attendre plus longtemps que le timeout du contexte time.Sleep(2 * time.Second) w.WriteHeader(http.StatusOK) })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) // Créer un contexte avec un timeout très court (500ms) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() trackID := uuid.New() err := service.StartProcessing(ctx, trackID, "/path/to/file") // Vérifier que la requête échoue à cause du timeout du contexte assert.Error(t, err, "Request should fail due to context timeout") assert.Contains(t, err.Error(), "context", "Error should mention context") } // TestStreamService_StartProcessing_ContextCancelledDuringBackoff vérifie que le contexte annule pendant le backoff // MOD-P1-RES-002: Test pour vérifier que le contexte est respecté pendant le backoff func TestStreamService_StartProcessing_ContextCancelledDuringBackoff(t *testing.T) { // Mock server qui échoue toujours attemptCount := int32(0) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&attemptCount, 1) w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) // Créer un contexte qui sera annulé après le premier échec ctx, cancel := context.WithCancel(context.Background()) // Annuler le contexte après un court délai (pendant le backoff) go func() { time.Sleep(100 * time.Millisecond) cancel() }() trackID := uuid.New() err := service.StartProcessing(ctx, trackID, "/path/to/file") // Vérifier que la requête échoue à cause de l'annulation du contexte assert.Error(t, err, "Request should fail due to context cancellation") assert.Contains(t, err.Error(), "context cancelled", "Error should mention context cancellation") // Le nombre de tentatives devrait être 1 (première tentative échoue, puis contexte annulé pendant backoff) assert.Equal(t, int32(1), attemptCount, "Should have made 1 attempt before context cancellation") } // TestStreamService_StartProcessing_BackoffTiming vérifie que le backoff exponentiel fonctionne // MOD-P1-RES-002: Test pour vérifier le timing du backoff exponentiel func TestStreamService_StartProcessing_BackoffTiming(t *testing.T) { // Mock server qui échoue 2 fois puis réussit attemptCount := int32(0) attemptTimes := make([]time.Time, 0, 3) var mu sync.Mutex server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() attemptTimes = append(attemptTimes, time.Now()) mu.Unlock() attempt := atomic.AddInt32(&attemptCount, 1) if attempt <= 2 { w.WriteHeader(http.StatusInternalServerError) } else { w.WriteHeader(http.StatusOK) } })) defer server.Close() logger := zap.NewNop() service := NewStreamService(server.URL, logger) trackID := uuid.New() startTime := time.Now() err := service.StartProcessing(context.Background(), trackID, "/path/to/file") duration := time.Since(startTime) // Vérifier que la requête a réussi assert.NoError(t, err, "Request should succeed after retries") // Vérifier que le timing correspond au backoff exponentiel // Tentative 1: immédiate // Tentative 2: après 1s (backoff initial) // Tentative 3: après 2s (backoff * 2) // Total attendu: ~3s minimum (1s + 2s) assert.GreaterOrEqual(t, duration, 3*time.Second, "Total duration should be at least 3s (1s + 2s backoff)") assert.Less(t, duration, 5*time.Second, "Total duration should be less than 5s (with some margin)") // Vérifier que les tentatives sont espacées correctement mu.Lock() defer mu.Unlock() require.Len(t, attemptTimes, 3, "Should have made 3 attempts") if len(attemptTimes) >= 2 { // Intervalle entre tentative 1 et 2 devrait être ~1s interval1 := attemptTimes[1].Sub(attemptTimes[0]) assert.GreaterOrEqual(t, interval1, 900*time.Millisecond, "Backoff between attempt 1 and 2 should be ~1s") assert.Less(t, interval1, 2*time.Second, "Backoff between attempt 1 and 2 should be ~1s") } if len(attemptTimes) >= 3 { // Intervalle entre tentative 2 et 3 devrait être ~2s interval2 := attemptTimes[2].Sub(attemptTimes[1]) assert.GreaterOrEqual(t, interval2, 1800*time.Millisecond, "Backoff between attempt 2 and 3 should be ~2s") assert.Less(t, interval2, 3*time.Second, "Backoff between attempt 2 and 3 should be ~2s") } }