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) }