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

194 lines
5.6 KiB
Go

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