package services import ( "context" "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" ) func TestNewCircuitBreakerHTTPClient(t *testing.T) { logger := zaptest.NewLogger(t) client := &http.Client{Timeout: 5 * time.Second} cbClient := NewCircuitBreakerHTTPClient(client, "test-circuit", logger) assert.NotNil(t, cbClient) assert.NotNil(t, cbClient.client) assert.NotNil(t, cbClient.circuitBreaker) assert.Equal(t, "test-circuit", cbClient.circuitBreaker.Name()) assert.Equal(t, gobreaker.StateClosed, cbClient.circuitBreaker.State()) } func TestCircuitBreakerHTTPClient_Do_Success(t *testing.T) { logger := zaptest.NewLogger(t) // Mock server qui retourne 200 OK server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} cbClient := NewCircuitBreakerHTTPClient(client, "test-success", logger) req, err := http.NewRequest("GET", server.URL, nil) require.NoError(t, err) resp, err := cbClient.Do(req) require.NoError(t, err) assert.NotNil(t, resp) assert.Equal(t, http.StatusOK, resp.StatusCode) resp.Body.Close() // Vérifier que le circuit breaker est toujours fermé assert.Equal(t, gobreaker.StateClosed, cbClient.circuitBreaker.State()) } func TestCircuitBreakerHTTPClient_Do_ServerError(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} // Créer un circuit breaker avec seuil bas pour tester rapidement cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: "test-5xx", MaxRequests: 3, Interval: 1 * time.Second, Timeout: 1 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures >= 3 // S'ouvre après 3 échecs }, }) cbClient := &CircuitBreakerHTTPClient{ client: client, circuitBreaker: cb, logger: logger, } // Faire 3 requêtes qui échouent (500) for i := 0; i < 3; i++ { 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(), "server error: 500") if resp != nil { resp.Body.Close() } } // Vérifier que le circuit breaker est maintenant ouvert // Note: Il peut y avoir un délai, donc on vérifie après un court instant time.Sleep(100 * time.Millisecond) state := cbClient.circuitBreaker.State() assert.True(t, state == gobreaker.StateOpen || state == gobreaker.StateHalfOpen, fmt.Sprintf("Expected Open or HalfOpen, got %v", state)) } func TestCircuitBreakerHTTPClient_Do_OpenState(t *testing.T) { logger := zaptest.NewLogger(t) client := &http.Client{Timeout: 5 * time.Second} // Créer un circuit breaker déjà ouvert cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: "test-open", MaxRequests: 1, Interval: 1 * time.Second, Timeout: 1 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures >= 1 }, }) // Forcer l'ouverture en faisant échouer une requête server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() req, _ := http.NewRequest("GET", server.URL, nil) cb.Execute(func() (interface{}, error) { return nil, fmt.Errorf("test error") }) cbClient := &CircuitBreakerHTTPClient{ client: client, circuitBreaker: cb, logger: logger, } // Attendre que le circuit breaker s'ouvre time.Sleep(100 * time.Millisecond) // Tenter une nouvelle requête - devrait être rejetée 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) } func TestCircuitBreakerHTTPClient_DoWithContext(t *testing.T) { logger := zaptest.NewLogger(t) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} cbClient := NewCircuitBreakerHTTPClient(client, "test-context", logger) ctx := context.Background() req, err := http.NewRequest("GET", server.URL, nil) require.NoError(t, err) resp, err := cbClient.DoWithContext(ctx, req) require.NoError(t, err) assert.NotNil(t, resp) assert.Equal(t, http.StatusOK, resp.StatusCode) resp.Body.Close() } func TestCircuitBreakerHTTPClient_DoWithContext_Cancelled(t *testing.T) { logger := zaptest.NewLogger(t) // Mock server qui prend du temps server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) w.WriteHeader(http.StatusOK) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} cbClient := NewCircuitBreakerHTTPClient(client, "test-cancelled", logger) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() req, err := http.NewRequest("GET", server.URL, nil) require.NoError(t, err) resp, err := cbClient.DoWithContext(ctx, req) assert.Error(t, err) assert.Nil(t, resp) assert.Contains(t, err.Error(), "context deadline exceeded") }