Files
traefikoidc/internal/recovery/recovery_boost_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

525 lines
13 KiB
Go

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