mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
fixup! fixup! ... and another all nighter.
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogDebug Tests
|
||||
func TestBaseRecoveryMechanism_LogDebug(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
base := NewBaseRecoveryMechanism("test-debug", logger)
|
||||
|
||||
// Call LogDebug
|
||||
base.LogDebug("test message: %s", "value")
|
||||
|
||||
// Verify debug log was called
|
||||
if len(logger.debugLog) != 1 {
|
||||
t.Errorf("Expected 1 debug log entry, got %d", len(logger.debugLog))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_LogDebug_NilLogger(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", nil)
|
||||
|
||||
// Should not panic with nil logger
|
||||
base.LogDebug("this should not crash")
|
||||
}
|
||||
|
||||
// HTTPError Tests
|
||||
func TestHTTPError_Error(t *testing.T) {
|
||||
err := &HTTPError{
|
||||
StatusCode: 404,
|
||||
Message: "Not Found",
|
||||
}
|
||||
|
||||
expected := "HTTP 404: Not Found"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected '%s', got '%s'", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPError_IsRetryable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
retryable bool
|
||||
}{
|
||||
{"500 Internal Server Error", 500, true},
|
||||
{"502 Bad Gateway", 502, true},
|
||||
{"503 Service Unavailable", 503, true},
|
||||
{"504 Gateway Timeout", 504, true},
|
||||
{"429 Too Many Requests", 429, true},
|
||||
{"408 Request Timeout", 408, true},
|
||||
{"400 Bad Request", 400, false},
|
||||
{"401 Unauthorized", 401, false},
|
||||
{"403 Forbidden", 403, false},
|
||||
{"404 Not Found", 404, false},
|
||||
{"200 OK", 200, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &HTTPError{
|
||||
StatusCode: tt.statusCode,
|
||||
Message: tt.name,
|
||||
}
|
||||
|
||||
if err.IsRetryable() != tt.retryable {
|
||||
t.Errorf("StatusCode %d: expected retryable=%v, got %v",
|
||||
tt.statusCode, tt.retryable, err.IsRetryable())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OIDCError Tests
|
||||
func TestOIDCError_Error_WithDescription(t *testing.T) {
|
||||
err := &OIDCError{
|
||||
Code: "invalid_request",
|
||||
Description: "Missing required parameter",
|
||||
}
|
||||
|
||||
expected := "OIDC error invalid_request: Missing required parameter"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected '%s', got '%s'", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCError_Error_WithoutDescription(t *testing.T) {
|
||||
err := &OIDCError{
|
||||
Code: "server_error",
|
||||
}
|
||||
|
||||
expected := "OIDC error: server_error"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected '%s', got '%s'", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCError_IsRetryable(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
retryable bool
|
||||
}{
|
||||
{"temporarily_unavailable", true},
|
||||
{"server_error", true},
|
||||
{"invalid_request", false},
|
||||
{"invalid_client", false},
|
||||
{"invalid_grant", false},
|
||||
{"unauthorized_client", false},
|
||||
{"unsupported_grant_type", false},
|
||||
{"access_denied", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
err := &OIDCError{
|
||||
Code: tt.code,
|
||||
}
|
||||
|
||||
if err.IsRetryable() != tt.retryable {
|
||||
t.Errorf("Code '%s': expected retryable=%v, got %v",
|
||||
tt.code, tt.retryable, err.IsRetryable())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FallbackMechanism Tests
|
||||
func TestNewFallbackMechanism(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackFunc := func() error { return nil }
|
||||
|
||||
fm := NewFallbackMechanism("test-fallback", logger, fallbackFunc)
|
||||
|
||||
if fm == nil {
|
||||
t.Fatal("Expected NewFallbackMechanism to return non-nil")
|
||||
}
|
||||
|
||||
if fm.name != "test-fallback" {
|
||||
t.Errorf("Expected name 'test-fallback', got '%s'", fm.name)
|
||||
}
|
||||
|
||||
if fm.fallbackFunc == nil {
|
||||
t.Error("Expected fallbackFunc to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_ExecuteWithContext_PrimarySuccess(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackCalled := false
|
||||
fallbackFunc := func() error {
|
||||
fallbackCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
fm := NewFallbackMechanism("test", logger, fallbackFunc)
|
||||
|
||||
// Primary function succeeds
|
||||
err := fm.ExecuteWithContext(context.Background(), func() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if fallbackCalled {
|
||||
t.Error("Expected fallback to not be called when primary succeeds")
|
||||
}
|
||||
|
||||
if fm.successCount != 1 {
|
||||
t.Errorf("Expected successCount=1, got %d", fm.successCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_ExecuteWithContext_FallbackSuccess(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackCalled := false
|
||||
fallbackFunc := func() error {
|
||||
fallbackCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
fm := NewFallbackMechanism("test", logger, fallbackFunc)
|
||||
|
||||
// Primary fails, fallback succeeds
|
||||
err := fm.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("primary failed")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error (fallback succeeded), got %v", err)
|
||||
}
|
||||
|
||||
if !fallbackCalled {
|
||||
t.Error("Expected fallback to be called")
|
||||
}
|
||||
|
||||
if fm.successCount != 1 {
|
||||
t.Errorf("Expected successCount=1, got %d", fm.successCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_ExecuteWithContext_BothFail(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackFunc := func() error {
|
||||
return errors.New("fallback failed")
|
||||
}
|
||||
|
||||
fm := NewFallbackMechanism("test", logger, fallbackFunc)
|
||||
|
||||
// Both primary and fallback fail
|
||||
err := fm.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("primary failed")
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error when both primary and fallback fail")
|
||||
}
|
||||
|
||||
if fm.failureCount != 1 {
|
||||
t.Errorf("Expected failureCount=1, got %d", fm.failureCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_ExecuteWithContext_NoFallback(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fm := NewFallbackMechanism("test", logger, nil) // No fallback function
|
||||
|
||||
// Primary fails, no fallback
|
||||
primaryErr := errors.New("primary failed")
|
||||
err := fm.ExecuteWithContext(context.Background(), func() error {
|
||||
return primaryErr
|
||||
})
|
||||
|
||||
if err != primaryErr {
|
||||
t.Errorf("Expected primary error %v, got %v", primaryErr, err)
|
||||
}
|
||||
|
||||
if fm.failureCount != 1 {
|
||||
t.Errorf("Expected failureCount=1, got %d", fm.failureCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_ExecuteWithContext_ContextCanceled(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackFunc := func() error { return nil }
|
||||
fm := NewFallbackMechanism("test", logger, fallbackFunc)
|
||||
|
||||
// Context already canceled
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := fm.ExecuteWithContext(ctx, func() error {
|
||||
t.Error("Function should not be called when context is canceled")
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != context.Canceled {
|
||||
t.Errorf("Expected context.Canceled error, got %v", err)
|
||||
}
|
||||
|
||||
if fm.failureCount != 1 {
|
||||
t.Errorf("Expected failureCount=1, got %d", fm.failureCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_Reset(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fm := NewFallbackMechanism("test", logger, nil)
|
||||
|
||||
// Record some metrics
|
||||
fm.RecordRequest()
|
||||
fm.RecordSuccess()
|
||||
fm.RecordFailure()
|
||||
|
||||
if fm.totalRequests == 0 {
|
||||
t.Error("Expected some requests before reset")
|
||||
}
|
||||
|
||||
// Reset
|
||||
fm.Reset()
|
||||
|
||||
if fm.totalRequests != 0 {
|
||||
t.Errorf("Expected totalRequests=0 after reset, got %d", fm.totalRequests)
|
||||
}
|
||||
|
||||
if fm.successCount != 0 {
|
||||
t.Errorf("Expected successCount=0 after reset, got %d", fm.successCount)
|
||||
}
|
||||
|
||||
if fm.failureCount != 0 {
|
||||
t.Errorf("Expected failureCount=0 after reset, got %d", fm.failureCount)
|
||||
}
|
||||
|
||||
if fm.lastSuccessStr != "never" {
|
||||
t.Errorf("Expected lastSuccessStr='never' after reset, got '%s'", fm.lastSuccessStr)
|
||||
}
|
||||
|
||||
if fm.lastFailureStr != "never" {
|
||||
t.Errorf("Expected lastFailureStr='never' after reset, got '%s'", fm.lastFailureStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_IsAvailable(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fm := NewFallbackMechanism("test", logger, nil)
|
||||
|
||||
// Fallback mechanism is always available
|
||||
if !fm.IsAvailable() {
|
||||
t.Error("Expected IsAvailable to return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_GetMetrics(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fallbackFunc := func() error { return nil }
|
||||
fm := NewFallbackMechanism("test-metrics", logger, fallbackFunc)
|
||||
|
||||
fm.RecordRequest()
|
||||
fm.RecordSuccess()
|
||||
|
||||
metrics := fm.GetMetrics()
|
||||
|
||||
if metrics == nil {
|
||||
t.Fatal("Expected GetMetrics to return non-nil")
|
||||
}
|
||||
|
||||
if metrics["type"] != "fallback" {
|
||||
t.Errorf("Expected type='fallback', got %v", metrics["type"])
|
||||
}
|
||||
|
||||
if metrics["name"] != "test-metrics" {
|
||||
t.Errorf("Expected name='test-metrics', got %v", metrics["name"])
|
||||
}
|
||||
|
||||
if metrics["hasFallback"] != true {
|
||||
t.Error("Expected hasFallback=true")
|
||||
}
|
||||
|
||||
if metrics["totalRequests"].(int64) != 1 {
|
||||
t.Errorf("Expected totalRequests=1, got %v", metrics["totalRequests"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackMechanism_GetMetrics_NoFallback(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
fm := NewFallbackMechanism("test", logger, nil)
|
||||
|
||||
metrics := fm.GetMetrics()
|
||||
|
||||
if metrics["hasFallback"] != false {
|
||||
t.Error("Expected hasFallback=false when no fallback function")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CIRCUIT BREAKER ADDITIONAL TESTS
|
||||
// ============================================================================
|
||||
|
||||
// TestCircuitBreaker_Execute tests the legacy Execute method
|
||||
func TestCircuitBreaker_Execute(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, logger)
|
||||
|
||||
// Test successful execution via Execute (legacy method)
|
||||
called := false
|
||||
err := cb.Execute(func() error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Error("Expected function to be called")
|
||||
}
|
||||
|
||||
// Test error propagation via Execute
|
||||
expectedErr := errors.New("test error")
|
||||
err = cb.Execute(func() error {
|
||||
return expectedErr
|
||||
})
|
||||
|
||||
if err != expectedErr {
|
||||
t.Errorf("Expected error %v, got %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCircuitBreaker_ForceOpen tests forcing circuit breaker to open state
|
||||
func TestCircuitBreaker_ForceOpen(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, logger)
|
||||
|
||||
// Initially circuit should be closed
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Errorf("Expected initial state Closed, got %v", cb.GetState())
|
||||
}
|
||||
|
||||
// Force open
|
||||
cb.ForceOpen()
|
||||
|
||||
// Verify state is now open
|
||||
if cb.GetState() != CircuitBreakerOpen {
|
||||
t.Errorf("Expected state Open after ForceOpen, got %v", cb.GetState())
|
||||
}
|
||||
|
||||
// Verify circuit blocks requests
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
t.Error("Function should not be called when circuit is forced open")
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error when circuit is forced open")
|
||||
}
|
||||
|
||||
// Verify logger was called
|
||||
if len(logger.logs) == 0 {
|
||||
t.Error("Expected info log when forcing circuit open")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCircuitBreaker_ForceClosed tests forcing circuit breaker to closed state
|
||||
func TestCircuitBreaker_ForceClosed(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
config.FailureThreshold = 1
|
||||
cb := NewCircuitBreaker(config, logger)
|
||||
|
||||
// Trigger failures to open circuit
|
||||
cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("failure")
|
||||
})
|
||||
cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("failure")
|
||||
})
|
||||
cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("failure")
|
||||
})
|
||||
|
||||
// Circuit should be open after failures
|
||||
if cb.GetState() != CircuitBreakerOpen {
|
||||
t.Errorf("Expected state Open after failures, got %v", cb.GetState())
|
||||
}
|
||||
|
||||
// Force closed
|
||||
cb.ForceClosed()
|
||||
|
||||
// Verify state is now closed
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Errorf("Expected state Closed after ForceClosed, got %v", cb.GetState())
|
||||
}
|
||||
|
||||
// Verify circuit allows requests
|
||||
called := false
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error after forcing closed, got %v", err)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Error("Expected function to be called after forcing closed")
|
||||
}
|
||||
|
||||
// Verify counters are reset
|
||||
metrics := cb.GetMetrics()
|
||||
consecutiveFailures, ok := metrics["consecutiveFailures"].(int32)
|
||||
if !ok || consecutiveFailures != 0 {
|
||||
t.Errorf("Expected consecutiveFailures=0 after ForceClosed, got %v", consecutiveFailures)
|
||||
}
|
||||
|
||||
// Verify logger was called
|
||||
if len(logger.logs) == 0 {
|
||||
t.Error("Expected info log when forcing circuit closed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCircuitBreaker_ForceOpen_AllowsRecovery tests that forced open can transition to half-open
|
||||
func TestCircuitBreaker_ForceOpen_AllowsRecovery(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
config.Timeout = 50 * time.Millisecond // Very short timeout for testing
|
||||
cb := NewCircuitBreaker(config, logger)
|
||||
|
||||
// Force open
|
||||
cb.ForceOpen()
|
||||
|
||||
// Wait for timeout to allow transition to half-open
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Circuit should allow a test request in half-open state
|
||||
called := false
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
|
||||
// After successful execution, circuit should close
|
||||
if err != nil {
|
||||
t.Logf("Note: Circuit may still be in transition, error: %v", err)
|
||||
}
|
||||
|
||||
if called {
|
||||
// If called, verify circuit recovered
|
||||
state := cb.GetState()
|
||||
if state != CircuitBreakerClosed && state != CircuitBreakerHalfOpen {
|
||||
t.Errorf("Expected Closed or HalfOpen after successful recovery, got %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock logger for testing
|
||||
type mockLogger struct {
|
||||
mu sync.Mutex
|
||||
logs []string
|
||||
errLogs []string
|
||||
debugLog []string
|
||||
}
|
||||
|
||||
func (m *mockLogger) Logf(format string, args ...interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.logs = append(m.logs, format)
|
||||
}
|
||||
|
||||
func (m *mockLogger) ErrorLogf(format string, args ...interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.errLogs = append(m.errLogs, format)
|
||||
}
|
||||
|
||||
func (m *mockLogger) DebugLogf(format string, args ...interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.debugLog = append(m.debugLog, format)
|
||||
}
|
||||
|
||||
// BaseRecoveryMechanism tests
|
||||
func TestNewBaseRecoveryMechanism(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
base := NewBaseRecoveryMechanism("test-recovery", logger)
|
||||
|
||||
if base == nil {
|
||||
t.Fatal("Expected NewBaseRecoveryMechanism to return non-nil")
|
||||
}
|
||||
|
||||
if base.name != "test-recovery" {
|
||||
t.Errorf("Expected name 'test-recovery', got '%s'", base.name)
|
||||
}
|
||||
|
||||
if base.totalRequests != 0 {
|
||||
t.Error("Expected totalRequests to be 0")
|
||||
}
|
||||
|
||||
if base.successCount != 0 {
|
||||
t.Error("Expected successCount to be 0")
|
||||
}
|
||||
|
||||
if base.failureCount != 0 {
|
||||
t.Error("Expected failureCount to be 0")
|
||||
}
|
||||
|
||||
if base.lastSuccessStr != "never" {
|
||||
t.Error("Expected lastSuccessStr to be 'never'")
|
||||
}
|
||||
|
||||
if base.lastFailureStr != "never" {
|
||||
t.Error("Expected lastFailureStr to be 'never'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_RecordRequest(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", &mockLogger{})
|
||||
|
||||
base.RecordRequest()
|
||||
if atomic.LoadInt64(&base.totalRequests) != 1 {
|
||||
t.Error("Expected totalRequests to be 1")
|
||||
}
|
||||
|
||||
base.RecordRequest()
|
||||
base.RecordRequest()
|
||||
if atomic.LoadInt64(&base.totalRequests) != 3 {
|
||||
t.Error("Expected totalRequests to be 3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_RecordSuccess(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", &mockLogger{})
|
||||
|
||||
base.RecordSuccess()
|
||||
if atomic.LoadInt64(&base.successCount) != 1 {
|
||||
t.Error("Expected successCount to be 1")
|
||||
}
|
||||
|
||||
base.successMutex.RLock()
|
||||
lastSuccess := base.lastSuccessStr
|
||||
base.successMutex.RUnlock()
|
||||
|
||||
if lastSuccess == "never" {
|
||||
t.Error("Expected lastSuccessStr to be updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_RecordFailure(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", &mockLogger{})
|
||||
|
||||
base.RecordFailure()
|
||||
if atomic.LoadInt64(&base.failureCount) != 1 {
|
||||
t.Error("Expected failureCount to be 1")
|
||||
}
|
||||
|
||||
base.failureMutex.RLock()
|
||||
lastFailure := base.lastFailureStr
|
||||
base.failureMutex.RUnlock()
|
||||
|
||||
if lastFailure == "never" {
|
||||
t.Error("Expected lastFailureStr to be updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_GetBaseMetrics(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", &mockLogger{})
|
||||
|
||||
base.RecordRequest()
|
||||
base.RecordRequest()
|
||||
base.RecordSuccess()
|
||||
base.RecordFailure()
|
||||
|
||||
metrics := base.GetBaseMetrics()
|
||||
|
||||
if metrics["totalRequests"].(int64) != 2 {
|
||||
t.Error("Expected totalRequests to be 2")
|
||||
}
|
||||
|
||||
if metrics["successCount"].(int64) != 1 {
|
||||
t.Error("Expected successCount to be 1")
|
||||
}
|
||||
|
||||
if metrics["failureCount"].(int64) != 1 {
|
||||
t.Error("Expected failureCount to be 1")
|
||||
}
|
||||
|
||||
if metrics["successRate"].(string) != "50.00%" {
|
||||
t.Errorf("Expected successRate to be '50.00%%', got %v", metrics["successRate"])
|
||||
}
|
||||
|
||||
if metrics["name"].(string) != "test" {
|
||||
t.Error("Expected name to be 'test'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseRecoveryMechanism_ConcurrentAccess(t *testing.T) {
|
||||
base := NewBaseRecoveryMechanism("test", &mockLogger{})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
// Concurrent requests
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
base.RecordRequest()
|
||||
}()
|
||||
}
|
||||
|
||||
// Concurrent successes
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
base.RecordSuccess()
|
||||
}()
|
||||
}
|
||||
|
||||
// Concurrent failures
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
base.RecordFailure()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if atomic.LoadInt64(&base.totalRequests) != int64(iterations) {
|
||||
t.Errorf("Expected %d total requests, got %d", iterations, base.totalRequests)
|
||||
}
|
||||
|
||||
if atomic.LoadInt64(&base.successCount) != int64(iterations) {
|
||||
t.Errorf("Expected %d successes, got %d", iterations, base.successCount)
|
||||
}
|
||||
|
||||
if atomic.LoadInt64(&base.failureCount) != int64(iterations) {
|
||||
t.Errorf("Expected %d failures, got %d", iterations, base.failureCount)
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreakerState tests
|
||||
func TestCircuitBreakerState_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
state CircuitBreakerState
|
||||
expected string
|
||||
}{
|
||||
{CircuitBreakerClosed, "closed"},
|
||||
{CircuitBreakerOpen, "open"},
|
||||
{CircuitBreakerHalfOpen, "half-open"},
|
||||
{CircuitBreakerState(99), "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.state.String() != tt.expected {
|
||||
t.Errorf("Expected state %d to be '%s', got '%s'", tt.state, tt.expected, tt.state.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreakerConfig tests
|
||||
func TestDefaultCircuitBreakerConfig(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
|
||||
if config.FailureThreshold != 5 {
|
||||
t.Errorf("Expected FailureThreshold 5, got %d", config.FailureThreshold)
|
||||
}
|
||||
|
||||
if config.SuccessThreshold != 2 {
|
||||
t.Errorf("Expected SuccessThreshold 2, got %d", config.SuccessThreshold)
|
||||
}
|
||||
|
||||
if config.Timeout != 30*time.Second {
|
||||
t.Errorf("Expected Timeout 30s, got %v", config.Timeout)
|
||||
}
|
||||
|
||||
if config.MaxRequests != 3 {
|
||||
t.Errorf("Expected MaxRequests 3, got %d", config.MaxRequests)
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreaker tests
|
||||
func TestNewCircuitBreaker(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
logger := &mockLogger{}
|
||||
cb := NewCircuitBreaker(config, logger)
|
||||
|
||||
if cb == nil {
|
||||
t.Fatal("Expected NewCircuitBreaker to return non-nil")
|
||||
}
|
||||
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Error("Expected initial state to be Closed")
|
||||
}
|
||||
|
||||
if cb.config.FailureThreshold != 5 {
|
||||
t.Error("Expected config to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_InitiallyClosed(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
if !cb.IsAvailable() {
|
||||
t.Error("Expected circuit breaker to be available initially")
|
||||
}
|
||||
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Error("Expected state to be Closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_ExecuteWithContext_Success(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
callCount := 0
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
callCount++
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if callCount != 1 {
|
||||
t.Error("Expected function to be called once")
|
||||
}
|
||||
|
||||
if atomic.LoadInt64(&cb.successCount) != 1 {
|
||||
t.Error("Expected success count to be 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_ExecuteWithContext_Failure(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
testErr := errors.New("test error")
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return testErr
|
||||
})
|
||||
|
||||
if err != testErr {
|
||||
t.Errorf("Expected error %v, got %v", testErr, err)
|
||||
}
|
||||
|
||||
if atomic.LoadInt64(&cb.failureCount) != 1 {
|
||||
t.Error("Expected failure count to be 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_OpensAfterThresholdFailures(t *testing.T) {
|
||||
config := CircuitBreakerConfig{
|
||||
FailureThreshold: 3,
|
||||
SuccessThreshold: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
MaxRequests: 2,
|
||||
}
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Cause failures to reach threshold
|
||||
for i := 0; i < 3; i++ {
|
||||
_ = cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return testErr
|
||||
})
|
||||
}
|
||||
|
||||
// Circuit should now be open
|
||||
if cb.GetState() != CircuitBreakerOpen {
|
||||
t.Errorf("Expected state to be Open after %d failures, got %s", config.FailureThreshold, cb.GetState())
|
||||
}
|
||||
|
||||
if cb.IsAvailable() {
|
||||
t.Error("Expected circuit breaker to be unavailable when open")
|
||||
}
|
||||
|
||||
// Subsequent requests should be blocked
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
t.Error("Function should not be called when circuit is open")
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error when circuit is open")
|
||||
}
|
||||
|
||||
if err.Error() != "circuit breaker is open" {
|
||||
t.Errorf("Expected 'circuit breaker is open' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_TransitionsToHalfOpen(t *testing.T) {
|
||||
config := CircuitBreakerConfig{
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 1,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
MaxRequests: 2,
|
||||
}
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
// Open the circuit
|
||||
for i := 0; i < 2; i++ {
|
||||
_ = cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("fail")
|
||||
})
|
||||
}
|
||||
|
||||
if cb.GetState() != CircuitBreakerOpen {
|
||||
t.Error("Expected circuit to be open")
|
||||
}
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
// Next request should transition to half-open
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error in half-open state, got %v", err)
|
||||
}
|
||||
|
||||
// State should be closed after successful request in half-open
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Errorf("Expected state to be Closed after success in half-open, got %s", cb.GetState())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_Reset(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
// Record some metrics
|
||||
cb.RecordRequest()
|
||||
cb.RecordSuccess()
|
||||
cb.RecordFailure()
|
||||
|
||||
// Reset
|
||||
cb.Reset()
|
||||
|
||||
if atomic.LoadInt64(&cb.totalRequests) != 0 {
|
||||
t.Error("Expected totalRequests to be 0 after reset")
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&cb.consecutiveFailures) != 0 {
|
||||
t.Error("Expected consecutiveFailures to be 0 after reset")
|
||||
}
|
||||
|
||||
if cb.GetState() != CircuitBreakerClosed {
|
||||
t.Error("Expected state to be Closed after reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_GetMetrics(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
cb.RecordRequest()
|
||||
cb.RecordSuccess()
|
||||
|
||||
metrics := cb.GetMetrics()
|
||||
|
||||
if metrics == nil {
|
||||
t.Fatal("Expected metrics to be non-nil")
|
||||
}
|
||||
|
||||
if metrics["state"] != "closed" {
|
||||
t.Errorf("Expected state 'closed', got %v", metrics["state"])
|
||||
}
|
||||
|
||||
if metrics["totalRequests"].(int64) != 1 {
|
||||
t.Errorf("Expected totalRequests 1, got %v", metrics["totalRequests"])
|
||||
}
|
||||
|
||||
if metrics["successCount"].(int64) != 1 {
|
||||
t.Error("Expected successCount to be 1")
|
||||
}
|
||||
|
||||
if _, ok := metrics["config"]; !ok {
|
||||
t.Error("Expected config in metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_ConcurrentExecute(t *testing.T) {
|
||||
config := CircuitBreakerConfig{
|
||||
FailureThreshold: 10,
|
||||
SuccessThreshold: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
MaxRequests: 5,
|
||||
}
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
successCount := atomic.Int32{}
|
||||
iterations := 50
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
err := cb.ExecuteWithContext(context.Background(), func() error {
|
||||
time.Sleep(time.Millisecond)
|
||||
if idx%2 == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New("error")
|
||||
})
|
||||
if err == nil {
|
||||
successCount.Add(1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Should have processed requests without panicking
|
||||
if atomic.LoadInt64(&cb.totalRequests) < int64(iterations) {
|
||||
t.Logf("Processed %d requests out of %d (some may have been blocked)", cb.totalRequests, iterations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_ContextCancellation(t *testing.T) {
|
||||
config := DefaultCircuitBreakerConfig()
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Execute with valid context
|
||||
err := cb.ExecuteWithContext(ctx, func() error {
|
||||
// Cancel during execution
|
||||
cancel()
|
||||
// Circuit breaker doesn't check context during execution by design
|
||||
// It's the responsibility of the function to check context
|
||||
return nil
|
||||
})
|
||||
|
||||
// Should complete successfully - circuit breaker passes context but doesn't enforce it
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_HalfOpenMaxRequests(t *testing.T) {
|
||||
config := CircuitBreakerConfig{
|
||||
FailureThreshold: 2,
|
||||
SuccessThreshold: 1,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
MaxRequests: 1,
|
||||
}
|
||||
cb := NewCircuitBreaker(config, &mockLogger{})
|
||||
|
||||
// Open the circuit
|
||||
for i := 0; i < 2; i++ {
|
||||
_ = cb.ExecuteWithContext(context.Background(), func() error {
|
||||
return errors.New("fail")
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for timeout to transition to half-open
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
// First request should be allowed
|
||||
allowed := cb.allowRequest()
|
||||
if !allowed {
|
||||
t.Error("Expected first request to be allowed in half-open state")
|
||||
}
|
||||
|
||||
// Manually transition to half-open if not already
|
||||
cb.stateMutex.Lock()
|
||||
atomic.StoreInt32(&cb.state, int32(CircuitBreakerHalfOpen))
|
||||
cb.stateMutex.Unlock()
|
||||
|
||||
// Increment half-open requests to max
|
||||
atomic.StoreInt32(&cb.halfOpenRequests, int32(config.MaxRequests))
|
||||
|
||||
// Next request should be blocked
|
||||
allowed = cb.allowRequest()
|
||||
if allowed {
|
||||
t.Error("Expected request to be blocked when max half-open requests reached")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user