146 lines
5.5 KiB
Go
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)
|
|
}
|