mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
e64fc7f730
* 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>
525 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|