Files
traefikoidc/internal/recovery/recovery_test.go
T
lukaszraczylo e64fc7f730 Add redis support for distributed caching (#83)
* Add redis support for distributed caching

* Move towards the self-provided Redis connection pool and RESP protocol implementation.
Official redis client library won't work with yaegi.

* fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* ... and another all nighter.

* fixup! ... and another all nighter.

* fixup! fixup! ... and another all nighter.

* fixup! fixup! fixup! ... and another all nighter.

* Resolve issue #85 by adding ability to set custom claims in JWT tokens

* Remove redundant validation in auth middleware ( issue #89 )

* Add ability to set cookie prefix for session cookies ( #87 )

* fixup! Add ability to set cookie prefix for session cookies ( #87 )

* Add ability to set cookie max age - issue #91

* Potential fix for code scanning alert no. 10: Size computation for allocation may overflow

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fixup! Merge main into 0.8.0-redis: resolve conflicts

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-30 02:18:46 +00:00

548 lines
13 KiB
Go

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