217 lines
8.2 KiB
Go
217 lines
8.2 KiB
Go
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")
|
|
}
|
|
}
|