veza/veza-backend-api/internal/services/circuit_breaker_integration_test.go
2025-12-16 11:23:49 -05:00

146 lines
5.5 KiB
Go

package services
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/sony/gobreaker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
// TestCircuitBreakerIntegration_5xxSimulation simule un scénario réel où un service externe
// retourne des erreurs 5xx, déclenchant l'ouverture du circuit breaker
// MOD-P2-007: Test d'intégration pour valider le déclenchement avec erreurs 5xx
func TestCircuitBreakerIntegration_5xxSimulation(t *testing.T) {
logger := zaptest.NewLogger(t)
// Mock server qui retourne 500 pour les 5 premières requêtes, puis 200
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
if requestCount <= 5 {
// Retourner 500 pour les 5 premières requêtes
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
} else {
// Retourner 200 après
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
}))
defer server.Close()
// Créer un circuit breaker avec seuil bas pour tester rapidement
client := &http.Client{Timeout: 5 * time.Second}
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "integration-test",
MaxRequests: 3,
Interval: 1 * time.Second,
Timeout: 1 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5 // S'ouvre après 5 échecs
},
})
cbClient := &CircuitBreakerHTTPClient{
client: client,
circuitBreaker: cb,
logger: logger,
}
// Phase 1: Faire 5 requêtes qui échouent (500)
t.Log("Phase 1: Simuler 5 erreurs 5xx")
for i := 0; i < 5; i++ {
req, err := http.NewRequest("GET", server.URL, nil)
require.NoError(t, err)
resp, err := cbClient.Do(req)
assert.Error(t, err, fmt.Sprintf("Request %d should fail", i+1))
assert.Contains(t, err.Error(), "server error: 500")
if resp != nil {
resp.Body.Close()
}
}
// Vérifier que le circuit breaker est maintenant ouvert
time.Sleep(100 * time.Millisecond)
state := cbClient.circuitBreaker.State()
assert.Equal(t, gobreaker.StateOpen, state, "Circuit breaker should be open after 5 failures")
t.Logf("Circuit breaker state: %v (expected: Open)", state)
// Phase 2: Tenter une requête - devrait être rejetée immédiatement
t.Log("Phase 2: Vérifier que les requêtes sont rejetées quand circuit ouvert")
req, err := http.NewRequest("GET", server.URL, nil)
require.NoError(t, err)
resp, err := cbClient.Do(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "circuit breaker is open")
assert.Nil(t, resp, "Response should be nil when circuit is open")
t.Log("Request correctly rejected when circuit is open")
// Phase 3: Attendre le timeout pour passer en half-open
t.Log("Phase 3: Attendre timeout pour passer en half-open")
time.Sleep(1100 * time.Millisecond) // Attendre un peu plus que le timeout (1s)
state = cbClient.circuitBreaker.State()
assert.True(t, state == gobreaker.StateHalfOpen || state == gobreaker.StateOpen,
fmt.Sprintf("Expected HalfOpen or Open after timeout, got %v", state))
t.Logf("Circuit breaker state after timeout: %v", state)
// Phase 4: Si half-open, une requête réussie devrait permettre au circuit de se fermer
// Note: gobreaker peut nécessiter plusieurs succès consécutifs pour fermer complètement
if state == gobreaker.StateHalfOpen {
t.Log("Phase 4: Tester half-open avec requête réussie")
req, err = http.NewRequest("GET", server.URL, nil)
require.NoError(t, err)
// Le serveur retourne maintenant 200 (requestCount > 5)
resp, err = cbClient.Do(req)
require.NoError(t, err, "Request should succeed when server returns 200")
assert.NotNil(t, resp)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
// Vérifier que le circuit est en half-open ou fermé après succès
// (gobreaker peut nécessiter plusieurs succès pour fermer complètement)
time.Sleep(100 * time.Millisecond)
finalState := cbClient.circuitBreaker.State()
assert.True(t, finalState == gobreaker.StateHalfOpen || finalState == gobreaker.StateClosed,
fmt.Sprintf("Circuit should be half-open or closed after successful request, got %v", finalState))
t.Logf("Circuit breaker state after success: %v (half-open or closed is acceptable)", finalState)
}
}
// TestCircuitBreakerIntegration_MetricsValidation valide que les métriques sont mises à jour
// MOD-P2-007: Test pour vérifier que les métriques Prometheus sont correctement enregistrées
func TestCircuitBreakerIntegration_MetricsValidation(t *testing.T) {
logger := zaptest.NewLogger(t)
// Mock server qui retourne 500
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
cbClient := NewCircuitBreakerHTTPClient(client, "metrics-test", logger)
// Faire quelques requêtes qui échouent
for i := 0; i < 3; i++ {
req, _ := http.NewRequest("GET", server.URL, nil)
cbClient.Do(req)
}
// Vérifier que les métriques ont été mises à jour
// (On ne peut pas lire directement les métriques Prometheus, mais on vérifie qu'il n'y a pas d'erreur)
counts := cbClient.circuitBreaker.Counts()
assert.Greater(t, counts.TotalFailures, uint32(0), "Should have recorded failures")
assert.Greater(t, counts.ConsecutiveFailures, uint32(0), "Should have consecutive failures")
t.Logf("Metrics: TotalFailures=%d, ConsecutiveFailures=%d", counts.TotalFailures, counts.ConsecutiveFailures)
}