mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
284 lines
8.6 KiB
Go
284 lines
8.6 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTokenResilienceManager(t *testing.T) {
|
|
logger := NewLogger("debug")
|
|
config := DefaultTokenResilienceConfig()
|
|
|
|
// Test basic functionality
|
|
t.Run("DefaultConfiguration", func(t *testing.T) {
|
|
manager := NewTokenResilienceManager(config, logger)
|
|
if manager == nil {
|
|
t.Fatal("Expected non-nil manager")
|
|
}
|
|
|
|
if manager.config.CircuitBreakerEnabled != true {
|
|
t.Errorf("Expected circuit breaker to be enabled by default")
|
|
}
|
|
|
|
if manager.config.RetryEnabled != true {
|
|
t.Errorf("Expected retry to be enabled by default")
|
|
}
|
|
|
|
// Test circuit breaker creation
|
|
if manager.circuitBreaker == nil {
|
|
t.Errorf("Expected circuit breaker to be created")
|
|
}
|
|
|
|
// Test retry executor creation
|
|
if manager.retryExecutor == nil {
|
|
t.Errorf("Expected retry executor to be created")
|
|
}
|
|
})
|
|
|
|
// Test resilience execution
|
|
t.Run("ResilienceExecution", func(t *testing.T) {
|
|
manager := NewTokenResilienceManager(config, logger)
|
|
|
|
// Test successful operation
|
|
callCount := 0
|
|
err := manager.ExecuteTokenOperation(context.Background(), "test_operation", func() error {
|
|
callCount++
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error for successful operation, got: %v", err)
|
|
}
|
|
|
|
if callCount != 1 {
|
|
t.Errorf("Expected function to be called once, got: %d", callCount)
|
|
}
|
|
})
|
|
|
|
// Test retry on failure
|
|
t.Run("RetryOnFailure", func(t *testing.T) {
|
|
manager := NewTokenResilienceManager(config, logger)
|
|
|
|
callCount := 0
|
|
err := manager.ExecuteTokenOperation(context.Background(), "test_retry", func() error {
|
|
callCount++
|
|
if callCount < 3 {
|
|
return fmt.Errorf("connection refused") // Retryable error
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error after retry, got: %v", err)
|
|
}
|
|
|
|
if callCount != 3 {
|
|
t.Errorf("Expected function to be called 3 times (with retries), got: %d", callCount)
|
|
}
|
|
})
|
|
|
|
// Test circuit breaker functionality
|
|
t.Run("CircuitBreakerTrip", func(t *testing.T) {
|
|
// Use config with low failure threshold for testing
|
|
testConfig := config
|
|
testConfig.CircuitBreakerConfig.MaxFailures = 2
|
|
|
|
manager := NewTokenResilienceManager(testConfig, logger)
|
|
|
|
// Cause failures to trip circuit breaker
|
|
for i := 0; i < 3; i++ {
|
|
err := manager.ExecuteTokenOperation(context.Background(), "test_circuit_trip", func() error {
|
|
return fmt.Errorf("service unavailable")
|
|
})
|
|
if err == nil {
|
|
t.Errorf("Expected error for failed operation")
|
|
}
|
|
}
|
|
|
|
// Circuit should now be open, next call should fail immediately
|
|
callCount := 0
|
|
err := manager.ExecuteTokenOperation(context.Background(), "test_circuit_open", func() error {
|
|
callCount++
|
|
return nil // This should not be called due to open circuit
|
|
})
|
|
|
|
if err == nil {
|
|
t.Errorf("Expected circuit breaker to block request")
|
|
}
|
|
|
|
if callCount > 0 {
|
|
t.Errorf("Expected function not to be called due to circuit breaker, got: %d calls", callCount)
|
|
}
|
|
})
|
|
|
|
// Test metrics collection
|
|
t.Run("MetricsCollection", func(t *testing.T) {
|
|
manager := NewTokenResilienceManager(config, logger)
|
|
|
|
// Execute some operations to generate metrics
|
|
manager.ExecuteTokenOperation(context.Background(), "metrics_test", func() error {
|
|
return nil
|
|
})
|
|
|
|
metrics := manager.GetMetrics()
|
|
if metrics == nil {
|
|
t.Fatal("Expected non-nil metrics")
|
|
}
|
|
|
|
if _, exists := metrics["circuit_breaker"]; !exists {
|
|
t.Errorf("Expected circuit breaker metrics")
|
|
}
|
|
|
|
if _, exists := metrics["retry_executor"]; !exists {
|
|
t.Errorf("Expected retry executor metrics")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMetadataCacheResilienceConfig(t *testing.T) {
|
|
config := DefaultMetadataCacheResilienceConfig()
|
|
|
|
// Test security-critical field detection
|
|
t.Run("SecurityCriticalFields", func(t *testing.T) {
|
|
if !config.IsSecurityCriticalField("jwks_uri") {
|
|
t.Errorf("Expected jwks_uri to be security-critical")
|
|
}
|
|
|
|
if !config.IsSecurityCriticalField("token_endpoint") {
|
|
t.Errorf("Expected token_endpoint to be security-critical")
|
|
}
|
|
|
|
if config.IsSecurityCriticalField("non_critical_field") {
|
|
t.Errorf("Expected non_critical_field to not be security-critical")
|
|
}
|
|
})
|
|
|
|
// Test effective max grace period calculation
|
|
t.Run("EffectiveMaxGracePeriod", func(t *testing.T) {
|
|
maxGrace := config.GetEffectiveMaxGracePeriod("jwks_uri")
|
|
expectedMax := 15 * time.Minute // Allan's security limit
|
|
|
|
if maxGrace != expectedMax {
|
|
t.Errorf("Expected security-critical field max grace period to be %v, got %v", expectedMax, maxGrace)
|
|
}
|
|
|
|
normalMax := config.GetEffectiveMaxGracePeriod("non_critical_field")
|
|
expectedNormal := 30 * time.Minute
|
|
|
|
if normalMax != expectedNormal {
|
|
t.Errorf("Expected normal field max grace period to be %v, got %v", expectedNormal, normalMax)
|
|
}
|
|
})
|
|
|
|
// Test progressive grace period values
|
|
t.Run("ProgressiveGracePeriods", func(t *testing.T) {
|
|
if config.InitialGracePeriod != 5*time.Minute {
|
|
t.Errorf("Expected initial grace period to be 5 minutes, got %v", config.InitialGracePeriod)
|
|
}
|
|
|
|
if config.ExtendedGracePeriod != 15*time.Minute {
|
|
t.Errorf("Expected extended grace period to be 15 minutes, got %v", config.ExtendedGracePeriod)
|
|
}
|
|
|
|
if config.MaxGracePeriod != 30*time.Minute {
|
|
t.Errorf("Expected max grace period to be 30 minutes, got %v", config.MaxGracePeriod)
|
|
}
|
|
|
|
if config.SecurityCriticalMaxGracePeriod != 15*time.Minute {
|
|
t.Errorf("Expected security-critical max grace period to be 15 minutes (Allan's limit), got %v", config.SecurityCriticalMaxGracePeriod)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConnectionPoolingFix(t *testing.T) {
|
|
// Test that token HTTP client uses pooled connections
|
|
t.Run("TokenHTTPClientUsesPool", func(t *testing.T) {
|
|
client1 := CreateTokenHTTPClient()
|
|
client2 := CreateTokenHTTPClient()
|
|
|
|
if client1 == nil || client2 == nil {
|
|
t.Fatal("Expected non-nil HTTP clients")
|
|
}
|
|
|
|
// Both clients should use the same underlying transport pool
|
|
// This is hard to test directly, but we can verify they're created successfully
|
|
if client1.Transport == nil || client2.Transport == nil {
|
|
t.Error("Expected non-nil transports")
|
|
}
|
|
|
|
// Verify timeout configuration
|
|
if client1.Timeout <= 0 {
|
|
t.Errorf("Expected positive timeout, got %v", client1.Timeout)
|
|
}
|
|
})
|
|
|
|
// Test that the helper functions use pooled clients
|
|
t.Run("HelperFunctionsUsePooling", func(t *testing.T) {
|
|
// This is a smoke test to ensure the helpers compile and can be called
|
|
// The actual pooling logic is tested by verifying no nil clients are created
|
|
client := CreateTokenHTTPClient()
|
|
if client == nil {
|
|
t.Error("Expected CreateTokenHTTPClient to return non-nil client")
|
|
return
|
|
}
|
|
|
|
defaultClient := CreateDefaultHTTPClient()
|
|
if defaultClient == nil {
|
|
t.Error("Expected CreateDefaultHTTPClient to return non-nil client")
|
|
return
|
|
}
|
|
|
|
// Test that both have transports (indicating they use the pool)
|
|
if client.Transport == nil {
|
|
t.Error("Expected token client to have non-nil transport")
|
|
}
|
|
|
|
if defaultClient.Transport == nil {
|
|
t.Error("Expected default client to have non-nil transport")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test backward compatibility - ensure old behavior still works when resilience manager is nil
|
|
func TestTokenResilienceBackwardCompatibility(t *testing.T) {
|
|
// Create a TraefikOidc instance without token resilience manager
|
|
tOidc := &TraefikOidc{
|
|
logger: NewLogger("debug"),
|
|
tokenResilienceManager: nil, // Explicitly nil for backward compatibility test
|
|
}
|
|
|
|
// Test that getNewTokenWithRefreshToken still works with nil resilience manager
|
|
t.Run("TokenRefreshFallback", func(t *testing.T) {
|
|
// This should not panic and should use the fallback logic
|
|
_, err := tOidc.getNewTokenWithRefreshToken("test-refresh-token")
|
|
|
|
// We expect an error because we don't have a real token endpoint configured,
|
|
// but we should NOT get a panic from nil pointer dereference
|
|
if err == nil {
|
|
t.Log("No error (possibly valid if using mock endpoints)")
|
|
} else {
|
|
t.Logf("Expected error due to missing configuration: %v", err)
|
|
}
|
|
|
|
// The important thing is that we get here without panicking
|
|
})
|
|
|
|
// Test that exchangeCodeForToken still works with nil resilience manager
|
|
t.Run("CodeExchangeFallback", func(t *testing.T) {
|
|
// This should not panic and should use the fallback logic
|
|
_, err := tOidc.exchangeCodeForToken("test-code", "http://localhost/callback", "")
|
|
|
|
// We expect an error because we don't have a real token endpoint configured,
|
|
// but we should NOT get a panic from nil pointer dereference
|
|
if err == nil {
|
|
t.Log("No error (possibly valid if using mock endpoints)")
|
|
} else {
|
|
t.Logf("Expected error due to missing configuration: %v", err)
|
|
}
|
|
|
|
// The important thing is that we get here without panicking
|
|
})
|
|
}
|