fixup! fixup! fixup! fixup! Cleanup.

This commit is contained in:
2025-09-28 00:12:16 +01:00
parent 5dfbbff858
commit 6d758276b5
12 changed files with 4997 additions and 17 deletions
+224
View File
@@ -0,0 +1,224 @@
package traefikoidc
import (
"errors"
"sync"
"testing"
"time"
)
// globalRegistryMutex protects only the global registry operations
var globalRegistryMutex sync.Mutex
// TestTaskCircuitBreakerOnTaskFailure tests the OnTaskFailure method
func TestTaskCircuitBreakerOnTaskFailure(t *testing.T) {
logger := NewLogger("debug") // Create a real logger
cb := NewTaskCircuitBreaker(3, time.Minute, logger)
// Test failure doesn't trigger open state before threshold
cb.OnTaskFailure("test-task", errors.New("test error"))
if err := cb.CanCreateTask("test-task"); err != nil {
t.Error("Circuit breaker should allow task creation after 1 failure (threshold: 3)")
}
// Test failure count reaches threshold and opens circuit
cb.OnTaskFailure("test-task", errors.New("test error 2"))
cb.OnTaskFailure("test-task", errors.New("test error 3"))
if err := cb.CanCreateTask("test-task"); err == nil {
t.Error("Circuit breaker should prevent task creation after reaching failure threshold")
}
}
// TestResetGlobalTaskRegistry tests the reset functionality
func TestResetGlobalTaskRegistry(t *testing.T) {
globalRegistryMutex.Lock()
defer globalRegistryMutex.Unlock()
// Get the global registry first
registry := GetGlobalTaskRegistry()
// Create and register a dummy task
logger := NewLogger("debug")
task := NewBackgroundTask("test-task", time.Second, func() {
// Do nothing
}, logger)
registry.RegisterTask("test-task", task)
// Verify task is registered
if registry.GetTaskCount() == 0 {
t.Error("Expected task to be registered")
}
// Reset the registry
ResetGlobalTaskRegistry()
// Get registry again and verify it's empty
newRegistry := GetGlobalTaskRegistry()
if newRegistry.GetTaskCount() != 0 {
t.Error("Expected registry to be empty after reset")
}
}
// TestGetTask tests the GetTask method
func TestGetTask(t *testing.T) {
globalRegistryMutex.Lock()
defer globalRegistryMutex.Unlock()
// Reset registry to ensure clean state
ResetGlobalTaskRegistry()
registry := GetGlobalTaskRegistry()
// Test getting non-existent task
task, exists := registry.GetTask("non-existent")
if task != nil || exists {
t.Error("Expected nil and false for non-existent task")
}
// Create and register a task
logger := NewLogger("debug")
newTask := NewBackgroundTask("test-task", time.Second, func() {
// Do nothing
}, logger)
registry.RegisterTask("test-task", newTask)
// Test getting existing task
retrievedTask, exists := registry.GetTask("test-task")
if retrievedTask == nil || !exists {
t.Error("Expected to retrieve registered task")
return
}
if retrievedTask.name != "test-task" {
t.Errorf("Expected task name 'test-task', got '%s'", retrievedTask.name)
}
}
// TestNewTaskMemoryMonitor tests the NewTaskMemoryMonitor function
func TestNewTaskMemoryMonitor(t *testing.T) {
// No mutex needed - this doesn't modify global state
logger := NewLogger("debug")
registry := GetGlobalTaskRegistry()
monitor := NewTaskMemoryMonitor(logger, registry)
if monitor == nil {
t.Error("Expected NewTaskMemoryMonitor to return non-nil monitor")
}
}
// TestGetCurrentStats tests the GetCurrentStats method
func TestGetCurrentStats(t *testing.T) {
// Don't hold mutex during background task execution to avoid deadlocks
logger := NewLogger("debug")
registry := GetGlobalTaskRegistry()
monitor := NewTaskMemoryMonitor(logger, registry)
// Start the monitor and let it collect at least one statistic
err := monitor.Start(50 * time.Millisecond)
if err != nil {
t.Fatalf("Failed to start monitor: %v", err)
}
// Ensure monitor is stopped even if test fails
defer func() {
monitor.Stop()
// Give extra time for cleanup
time.Sleep(50 * time.Millisecond)
}()
// Wait a bit for the monitor to collect stats
time.Sleep(150 * time.Millisecond)
stats, err := monitor.GetCurrentStats()
if err != nil {
// If no stats are available yet, that's acceptable for this test
t.Logf("No memory statistics available yet: %v", err)
return
}
// TaskMemoryStats is a struct, not a pointer, so it can't be nil
if stats.Timestamp.IsZero() {
t.Error("Expected GetCurrentStats to return valid timestamp")
}
}
// TestGetStatsHistory tests the GetStatsHistory method
func TestGetStatsHistory(t *testing.T) {
// No mutex needed - this just creates a monitor and checks its initial state
logger := NewLogger("debug")
registry := GetGlobalTaskRegistry()
monitor := NewTaskMemoryMonitor(logger, registry)
history := monitor.GetStatsHistory()
if history == nil {
t.Error("Expected GetStatsHistory to return non-nil history")
}
// A fresh monitor should have empty history
if len(history) != 0 {
t.Logf("History length: %d (may be non-empty due to shared global state)", len(history))
}
}
// TestForceGC tests the ForceGC method
func TestForceGC(t *testing.T) {
// No mutex needed - this doesn't modify global state
logger := NewLogger("debug")
registry := GetGlobalTaskRegistry()
monitor := NewTaskMemoryMonitor(logger, registry)
// This should not panic and should work
monitor.ForceGC()
// No specific verification needed, just ensuring it doesn't crash
}
// TestShutdownAllTasks tests the ShutdownAllTasks function
func TestShutdownAllTasks(t *testing.T) {
// Use a unique task name prefix to avoid conflicts with other tests
taskPrefix := "shutdown-test-"
// Create a temporary clean registry state
func() {
globalRegistryMutex.Lock()
defer globalRegistryMutex.Unlock()
ResetGlobalTaskRegistry()
}()
registry := GetGlobalTaskRegistry()
logger := NewLogger("debug")
// Create some test tasks with unique names
task1 := NewBackgroundTask(taskPrefix+"task1", time.Millisecond, func() {
time.Sleep(100 * time.Millisecond) // Simulate work
}, logger)
task2 := NewBackgroundTask(taskPrefix+"task2", time.Millisecond, func() {
time.Sleep(100 * time.Millisecond) // Simulate work
}, logger)
// Register tasks under mutex protection
func() {
globalRegistryMutex.Lock()
defer globalRegistryMutex.Unlock()
registry.RegisterTask(taskPrefix+"task1", task1)
registry.RegisterTask(taskPrefix+"task2", task2)
}()
// Start the tasks (outside mutex to avoid deadlock)
task1.Start()
task2.Start()
// Give tasks time to start
time.Sleep(50 * time.Millisecond)
// Shutdown all tasks
ShutdownAllTasks()
// Give shutdown time to complete
time.Sleep(200 * time.Millisecond)
// Note: We can't reliably verify task count due to other tests
// Just ensure shutdown doesn't panic
}
+758
View File
@@ -2,6 +2,10 @@
package config
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
@@ -248,3 +252,757 @@ func TestConstants(t *testing.T) {
t.Errorf("Expected ConstSessionTimeout 86400, got %d", ConstSessionTimeout)
}
}
func TestCreateDefaultSecurityConfig(t *testing.T) {
config := createDefaultSecurityConfig()
if config == nil {
t.Fatal("createDefaultSecurityConfig() returned nil")
}
// Test default values
if !config.Enabled {
t.Error("Expected Enabled to be true")
}
if config.Profile != "default" {
t.Errorf("Expected Profile 'default', got '%s'", config.Profile)
}
if !config.StrictTransportSecurity {
t.Error("Expected StrictTransportSecurity to be true")
}
if config.StrictTransportSecurityMaxAge != 31536000 {
t.Errorf("Expected StrictTransportSecurityMaxAge 31536000, got %d", config.StrictTransportSecurityMaxAge)
}
if config.FrameOptions != "DENY" {
t.Errorf("Expected FrameOptions 'DENY', got '%s'", config.FrameOptions)
}
if config.ContentTypeOptions != "nosniff" {
t.Errorf("Expected ContentTypeOptions 'nosniff', got '%s'", config.ContentTypeOptions)
}
if config.XSSProtection != "1; mode=block" {
t.Errorf("Expected XSSProtection '1; mode=block', got '%s'", config.XSSProtection)
}
if config.CORSEnabled {
t.Error("Expected CORSEnabled to be false")
}
if !config.DisableServerHeader {
t.Error("Expected DisableServerHeader to be true")
}
}
func TestToInternalSecurityConfig(t *testing.T) {
tests := []struct {
name string
config *SecurityHeadersConfig
expected map[string]interface{}
}{
{
name: "nil config",
config: nil,
expected: nil,
},
{
name: "disabled config",
config: &SecurityHeadersConfig{
Enabled: false,
},
expected: nil,
},
{
name: "default profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "default",
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';",
"FrameOptions": "DENY",
},
},
{
name: "strict profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "strict",
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';",
},
},
{
name: "development profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "development",
},
expected: map[string]interface{}{
"DevelopmentMode": true,
"FrameOptions": "SAMEORIGIN",
},
},
{
name: "api profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "api",
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'none'; frame-ancestors 'none';",
"FrameOptions": "DENY",
},
},
{
name: "custom config with overrides",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "custom",
ContentSecurityPolicy: "custom-csp",
FrameOptions: "SAMEORIGIN",
StrictTransportSecurity: true,
StrictTransportSecurityMaxAge: 86400,
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "custom-csp",
"FrameOptions": "SAMEORIGIN",
"StrictTransportSecurityMaxAge": 86400,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.ToInternalSecurityConfig()
if tt.expected == nil {
if result != nil {
t.Errorf("Expected nil result, got %v", result)
}
return
}
if result == nil {
t.Fatal("Expected non-nil result")
}
configMap, ok := result.(map[string]interface{})
if !ok {
t.Fatalf("Expected map[string]interface{}, got %T", result)
}
// Check a few key values
for key, expectedValue := range tt.expected {
if actualValue, exists := configMap[key]; !exists {
t.Errorf("Expected key '%s' not found", key)
} else if actualValue != expectedValue {
t.Errorf("For key '%s': expected %v, got %v", key, expectedValue, actualValue)
}
}
})
}
}
func TestGetSecurityHeadersApplier(t *testing.T) {
tests := []struct {
name string
config *Config
expected bool // whether applier should be nil
}{
{
name: "nil security headers",
config: &Config{
SecurityHeaders: nil,
},
expected: true, // applier should be nil
},
{
name: "disabled security headers",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: false,
},
},
expected: true, // applier should be nil
},
{
name: "enabled security headers",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
},
},
expected: false, // applier should not be nil
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
applier := tt.config.GetSecurityHeadersApplier()
if tt.expected && applier != nil {
t.Error("Expected applier to be nil")
}
if !tt.expected && applier == nil {
t.Error("Expected applier to not be nil")
}
})
}
}
func TestIsOriginAllowed(t *testing.T) {
tests := []struct {
name string
origin string
allowedOrigins []string
expected bool
}{
{
name: "exact match",
origin: "https://example.com",
allowedOrigins: []string{"https://example.com", "https://other.com"},
expected: true,
},
{
name: "wildcard match",
origin: "https://test.example.com",
allowedOrigins: []string{"https://*.example.com"},
expected: true,
},
{
name: "root domain match with wildcard",
origin: "https://example.com",
allowedOrigins: []string{"https://*.example.com"},
expected: true,
},
{
name: "http wildcard match",
origin: "http://test.example.com",
allowedOrigins: []string{"http://*.example.com"},
expected: true,
},
{
name: "catch-all wildcard",
origin: "https://anything.com",
allowedOrigins: []string{"*"},
expected: true,
},
{
name: "no match",
origin: "https://notallowed.com",
allowedOrigins: []string{"https://example.com"},
expected: false,
},
{
name: "empty allowed origins",
origin: "https://example.com",
allowedOrigins: []string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isOriginAllowed(tt.origin, tt.allowedOrigins)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestSecurityHeadersConfigValidation(t *testing.T) {
tests := []struct {
name string
config *SecurityHeadersConfig
valid bool
}{
{
name: "valid default config",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "default",
},
valid: true,
},
{
name: "valid strict config",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "strict",
},
valid: true,
},
{
name: "valid development config",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "development",
},
valid: true,
},
{
name: "valid api config",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "api",
},
valid: true,
},
{
name: "valid custom config",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "custom",
ContentSecurityPolicy: "default-src 'self'",
},
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Basic validation - ensure config can be processed
if tt.config == nil && tt.valid {
t.Error("Expected valid config, got nil")
}
// Test ToInternalSecurityConfig doesn't panic
result := tt.config.ToInternalSecurityConfig()
if tt.config.Enabled && result == nil {
t.Error("Expected non-nil result for enabled config")
}
})
}
}
func TestConfigWithSecurityHeaders(t *testing.T) {
config := CreateConfig()
// Test that default config has security headers
if config.SecurityHeaders == nil {
t.Fatal("Expected SecurityHeaders to be initialized")
}
if !config.SecurityHeaders.Enabled {
t.Error("Expected SecurityHeaders to be enabled by default")
}
// Test security headers applier
applier := config.GetSecurityHeadersApplier()
if applier == nil {
t.Error("Expected security headers applier to be non-nil")
}
// Test with custom security config
config.SecurityHeaders = &SecurityHeadersConfig{
Enabled: true,
Profile: "strict",
ContentSecurityPolicy: "default-src 'self'",
FrameOptions: "DENY",
StrictTransportSecurity: true,
StrictTransportSecurityMaxAge: 31536000,
CORSEnabled: false,
CustomHeaders: map[string]string{"X-Custom": "value"},
}
applier = config.GetSecurityHeadersApplier()
if applier == nil {
t.Error("Expected custom security headers applier to be non-nil")
}
}
func TestConfigEdgeCases(t *testing.T) {
// Test config with empty values
config := &Config{
ProviderURL: "",
ClientID: "",
ClientSecret: "",
LogLevel: "",
Scopes: []string{},
Headers: []HeaderConfig{},
}
if config.LogLevel != "" {
t.Errorf("Expected empty LogLevel, got '%s'", config.LogLevel)
}
if len(config.Scopes) != 0 {
t.Errorf("Expected empty Scopes, got %d", len(config.Scopes))
}
// Test config with nil slices
config = &Config{
Scopes: nil,
Headers: nil,
}
if len(config.Scopes) != 0 {
t.Errorf("Expected empty Scopes, got %v", config.Scopes)
}
}
func TestSecurityHeadersApplierComprehensive(t *testing.T) {
tests := []struct {
name string
config *Config
setup func(*http.Request) *http.Request
check func(*testing.T, http.Header)
}{
{
name: "All security headers with HTTPS",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
FrameOptions: "SAMEORIGIN",
ContentTypeOptions: "nosniff",
XSSProtection: "1; mode=block",
ReferrerPolicy: "strict-origin-when-cross-origin",
ContentSecurityPolicy: "default-src 'self'",
StrictTransportSecurity: true,
StrictTransportSecurityMaxAge: 31536000,
StrictTransportSecuritySubdomains: true,
StrictTransportSecurityPreload: true,
CORSEnabled: true,
CORSAllowedOrigins: []string{"https://example.com"},
CORSAllowedMethods: []string{"GET", "POST"},
CORSAllowedHeaders: []string{"Authorization", "Content-Type"},
CORSAllowCredentials: true,
CORSMaxAge: 86400,
CustomHeaders: map[string]string{"X-Custom": "value"},
DisableServerHeader: true,
DisablePoweredByHeader: true,
},
},
setup: func(req *http.Request) *http.Request {
req.Header.Set("Origin", "https://example.com")
req.Header.Set("X-Forwarded-Proto", "https")
return req
},
check: func(t *testing.T, headers http.Header) {
expectedHeaders := map[string]string{
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'self'",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "86400",
"X-Custom": "value",
}
for key, expected := range expectedHeaders {
if actual := headers.Get(key); actual != expected {
t.Errorf("Expected header %s: '%s', got '%s'", key, expected, actual)
}
}
},
},
{
name: "CORS with wildcard origin",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
CORSEnabled: true,
CORSAllowedOrigins: []string{"*"},
},
},
setup: func(req *http.Request) *http.Request {
req.Header.Set("Origin", "https://anywhere.com")
return req
},
check: func(t *testing.T, headers http.Header) {
if origin := headers.Get("Access-Control-Allow-Origin"); origin != "https://anywhere.com" {
t.Errorf("Expected CORS origin 'https://anywhere.com', got '%s'", origin)
}
},
},
{
name: "HSTS with TLS",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
StrictTransportSecurity: true,
StrictTransportSecurityMaxAge: 63072000,
StrictTransportSecurityPreload: false,
},
},
setup: func(req *http.Request) *http.Request {
// Simulate TLS request
req.TLS = &tls.ConnectionState{}
return req
},
check: func(t *testing.T, headers http.Header) {
hsts := headers.Get("Strict-Transport-Security")
expected := "max-age=63072000"
if hsts != expected {
t.Errorf("Expected HSTS '%s', got '%s'", expected, hsts)
}
},
},
{
name: "Disabled security headers",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: false,
},
},
setup: func(req *http.Request) *http.Request {
return req
},
check: func(t *testing.T, headers http.Header) {
// Since applier should be nil, this won't be called
// but we include it for completeness
},
},
{
name: "Remove server headers",
config: &Config{
SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
DisableServerHeader: true,
DisablePoweredByHeader: true,
},
},
setup: func(req *http.Request) *http.Request {
return req
},
check: func(t *testing.T, headers http.Header) {
// Headers should be explicitly deleted
// We can't easily test deletion, but we ensure they're not set
if server := headers.Get("Server"); server != "" {
t.Errorf("Expected Server header to be removed, got '%s'", server)
}
if powered := headers.Get("X-Powered-By"); powered != "" {
t.Errorf("Expected X-Powered-By header to be removed, got '%s'", powered)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
applier := tt.config.GetSecurityHeadersApplier()
if !tt.config.SecurityHeaders.Enabled {
if applier != nil {
t.Error("Expected nil applier for disabled security headers")
}
return
}
if applier == nil {
t.Fatal("Expected non-nil applier for enabled security headers")
}
req := httptest.NewRequest("GET", "https://example.com/test", nil)
req = tt.setup(req)
rw := httptest.NewRecorder()
// Pre-set some headers that should be removed
rw.Header().Set("Server", "nginx/1.0")
rw.Header().Set("X-Powered-By", "Express")
applier(rw, req)
tt.check(t, rw.Header())
})
}
}
func TestToInternalSecurityConfigComprehensive(t *testing.T) {
tests := []struct {
name string
config *SecurityHeadersConfig
expected map[string]interface{}
}{
{
name: "Nil config",
config: nil,
expected: nil,
},
{
name: "Disabled config",
config: &SecurityHeadersConfig{
Enabled: false,
},
expected: nil,
},
{
name: "Custom profile with all options",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "custom",
ContentSecurityPolicy: "default-src 'none'",
FrameOptions: "ALLOW-FROM https://example.com",
ContentTypeOptions: "nosniff",
XSSProtection: "0",
ReferrerPolicy: "no-referrer",
PermissionsPolicy: "camera=(), microphone=()",
CrossOriginEmbedderPolicy: "require-corp",
CrossOriginOpenerPolicy: "same-origin",
CrossOriginResourcePolicy: "cross-origin",
StrictTransportSecurity: true,
StrictTransportSecurityMaxAge: 15552000,
StrictTransportSecuritySubdomains: false,
StrictTransportSecurityPreload: true,
CORSEnabled: true,
CORSAllowedOrigins: []string{"https://api.example.com"},
CORSAllowedMethods: []string{"PUT", "DELETE"},
CORSAllowedHeaders: []string{"X-API-Key"},
CORSAllowCredentials: false,
CORSMaxAge: 3600,
CustomHeaders: map[string]string{"X-API-Version": "v1"},
DisableServerHeader: true,
DisablePoweredByHeader: false,
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'none'",
"FrameOptions": "ALLOW-FROM https://example.com",
"ContentTypeOptions": "nosniff",
"XSSProtection": "0",
"ReferrerPolicy": "no-referrer",
"PermissionsPolicy": "camera=(), microphone=()",
"CrossOriginEmbedderPolicy": "require-corp",
"CrossOriginOpenerPolicy": "same-origin",
"CrossOriginResourcePolicy": "cross-origin",
"StrictTransportSecurityMaxAge": 15552000,
"StrictTransportSecuritySubdomains": false,
"StrictTransportSecurityPreload": true,
"CORSEnabled": true,
"CORSAllowedOrigins": []string{"https://api.example.com"},
"CORSAllowedMethods": []string{"PUT", "DELETE"},
"CORSAllowedHeaders": []string{"X-API-Key"},
"CORSAllowCredentials": false,
"CORSMaxAge": 3600,
"CustomHeaders": map[string]string{"X-API-Version": "v1"},
"DisableServerHeader": true,
"DisablePoweredByHeader": false,
},
},
{
name: "Development profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "development",
},
expected: map[string]interface{}{
"DevelopmentMode": true,
"ContentSecurityPolicy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https: http:; connect-src 'self' ws: wss:;",
"FrameOptions": "SAMEORIGIN",
"ContentTypeOptions": "nosniff",
"XSSProtection": "1; mode=block",
"ReferrerPolicy": "strict-origin-when-cross-origin",
"CrossOriginOpenerPolicy": "unsafe-none",
"CrossOriginResourcePolicy": "cross-origin",
"CORSEnabled": false,
"CORSAllowCredentials": false,
"DisableServerHeader": false,
"DisablePoweredByHeader": false,
},
},
{
name: "API profile",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "api",
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'none'; frame-ancestors 'none';",
"FrameOptions": "DENY",
"ContentTypeOptions": "nosniff",
"XSSProtection": "1; mode=block",
"ReferrerPolicy": "strict-origin-when-cross-origin",
"CrossOriginResourcePolicy": "cross-origin",
"CORSEnabled": false,
"CORSAllowCredentials": false,
"DisableServerHeader": false,
"DisablePoweredByHeader": false,
},
},
{
name: "Partial configuration",
config: &SecurityHeadersConfig{
Enabled: true,
Profile: "default",
FrameOptions: "SAMEORIGIN", // Override default
CORSEnabled: true, // Enable CORS
},
expected: map[string]interface{}{
"DevelopmentMode": false,
"ContentSecurityPolicy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';",
"FrameOptions": "SAMEORIGIN", // Overridden
"ContentTypeOptions": "nosniff",
"XSSProtection": "1; mode=block",
"ReferrerPolicy": "strict-origin-when-cross-origin",
"PermissionsPolicy": "geolocation=(), microphone=(), camera=(), payment=(), usb=()",
"CrossOriginEmbedderPolicy": "require-corp",
"CrossOriginOpenerPolicy": "same-origin",
"CrossOriginResourcePolicy": "same-origin",
"CORSEnabled": true, // Explicitly set
"CORSAllowCredentials": false,
"DisableServerHeader": false,
"DisablePoweredByHeader": false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.ToInternalSecurityConfig()
if tt.expected == nil {
if result != nil {
t.Errorf("Expected nil result, got %+v", result)
}
return
}
if result == nil {
t.Fatal("Expected non-nil result")
}
resultMap, ok := result.(map[string]interface{})
if !ok {
t.Errorf("Expected result to be map[string]interface{}, got %T", result)
return
}
for key, expectedValue := range tt.expected {
actualValue, exists := resultMap[key]
if !exists {
t.Errorf("Expected key '%s' not found in result", key)
continue
}
if !reflect.DeepEqual(actualValue, expectedValue) {
t.Errorf("For key '%s': expected %v (%T), got %v (%T)",
key, expectedValue, expectedValue, actualValue, actualValue)
}
}
// Check that no unexpected keys are present
for key := range resultMap {
if _, expected := tt.expected[key]; !expected {
t.Errorf("Unexpected key '%s' found in result with value %v", key, resultMap[key])
}
}
})
}
}
+529
View File
@@ -0,0 +1,529 @@
package errors
import (
"errors"
"net/http"
"reflect"
"testing"
)
func TestOIDCError_Error(t *testing.T) {
tests := []struct {
name string
oidcErr *OIDCError
expected string
}{
{
name: "Error with details",
oidcErr: &OIDCError{
Code: ErrCodeTokenInvalid,
Message: "Token validation failed",
Details: "JWT signature invalid",
},
expected: "TOKEN_INVALID: Token validation failed (JWT signature invalid)",
},
{
name: "Error without details",
oidcErr: &OIDCError{
Code: ErrCodeAuthenticationFailed,
Message: "Authentication failed",
},
expected: "AUTH_FAILED: Authentication failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.oidcErr.Error()
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestOIDCError_Unwrap(t *testing.T) {
internalErr := errors.New("internal error")
oidcErr := &OIDCError{
Code: ErrCodeTokenInvalid,
Message: "Token validation failed",
Internal: internalErr,
}
unwrapped := oidcErr.Unwrap()
if unwrapped != internalErr {
t.Errorf("Expected internal error, got %v", unwrapped)
}
// Test with nil internal error
oidcErrNoInternal := &OIDCError{
Code: ErrCodeTokenInvalid,
Message: "Token validation failed",
}
unwrappedNil := oidcErrNoInternal.Unwrap()
if unwrappedNil != nil {
t.Errorf("Expected nil, got %v", unwrappedNil)
}
}
func TestOIDCError_IsRetryable(t *testing.T) {
tests := []struct {
name string
code ErrorCode
expected bool
}{
{"Network timeout", ErrCodeNetworkTimeout, true},
{"Service unavailable", ErrCodeServiceUnavailable, true},
{"Provider unreachable", ErrCodeProviderUnreachable, true},
{"Authentication failed", ErrCodeAuthenticationFailed, false},
{"Token invalid", ErrCodeTokenInvalid, false},
{"Rate limited", ErrCodeRateLimited, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oidcErr := &OIDCError{Code: tt.code}
result := oidcErr.IsRetryable()
if result != tt.expected {
t.Errorf("Expected %v, got %v for code %s", tt.expected, result, tt.code)
}
})
}
}
func TestOIDCError_IsAuthenticationError(t *testing.T) {
tests := []struct {
name string
code ErrorCode
expected bool
}{
{"Authentication failed", ErrCodeAuthenticationFailed, true},
{"Token expired", ErrCodeTokenExpired, true},
{"Token invalid", ErrCodeTokenInvalid, true},
{"Session expired", ErrCodeSessionExpired, true},
{"CSRF mismatch", ErrCodeCSRFMismatch, true},
{"Nonce mismatch", ErrCodeNonceMismatch, true},
{"Config invalid", ErrCodeConfigInvalid, false},
{"Domain not allowed", ErrCodeDomainNotAllowed, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oidcErr := &OIDCError{Code: tt.code}
result := oidcErr.IsAuthenticationError()
if result != tt.expected {
t.Errorf("Expected %v, got %v for code %s", tt.expected, result, tt.code)
}
})
}
}
func TestOIDCError_IsAuthorizationError(t *testing.T) {
tests := []struct {
name string
code ErrorCode
expected bool
}{
{"Domain not allowed", ErrCodeDomainNotAllowed, true},
{"User not allowed", ErrCodeUserNotAllowed, true},
{"Role not allowed", ErrCodeRoleNotAllowed, true},
{"Authentication failed", ErrCodeAuthenticationFailed, false},
{"Token expired", ErrCodeTokenExpired, false},
{"Config invalid", ErrCodeConfigInvalid, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oidcErr := &OIDCError{Code: tt.code}
result := oidcErr.IsAuthorizationError()
if result != tt.expected {
t.Errorf("Expected %v, got %v for code %s", tt.expected, result, tt.code)
}
})
}
}
func TestOIDCError_ToJSON(t *testing.T) {
tests := []struct {
name string
oidcErr *OIDCError
expected map[string]any
}{
{
name: "Error with details",
oidcErr: &OIDCError{
Code: ErrCodeTokenInvalid,
Message: "Token validation failed",
Details: "JWT signature invalid",
},
expected: map[string]any{
"error": map[string]any{
"code": "TOKEN_INVALID",
"message": "Token validation failed",
"details": "JWT signature invalid",
},
},
},
{
name: "Error without details",
oidcErr: &OIDCError{
Code: ErrCodeAuthenticationFailed,
Message: "Authentication failed",
},
expected: map[string]any{
"error": map[string]any{
"code": "AUTH_FAILED",
"message": "Authentication failed",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.oidcErr.ToJSON()
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("Expected %+v, got %+v", tt.expected, result)
}
})
}
}
func TestNewAuthenticationError(t *testing.T) {
internalErr := errors.New("internal error")
tests := []struct {
name string
code ErrorCode
message string
internal error
expectedHTTP int
}{
{
name: "Regular auth error",
code: ErrCodeAuthenticationFailed,
message: "Auth failed",
internal: internalErr,
expectedHTTP: http.StatusUnauthorized,
},
{
name: "Session expired error",
code: ErrCodeSessionExpired,
message: "Session expired",
internal: internalErr,
expectedHTTP: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewAuthenticationError(tt.code, tt.message, tt.internal)
if err.Code != tt.code {
t.Errorf("Expected code %s, got %s", tt.code, err.Code)
}
if err.Message != tt.message {
t.Errorf("Expected message '%s', got '%s'", tt.message, err.Message)
}
if err.Internal != tt.internal {
t.Errorf("Expected internal error %v, got %v", tt.internal, err.Internal)
}
if err.HTTPStatus != tt.expectedHTTP {
t.Errorf("Expected HTTP status %d, got %d", tt.expectedHTTP, err.HTTPStatus)
}
})
}
}
func TestNewAuthorizationError(t *testing.T) {
err := NewAuthorizationError(ErrCodeDomainNotAllowed, "Domain not allowed", "example.com not in whitelist")
if err.Code != ErrCodeDomainNotAllowed {
t.Errorf("Expected code %s, got %s", ErrCodeDomainNotAllowed, err.Code)
}
if err.Message != "Domain not allowed" {
t.Errorf("Expected message 'Domain not allowed', got '%s'", err.Message)
}
if err.Details != "example.com not in whitelist" {
t.Errorf("Expected details 'example.com not in whitelist', got '%s'", err.Details)
}
if err.HTTPStatus != http.StatusForbidden {
t.Errorf("Expected HTTP status %d, got %d", http.StatusForbidden, err.HTTPStatus)
}
}
func TestNewConfigurationError(t *testing.T) {
internalErr := errors.New("config parse error")
err := NewConfigurationError(ErrCodeConfigInvalid, "Invalid config", internalErr)
if err.Code != ErrCodeConfigInvalid {
t.Errorf("Expected code %s, got %s", ErrCodeConfigInvalid, err.Code)
}
if err.HTTPStatus != http.StatusInternalServerError {
t.Errorf("Expected HTTP status %d, got %d", http.StatusInternalServerError, err.HTTPStatus)
}
if err.Internal != internalErr {
t.Errorf("Expected internal error %v, got %v", internalErr, err.Internal)
}
}
func TestNewNetworkError(t *testing.T) {
internalErr := errors.New("network error")
tests := []struct {
name string
code ErrorCode
expectedHTTP int
}{
{
name: "Rate limited",
code: ErrCodeRateLimited,
expectedHTTP: http.StatusTooManyRequests,
},
{
name: "Service unavailable",
code: ErrCodeServiceUnavailable,
expectedHTTP: http.StatusServiceUnavailable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewNetworkError(tt.code, "Network error", internalErr)
if err.Code != tt.code {
t.Errorf("Expected code %s, got %s", tt.code, err.Code)
}
if err.HTTPStatus != tt.expectedHTTP {
t.Errorf("Expected HTTP status %d, got %d", tt.expectedHTTP, err.HTTPStatus)
}
})
}
}
func TestNewValidationError(t *testing.T) {
err := NewValidationError(ErrCodeValidationFailed, "Validation failed", "field 'email' is required")
if err.Code != ErrCodeValidationFailed {
t.Errorf("Expected code %s, got %s", ErrCodeValidationFailed, err.Code)
}
if err.HTTPStatus != http.StatusBadRequest {
t.Errorf("Expected HTTP status %d, got %d", http.StatusBadRequest, err.HTTPStatus)
}
if err.Details != "field 'email' is required" {
t.Errorf("Expected details 'field 'email' is required', got '%s'", err.Details)
}
}
func TestWrapAuthenticationError(t *testing.T) {
internalErr := errors.New("original error")
err := WrapAuthenticationError(internalErr, "Custom auth message")
if err.Code != ErrCodeAuthenticationFailed {
t.Errorf("Expected code %s, got %s", ErrCodeAuthenticationFailed, err.Code)
}
if err.Message != "Custom auth message" {
t.Errorf("Expected message 'Custom auth message', got '%s'", err.Message)
}
if err.Internal != internalErr {
t.Errorf("Expected internal error %v, got %v", internalErr, err.Internal)
}
}
func TestWrapTokenError(t *testing.T) {
internalErr := errors.New("token error")
err := WrapTokenError(internalErr, "ID token")
if err.Code != ErrCodeTokenInvalid {
t.Errorf("Expected code %s, got %s", ErrCodeTokenInvalid, err.Code)
}
if err.Message != "Token validation failed: ID token" {
t.Errorf("Expected message 'Token validation failed: ID token', got '%s'", err.Message)
}
if err.Internal != internalErr {
t.Errorf("Expected internal error %v, got %v", internalErr, err.Internal)
}
}
func TestWrapProviderError(t *testing.T) {
internalErr := errors.New("provider error")
err := WrapProviderError(internalErr, "https://provider.example.com")
if err.Code != ErrCodeProviderUnreachable {
t.Errorf("Expected code %s, got %s", ErrCodeProviderUnreachable, err.Code)
}
if err.Message != "Provider communication failed: https://provider.example.com" {
t.Errorf("Expected specific message, got '%s'", err.Message)
}
if err.Internal != internalErr {
t.Errorf("Expected internal error %v, got %v", internalErr, err.Internal)
}
}
func TestIsOIDCError(t *testing.T) {
// Test with OIDCError
oidcErr := &OIDCError{Code: ErrCodeTokenInvalid, Message: "test"}
result, ok := IsOIDCError(oidcErr)
if !ok {
t.Error("Expected IsOIDCError to return true for OIDCError")
}
if result != oidcErr {
t.Error("Expected to get the same OIDCError back")
}
// Test with regular error
regularErr := errors.New("regular error")
result, ok = IsOIDCError(regularErr)
if ok {
t.Error("Expected IsOIDCError to return false for regular error")
}
if result != nil {
t.Error("Expected nil result for regular error")
}
}
func TestGetHTTPStatus(t *testing.T) {
// Test with OIDCError
oidcErr := &OIDCError{
Code: ErrCodeTokenInvalid,
HTTPStatus: http.StatusUnauthorized,
}
status := GetHTTPStatus(oidcErr)
if status != http.StatusUnauthorized {
t.Errorf("Expected %d, got %d", http.StatusUnauthorized, status)
}
// Test with regular error
regularErr := errors.New("regular error")
status = GetHTTPStatus(regularErr)
if status != http.StatusInternalServerError {
t.Errorf("Expected %d, got %d", http.StatusInternalServerError, status)
}
}
func TestFormatUserMessage(t *testing.T) {
tests := []struct {
name string
err error
expected string
}{
{
name: "Domain not allowed",
err: &OIDCError{Code: ErrCodeDomainNotAllowed},
expected: "Your email domain is not authorized for this application",
},
{
name: "User not allowed",
err: &OIDCError{Code: ErrCodeUserNotAllowed},
expected: "Your account is not authorized for this application",
},
{
name: "Role not allowed",
err: &OIDCError{Code: ErrCodeRoleNotAllowed},
expected: "You do not have the required permissions for this application",
},
{
name: "Session expired",
err: &OIDCError{Code: ErrCodeSessionExpired},
expected: "Your session has expired. Please log in again",
},
{
name: "Token expired",
err: &OIDCError{Code: ErrCodeTokenExpired},
expected: "Your authentication has expired. Please log in again",
},
{
name: "Provider unreachable",
err: &OIDCError{Code: ErrCodeProviderUnreachable},
expected: "Authentication service is temporarily unavailable. Please try again later",
},
{
name: "Rate limited",
err: &OIDCError{Code: ErrCodeRateLimited},
expected: "Too many requests. Please wait a moment and try again",
},
{
name: "Unknown OIDC error",
err: &OIDCError{Code: ErrCodeConfigInvalid},
expected: "Authentication failed. Please try again",
},
{
name: "Regular error",
err: errors.New("regular error"),
expected: "An unexpected error occurred. Please try again",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FormatUserMessage(tt.err)
if result != tt.expected {
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
}
})
}
}
func TestErrorCodes(t *testing.T) {
// Test that all error codes are defined correctly
codes := []ErrorCode{
ErrCodeAuthenticationFailed,
ErrCodeTokenExpired,
ErrCodeTokenInvalid,
ErrCodeSessionExpired,
ErrCodeCSRFMismatch,
ErrCodeNonceMismatch,
ErrCodeConfigInvalid,
ErrCodeProviderUnreachable,
ErrCodeMetadataFailed,
ErrCodeNetworkTimeout,
ErrCodeRateLimited,
ErrCodeServiceUnavailable,
ErrCodeValidationFailed,
ErrCodeDomainNotAllowed,
ErrCodeUserNotAllowed,
ErrCodeRoleNotAllowed,
}
for _, code := range codes {
if string(code) == "" {
t.Errorf("Error code %v is empty", code)
}
}
}
func TestErrorConstructorCompleteness(t *testing.T) {
// Test each constructor function to ensure they set all required fields
internalErr := errors.New("test error")
// Test NewAuthenticationError
authErr := NewAuthenticationError(ErrCodeAuthenticationFailed, "auth message", internalErr)
if authErr.Code == "" || authErr.Message == "" || authErr.HTTPStatus == 0 {
t.Error("NewAuthenticationError did not set all required fields")
}
// Test NewAuthorizationError
authzErr := NewAuthorizationError(ErrCodeDomainNotAllowed, "authz message", "details")
if authzErr.Code == "" || authzErr.Message == "" || authzErr.HTTPStatus == 0 {
t.Error("NewAuthorizationError did not set all required fields")
}
// Test NewConfigurationError
configErr := NewConfigurationError(ErrCodeConfigInvalid, "config message", internalErr)
if configErr.Code == "" || configErr.Message == "" || configErr.HTTPStatus == 0 {
t.Error("NewConfigurationError did not set all required fields")
}
// Test NewNetworkError
netErr := NewNetworkError(ErrCodeNetworkTimeout, "network message", internalErr)
if netErr.Code == "" || netErr.Message == "" || netErr.HTTPStatus == 0 {
t.Error("NewNetworkError did not set all required fields")
}
// Test NewValidationError
validErr := NewValidationError(ErrCodeValidationFailed, "validation message", "details")
if validErr.Code == "" || validErr.Message == "" || validErr.HTTPStatus == 0 {
t.Error("NewValidationError did not set all required fields")
}
}
+588
View File
@@ -0,0 +1,588 @@
package handlers
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// Mock implementations that embed SessionHandler
type MockSessionHandlerWrapper struct {
*SessionHandler
}
func NewMockSessionHandlerWrapper() *MockSessionHandlerWrapper {
sessionManager := &MockSessionManager{}
logger := &MockLogger{}
sessionHandler := NewSessionHandler(
sessionManager,
logger,
"/logout",
"https://example.com/post-logout",
"https://provider.example.com/logout",
"test-client-id",
)
return &MockSessionHandlerWrapper{
SessionHandler: sessionHandler,
}
}
type MockSessionManager struct {
session Session
err error
}
func (m *MockSessionManager) GetSession(req *http.Request) (Session, error) {
return m.session, m.err
}
func (m *MockSessionManager) CleanupOldCookies(rw http.ResponseWriter, req *http.Request) {
// Mock implementation
}
type MockSession struct {
authenticated bool
email string
idToken string
accessToken string
refreshToken string
saveError error
clearError error
}
func (m *MockSession) GetAuthenticated() bool { return m.authenticated }
func (m *MockSession) SetAuthenticated(auth bool) error { m.authenticated = auth; return nil }
func (m *MockSession) GetEmail() string { return m.email }
func (m *MockSession) SetEmail(email string) { m.email = email }
func (m *MockSession) GetIDToken() string { return m.idToken }
func (m *MockSession) GetAccessToken() string { return m.accessToken }
func (m *MockSession) GetRefreshToken() string { return m.refreshToken }
func (m *MockSession) SetRefreshToken(token string) { m.refreshToken = token }
func (m *MockSession) Clear(req *http.Request, rw http.ResponseWriter) error { return m.clearError }
func (m *MockSession) Save(req *http.Request, rw http.ResponseWriter) error { return m.saveError }
func (m *MockSession) ReturnToPoolSafely() {}
type MockTokenHandler struct {
verifyError error
refreshError error
tokenResponse *TokenResponse
}
func (m *MockTokenHandler) VerifyToken(token string) error {
return m.verifyError
}
func (m *MockTokenHandler) RefreshToken(refreshToken string) (*TokenResponse, error) {
return m.tokenResponse, m.refreshError
}
type MockLogger struct {
debugMessages []string
errorMessages []string
}
func (m *MockLogger) Debug(msg string) {
m.debugMessages = append(m.debugMessages, msg)
}
func (m *MockLogger) Debugf(format string, args ...interface{}) {
m.debugMessages = append(m.debugMessages, format)
}
func (m *MockLogger) Info(msg string) {}
func (m *MockLogger) Infof(format string, args ...interface{}) {}
func (m *MockLogger) Error(msg string) {
m.errorMessages = append(m.errorMessages, msg)
}
func (m *MockLogger) Errorf(format string, args ...interface{}) {
m.errorMessages = append(m.errorMessages, format)
}
func TestNewAuthFlowHandler(t *testing.T) {
sessionHandler := NewMockSessionHandlerWrapper()
tokenHandler := &MockTokenHandler{}
logger := &MockLogger{}
excludedURLs := map[string]struct{}{"/health": {}}
initComplete := make(chan struct{})
issuerURL := "https://issuer.example.com"
handler := NewAuthFlowHandler(sessionHandler.SessionHandler, tokenHandler, logger, excludedURLs, initComplete, issuerURL)
if handler == nil {
t.Fatal("NewAuthFlowHandler returned nil")
}
if handler.sessionHandler == nil {
t.Error("SessionHandler not set correctly")
}
if handler.tokenHandler != tokenHandler {
t.Error("TokenHandler not set correctly")
}
if handler.logger != logger {
t.Error("Logger not set correctly")
}
if handler.issuerURL != issuerURL {
t.Error("IssuerURL not set correctly")
}
}
func TestAuthFlowHandler_shouldExcludeURL(t *testing.T) {
excludedURLs := map[string]struct{}{
"/health": {},
"/metrics": {},
"/api/public": {},
}
handler := &AuthFlowHandler{excludedURLs: excludedURLs}
tests := []struct {
path string
expected bool
}{
{"/health", true},
{"/health/check", true},
{"/metrics", true},
{"/metrics/prometheus", true},
{"/api/public", true},
{"/api/public/endpoint", true},
{"/api/private", false},
{"/login", false},
{"/dashboard", false},
}
for _, test := range tests {
result := handler.shouldExcludeURL(test.path)
if result != test.expected {
t.Errorf("For path '%s': expected %v, got %v", test.path, test.expected, result)
}
}
}
func TestAuthFlowHandler_isStreamingRequest(t *testing.T) {
handler := &AuthFlowHandler{}
tests := []struct {
name string
accept string
expected bool
}{
{
name: "SSE request",
accept: "text/event-stream",
expected: true,
},
{
name: "Regular HTML request",
accept: "text/html,application/xhtml+xml",
expected: false,
},
{
name: "JSON request",
accept: "application/json",
expected: false,
},
{
name: "Empty accept header",
accept: "",
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept", test.accept)
result := handler.isStreamingRequest(req)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}
func TestAuthFlowHandler_waitForInitialization(t *testing.T) {
tests := []struct {
name string
setupHandler func() (*AuthFlowHandler, context.CancelFunc)
expectedResult bool
}{
{
name: "Initialization complete successfully",
setupHandler: func() (*AuthFlowHandler, context.CancelFunc) {
initComplete := make(chan struct{})
close(initComplete) // Already complete
handler := &AuthFlowHandler{
initComplete: initComplete,
issuerURL: "https://issuer.example.com",
}
return handler, nil
},
expectedResult: true,
},
{
name: "Initialization complete but no issuer URL",
setupHandler: func() (*AuthFlowHandler, context.CancelFunc) {
initComplete := make(chan struct{})
close(initComplete)
handler := &AuthFlowHandler{
initComplete: initComplete,
issuerURL: "",
logger: &MockLogger{},
}
return handler, nil
},
expectedResult: false,
},
{
name: "Request cancelled",
setupHandler: func() (*AuthFlowHandler, context.CancelFunc) {
initComplete := make(chan struct{})
handler := &AuthFlowHandler{
initComplete: initComplete,
logger: &MockLogger{},
}
_, cancel := context.WithCancel(context.Background())
return handler, cancel
},
expectedResult: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler, cancelFunc := test.setupHandler()
req := httptest.NewRequest("GET", "/", nil)
if cancelFunc != nil {
ctx, cancel := context.WithCancel(context.Background())
req = req.WithContext(ctx)
cancel() // Cancel immediately
}
result := handler.waitForInitialization(req)
if result != test.expectedResult {
t.Errorf("Expected %v, got %v", test.expectedResult, result)
}
})
}
}
func TestAuthFlowHandler_ProcessRequest(t *testing.T) {
tests := []struct {
name string
setupRequest func() *http.Request
setupHandler func() *AuthFlowHandler
expectedResult AuthFlowResult
}{
{
name: "Excluded URL bypasses authentication",
setupRequest: func() *http.Request {
return httptest.NewRequest("GET", "/health", nil)
},
setupHandler: func() *AuthFlowHandler {
return &AuthFlowHandler{
excludedURLs: map[string]struct{}{"/health": {}},
logger: &MockLogger{},
}
},
expectedResult: AuthFlowResult{Authenticated: true},
},
{
name: "Streaming request bypasses authentication",
setupRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/events", nil)
req.Header.Set("Accept", "text/event-stream")
return req
},
setupHandler: func() *AuthFlowHandler {
return &AuthFlowHandler{
excludedURLs: map[string]struct{}{},
logger: &MockLogger{},
}
},
expectedResult: AuthFlowResult{Authenticated: true},
},
{
name: "Initialization timeout",
setupRequest: func() *http.Request {
return httptest.NewRequest("GET", "/dashboard", nil)
},
setupHandler: func() *AuthFlowHandler {
return &AuthFlowHandler{
excludedURLs: map[string]struct{}{},
initComplete: make(chan struct{}), // Never closes
logger: &MockLogger{},
}
},
expectedResult: AuthFlowResult{
Error: ErrInitializationTimeout,
StatusCode: http.StatusServiceUnavailable,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := test.setupRequest()
handler := test.setupHandler()
rw := httptest.NewRecorder()
// For timeout test, use context with timeout
if test.name == "Initialization timeout" {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
req = req.WithContext(ctx)
}
result := handler.ProcessRequest(rw, req)
if result.Authenticated != test.expectedResult.Authenticated {
t.Errorf("Expected Authenticated %v, got %v", test.expectedResult.Authenticated, result.Authenticated)
}
if result.StatusCode != test.expectedResult.StatusCode {
t.Errorf("Expected StatusCode %d, got %d", test.expectedResult.StatusCode, result.StatusCode)
}
if test.expectedResult.Error != nil && result.Error == nil {
t.Error("Expected error but got nil")
}
})
}
}
func TestAuthFlowHandler_validateAndRefreshTokens(t *testing.T) {
tests := []struct {
name string
session *MockSession
tokenHandler *MockTokenHandler
expectedResult AuthFlowResult
}{
{
name: "Valid access token",
session: &MockSession{
authenticated: true,
accessToken: "valid-access-token",
},
tokenHandler: &MockTokenHandler{
verifyError: nil,
},
expectedResult: AuthFlowResult{Authenticated: true},
},
{
name: "Invalid access token, successful refresh",
session: &MockSession{
authenticated: true,
accessToken: "invalid-access-token",
refreshToken: "valid-refresh-token",
},
tokenHandler: &MockTokenHandler{
verifyError: errors.New("token expired"),
refreshError: nil,
tokenResponse: &TokenResponse{
IDToken: "new-id-token",
AccessToken: "new-access-token",
},
},
expectedResult: AuthFlowResult{Authenticated: true},
},
{
name: "Invalid access token, no refresh token",
session: &MockSession{
authenticated: true,
accessToken: "invalid-access-token",
refreshToken: "",
},
tokenHandler: &MockTokenHandler{
verifyError: errors.New("token expired"),
},
expectedResult: AuthFlowResult{RequiresAuth: true},
},
{
name: "Valid ID token only",
session: &MockSession{
authenticated: true,
idToken: "valid-id-token",
},
tokenHandler: &MockTokenHandler{
verifyError: nil,
},
expectedResult: AuthFlowResult{Authenticated: true},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &AuthFlowHandler{
tokenHandler: test.tokenHandler,
logger: &MockLogger{},
}
req := httptest.NewRequest("GET", "/", nil)
rw := httptest.NewRecorder()
result := handler.validateAndRefreshTokens(test.session, req, rw)
if result.Authenticated != test.expectedResult.Authenticated {
t.Errorf("Expected Authenticated %v, got %v", test.expectedResult.Authenticated, result.Authenticated)
}
if result.RequiresAuth != test.expectedResult.RequiresAuth {
t.Errorf("Expected RequiresAuth %v, got %v", test.expectedResult.RequiresAuth, result.RequiresAuth)
}
})
}
}
func TestAuthFlowHandler_attemptTokenRefresh(t *testing.T) {
tests := []struct {
name string
session *MockSession
tokenHandler *MockTokenHandler
isAjax bool
expectedResult AuthFlowResult
}{
{
name: "No refresh token",
session: &MockSession{
refreshToken: "",
},
tokenHandler: &MockTokenHandler{},
expectedResult: AuthFlowResult{RequiresAuth: true},
},
{
name: "AJAX request with expired session",
session: &MockSession{
refreshToken: "refresh-token",
},
tokenHandler: &MockTokenHandler{},
isAjax: true,
expectedResult: AuthFlowResult{
Error: ErrSessionExpiredAjax,
StatusCode: http.StatusUnauthorized,
},
},
{
name: "Successful token refresh",
session: &MockSession{
refreshToken: "valid-refresh-token",
},
tokenHandler: &MockTokenHandler{
refreshError: nil,
tokenResponse: &TokenResponse{
IDToken: "new-id-token",
AccessToken: "new-access-token",
},
},
expectedResult: AuthFlowResult{Authenticated: true},
},
{
name: "Failed token refresh",
session: &MockSession{
refreshToken: "invalid-refresh-token",
},
tokenHandler: &MockTokenHandler{
refreshError: errors.New("refresh failed"),
},
expectedResult: AuthFlowResult{RequiresAuth: true},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sessionHandlerWrapper := NewMockSessionHandlerWrapper()
handler := &AuthFlowHandler{
sessionHandler: sessionHandlerWrapper.SessionHandler,
tokenHandler: test.tokenHandler,
logger: &MockLogger{},
}
req := httptest.NewRequest("GET", "/", nil)
if test.isAjax {
req.Header.Set("X-Requested-With", "XMLHttpRequest")
}
rw := httptest.NewRecorder()
result := handler.attemptTokenRefresh(test.session, req, rw)
if result.Authenticated != test.expectedResult.Authenticated {
t.Errorf("Expected Authenticated %v, got %v", test.expectedResult.Authenticated, result.Authenticated)
}
if result.RequiresAuth != test.expectedResult.RequiresAuth {
t.Errorf("Expected RequiresAuth %v, got %v", test.expectedResult.RequiresAuth, result.RequiresAuth)
}
if result.StatusCode != test.expectedResult.StatusCode {
t.Errorf("Expected StatusCode %d, got %d", test.expectedResult.StatusCode, result.StatusCode)
}
})
}
}
func TestAuthFlowError_Error(t *testing.T) {
err := &AuthFlowError{
Code: "TEST_ERROR",
Message: "This is a test error",
}
expected := "This is a test error"
result := err.Error()
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
}
func TestAuthFlowResult(t *testing.T) {
// Test AuthFlowResult struct
result := AuthFlowResult{
Authenticated: true,
RequiresAuth: false,
RequiresRefresh: false,
Error: nil,
RedirectURL: "https://example.com",
StatusCode: 200,
}
if !result.Authenticated {
t.Error("Expected Authenticated to be true")
}
if result.RequiresAuth {
t.Error("Expected RequiresAuth to be false")
}
if result.StatusCode != 200 {
t.Errorf("Expected StatusCode 200, got %d", result.StatusCode)
}
}
func TestTokenResponse(t *testing.T) {
response := &TokenResponse{
IDToken: "id-token-value",
AccessToken: "access-token-value",
RefreshToken: "refresh-token-value",
ExpiresIn: 3600,
}
if response.IDToken != "id-token-value" {
t.Errorf("Expected IDToken 'id-token-value', got '%s'", response.IDToken)
}
if response.ExpiresIn != 3600 {
t.Errorf("Expected ExpiresIn 3600, got %d", response.ExpiresIn)
}
}
+587
View File
@@ -0,0 +1,587 @@
package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestNewSessionHandler(t *testing.T) {
sessionManager := &MockSessionManager{}
logger := &MockLogger{}
logoutURLPath := "/logout"
postLogoutRedirectURI := "https://example.com/post-logout"
endSessionURL := "https://provider.example.com/logout"
clientID := "test-client-id"
handler := NewSessionHandler(
sessionManager,
logger,
logoutURLPath,
postLogoutRedirectURI,
endSessionURL,
clientID,
)
if handler == nil {
t.Fatal("NewSessionHandler returned nil")
}
if handler.sessionManager != sessionManager {
t.Error("SessionManager not set correctly")
}
if handler.logger != logger {
t.Error("Logger not set correctly")
}
if handler.logoutURLPath != logoutURLPath {
t.Error("LogoutURLPath not set correctly")
}
if handler.postLogoutRedirectURI != postLogoutRedirectURI {
t.Error("PostLogoutRedirectURI not set correctly")
}
if handler.endSessionURL != endSessionURL {
t.Error("EndSessionURL not set correctly")
}
if handler.clientID != clientID {
t.Error("ClientID not set correctly")
}
}
func TestSessionHandler_HandleLogout(t *testing.T) {
tests := []struct {
name string
setupSession func() *MockSession
setupManager func() *MockSessionManager
expectedCode int
expectedURL string
}{
{
name: "Successful logout with ID token",
setupSession: func() *MockSession {
return &MockSession{
authenticated: true,
idToken: "test-id-token",
}
},
setupManager: func() *MockSessionManager {
return &MockSessionManager{
session: &MockSession{
authenticated: true,
idToken: "test-id-token",
},
}
},
expectedCode: http.StatusFound,
expectedURL: "https://provider.example.com/logout?id_token_hint=test-id-token&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
{
name: "Logout without ID token",
setupSession: func() *MockSession {
return &MockSession{
authenticated: true,
idToken: "",
}
},
setupManager: func() *MockSessionManager {
return &MockSessionManager{
session: &MockSession{
authenticated: true,
idToken: "",
},
}
},
expectedCode: http.StatusFound,
expectedURL: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
{
name: "Session retrieval error",
setupSession: func() *MockSession { return nil },
setupManager: func() *MockSessionManager {
return &MockSessionManager{
err: fmt.Errorf("session error"),
}
},
expectedCode: http.StatusFound,
expectedURL: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &SessionHandler{
sessionManager: test.setupManager(),
logger: &MockLogger{},
logoutURLPath: "/logout",
postLogoutRedirectURI: "https://example.com/post-logout",
endSessionURL: "https://provider.example.com/logout",
clientID: "test-client-id",
}
req := httptest.NewRequest("POST", "/logout", nil)
rw := httptest.NewRecorder()
handler.HandleLogout(rw, req)
if rw.Code != test.expectedCode {
t.Errorf("Expected status code %d, got %d", test.expectedCode, rw.Code)
}
location := rw.Header().Get("Location")
if location != test.expectedURL {
t.Errorf("Expected location '%s', got '%s'", test.expectedURL, location)
}
})
}
}
func TestSessionHandler_buildLogoutURL(t *testing.T) {
tests := []struct {
name string
endSessionURL string
postLogoutRedirectURI string
clientID string
idToken string
expected string
}{
{
name: "Complete logout URL with all parameters",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://example.com/post-logout",
clientID: "test-client-id",
idToken: "test-id-token",
expected: "https://provider.example.com/logout?id_token_hint=test-id-token&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
{
name: "Logout URL without ID token",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://example.com/post-logout",
clientID: "test-client-id",
idToken: "",
expected: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
{
name: "No end session URL",
endSessionURL: "",
postLogoutRedirectURI: "https://example.com/post-logout",
clientID: "test-client-id",
idToken: "test-id-token",
expected: "https://example.com/post-logout",
},
{
name: "End session URL with existing query parameters",
endSessionURL: "https://provider.example.com/logout?foo=bar",
postLogoutRedirectURI: "https://example.com/post-logout",
clientID: "test-client-id",
idToken: "",
expected: "https://provider.example.com/logout?foo=bar&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &SessionHandler{
endSessionURL: test.endSessionURL,
postLogoutRedirectURI: test.postLogoutRedirectURI,
clientID: test.clientID,
}
result := handler.buildLogoutURL(test.idToken)
if result != test.expected {
t.Errorf("Expected '%s', got '%s'", test.expected, result)
}
})
}
}
func TestSessionHandler_ValidateSession(t *testing.T) {
handler := &SessionHandler{}
tests := []struct {
name string
session Session
expectedValid bool
expectedAuth bool
expectedMessage string
}{
{
name: "Nil session",
session: nil,
expectedValid: false,
expectedAuth: true,
expectedMessage: "session is nil",
},
{
name: "Not authenticated session",
session: &MockSession{
authenticated: false,
},
expectedValid: false,
expectedAuth: true,
expectedMessage: "session not authenticated",
},
{
name: "Authenticated session without email",
session: &MockSession{
authenticated: true,
email: "",
},
expectedValid: false,
expectedAuth: true,
expectedMessage: "no email in session",
},
{
name: "Valid authenticated session with email",
session: &MockSession{
authenticated: true,
email: "[email protected]",
},
expectedValid: true,
expectedAuth: false,
expectedMessage: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := handler.ValidateSession(test.session)
if result.Valid != test.expectedValid {
t.Errorf("Expected Valid %v, got %v", test.expectedValid, result.Valid)
}
if result.NeedsAuth != test.expectedAuth {
t.Errorf("Expected NeedsAuth %v, got %v", test.expectedAuth, result.NeedsAuth)
}
if result.ErrorMessage != test.expectedMessage {
t.Errorf("Expected ErrorMessage '%s', got '%s'", test.expectedMessage, result.ErrorMessage)
}
})
}
}
func TestSessionHandler_CleanupExpiredSession(t *testing.T) {
tests := []struct {
name string
session *MockSession
expectError bool
}{
{
name: "Successful cleanup",
session: &MockSession{
authenticated: true,
email: "[email protected]",
refreshToken: "refresh-token",
},
expectError: false,
},
{
name: "Save error during cleanup",
session: &MockSession{
authenticated: true,
email: "[email protected]",
refreshToken: "refresh-token",
saveError: fmt.Errorf("save failed"),
},
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &SessionHandler{
logger: &MockLogger{},
}
req := httptest.NewRequest("GET", "/", nil)
rw := httptest.NewRecorder()
err := handler.CleanupExpiredSession(rw, req, test.session)
if test.expectError && err == nil {
t.Error("Expected error but got nil")
}
if !test.expectError && err != nil {
t.Errorf("Expected no error but got: %v", err)
}
if test.session != nil && !test.expectError {
if test.session.authenticated {
t.Error("Expected session authenticated to be false after cleanup")
}
if test.session.email != "" {
t.Error("Expected session email to be empty after cleanup")
}
if test.session.refreshToken != "" {
t.Error("Expected session refresh token to be empty after cleanup")
}
}
})
}
// Test nil session separately
t.Run("Nil session", func(t *testing.T) {
handler := &SessionHandler{
logger: &MockLogger{},
}
req := httptest.NewRequest("GET", "/", nil)
rw := httptest.NewRecorder()
var nilSession Session = nil
err := handler.CleanupExpiredSession(rw, req, nilSession)
if err != nil {
t.Errorf("Expected no error for nil session, got: %v", err)
}
})
}
func TestSessionHandler_IsAjaxRequest(t *testing.T) {
handler := &SessionHandler{}
tests := []struct {
name string
headers map[string]string
expected bool
}{
{
name: "XMLHttpRequest header",
headers: map[string]string{
"X-Requested-With": "XMLHttpRequest",
},
expected: true,
},
{
name: "JSON Accept header without HTML",
headers: map[string]string{
"Accept": "application/json",
},
expected: true,
},
{
name: "JSON Accept header with HTML",
headers: map[string]string{
"Accept": "application/json, text/html",
},
expected: false,
},
{
name: "Fetch API CORS mode",
headers: map[string]string{
"Sec-Fetch-Mode": "cors",
},
expected: true,
},
{
name: "Regular browser request",
headers: map[string]string{
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
},
expected: false,
},
{
name: "No special headers",
headers: map[string]string{},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
for key, value := range test.headers {
req.Header.Set(key, value)
}
result := handler.IsAjaxRequest(req)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}
func TestSessionHandler_SendErrorResponse(t *testing.T) {
tests := []struct {
name string
isAjax bool
message string
statusCode int
expectedContentType string
expectedBodyContains string
}{
{
name: "AJAX error response",
isAjax: true,
message: "Authentication failed",
statusCode: http.StatusUnauthorized,
expectedContentType: "application/json",
expectedBodyContains: `{"error": "Authentication failed"}`,
},
{
name: "Browser error response",
isAjax: false,
message: "Session expired",
statusCode: http.StatusForbidden,
expectedContentType: "text/html",
expectedBodyContains: "<h1>Error 403</h1>",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &SessionHandler{}
req := httptest.NewRequest("GET", "/", nil)
if test.isAjax {
req.Header.Set("X-Requested-With", "XMLHttpRequest")
}
rw := httptest.NewRecorder()
handler.SendErrorResponse(rw, req, test.message, test.statusCode)
if rw.Code != test.statusCode {
t.Errorf("Expected status code %d, got %d", test.statusCode, rw.Code)
}
contentType := rw.Header().Get("Content-Type")
if contentType != test.expectedContentType {
t.Errorf("Expected Content-Type '%s', got '%s'", test.expectedContentType, contentType)
}
body := rw.Body.String()
if !strings.Contains(body, test.expectedBodyContains) {
t.Errorf("Expected body to contain '%s', got '%s'", test.expectedBodyContains, body)
}
})
}
}
func TestSessionHandler_SetSecurityHeaders(t *testing.T) {
tests := []struct {
name string
method string
origin string
expectedCORS bool
expectedStatus int
}{
{
name: "Regular request without CORS",
method: "GET",
origin: "",
expectedCORS: false,
expectedStatus: 0, // No status written
},
{
name: "CORS request with origin",
method: "GET",
origin: "https://example.com",
expectedCORS: true,
expectedStatus: 0,
},
{
name: "OPTIONS preflight request",
method: "OPTIONS",
origin: "https://example.com",
expectedCORS: true,
expectedStatus: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := &SessionHandler{}
req := httptest.NewRequest(test.method, "/", nil)
if test.origin != "" {
req.Header.Set("Origin", test.origin)
}
rw := httptest.NewRecorder()
handler.SetSecurityHeaders(rw, req)
// Check standard security headers
expectedSecurityHeaders := map[string]string{
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
}
for header, expectedValue := range expectedSecurityHeaders {
actualValue := rw.Header().Get(header)
if actualValue != expectedValue {
t.Errorf("Expected %s header '%s', got '%s'", header, expectedValue, actualValue)
}
}
// Check CORS headers
if test.expectedCORS {
corsOrigin := rw.Header().Get("Access-Control-Allow-Origin")
if corsOrigin != test.origin {
t.Errorf("Expected CORS origin '%s', got '%s'", test.origin, corsOrigin)
}
corsCredentials := rw.Header().Get("Access-Control-Allow-Credentials")
if corsCredentials != "true" {
t.Errorf("Expected CORS credentials 'true', got '%s'", corsCredentials)
}
corsMethods := rw.Header().Get("Access-Control-Allow-Methods")
if corsMethods != "GET, POST, OPTIONS" {
t.Errorf("Expected CORS methods 'GET, POST, OPTIONS', got '%s'", corsMethods)
}
corsHeaders := rw.Header().Get("Access-Control-Allow-Headers")
if corsHeaders != "Authorization, Content-Type" {
t.Errorf("Expected CORS headers 'Authorization, Content-Type', got '%s'", corsHeaders)
}
} else {
corsOrigin := rw.Header().Get("Access-Control-Allow-Origin")
if corsOrigin != "" {
t.Errorf("Expected no CORS origin header, got '%s'", corsOrigin)
}
}
// Check status code for OPTIONS requests
if test.expectedStatus > 0 {
if rw.Code != test.expectedStatus {
t.Errorf("Expected status code %d, got %d", test.expectedStatus, rw.Code)
}
}
})
}
}
func TestSessionValidationResult(t *testing.T) {
result := SessionValidationResult{
Valid: true,
NeedsAuth: false,
ErrorMessage: "test message",
}
if !result.Valid {
t.Error("Expected Valid to be true")
}
if result.NeedsAuth {
t.Error("Expected NeedsAuth to be false")
}
if result.ErrorMessage != "test message" {
t.Errorf("Expected ErrorMessage 'test message', got '%s'", result.ErrorMessage)
}
}
+655
View File
@@ -0,0 +1,655 @@
package middleware
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// MockLogger implements the Logger interface for testing
type MockLogger struct {
DebugCalls []string
DebugfCalls []string
ErrorCalls []string
ErrorfCalls []string
InfoCalls []string
InfofCalls []string
}
func (m *MockLogger) Debug(msg string) {
m.DebugCalls = append(m.DebugCalls, msg)
}
func (m *MockLogger) Debugf(format string, args ...interface{}) {
m.DebugfCalls = append(m.DebugfCalls, format)
}
func (m *MockLogger) Error(msg string) {
m.ErrorCalls = append(m.ErrorCalls, msg)
}
func (m *MockLogger) Errorf(format string, args ...interface{}) {
m.ErrorfCalls = append(m.ErrorfCalls, format)
}
func (m *MockLogger) Info(msg string) {
m.InfoCalls = append(m.InfoCalls, msg)
}
func (m *MockLogger) Infof(format string, args ...interface{}) {
m.InfofCalls = append(m.InfofCalls, format)
}
// TestNewRequestProcessor tests the constructor
func TestNewRequestProcessor(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
if processor == nil {
t.Error("Expected NewRequestProcessor to return non-nil processor")
return
}
if processor.logger != logger {
t.Error("Expected processor to use provided logger")
}
}
// TestBuildRequestContext tests request context building
func TestBuildRequestContext(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
setupRequest func() (*http.Request, http.ResponseWriter)
redirectPath string
expectedURL string
expectedHost string
}{
{
name: "Basic HTTP request",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
rw := httptest.NewRecorder()
return req, rw
},
redirectPath: "/callback",
expectedURL: "http://example.com/callback",
expectedHost: "example.com",
},
{
name: "HTTPS request with TLS",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "https://secure.com/test", nil)
req.TLS = &tls.ConnectionState{} // Simulate TLS
rw := httptest.NewRecorder()
return req, rw
},
redirectPath: "/auth",
expectedURL: "https://secure.com/auth",
expectedHost: "secure.com",
},
{
name: "Request with X-Forwarded-Proto header",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://internal.com/test", nil)
req.Header.Set("X-Forwarded-Proto", "https")
rw := httptest.NewRecorder()
return req, rw
},
redirectPath: "/callback",
expectedURL: "https://internal.com/callback",
expectedHost: "internal.com",
},
{
name: "Request with X-Forwarded-Host header",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://internal.com/test", nil)
req.Header.Set("X-Forwarded-Host", "public.com")
rw := httptest.NewRecorder()
return req, rw
},
redirectPath: "/callback",
expectedURL: "http://public.com/callback",
expectedHost: "public.com",
},
{
name: "Request with both forwarded headers",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://internal.com/test", nil)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "public.com")
rw := httptest.NewRecorder()
return req, rw
},
redirectPath: "/auth",
expectedURL: "https://public.com/auth",
expectedHost: "public.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, rw := tt.setupRequest()
ctx := processor.BuildRequestContext(rw, req, tt.redirectPath)
if ctx == nil {
t.Error("Expected BuildRequestContext to return non-nil context")
return
}
if ctx.Writer != rw {
t.Error("Expected context writer to match provided writer")
}
if ctx.Request != req {
t.Error("Expected context request to match provided request")
}
if ctx.RedirectURL != tt.expectedURL {
t.Errorf("Expected redirect URL '%s', got '%s'", tt.expectedURL, ctx.RedirectURL)
}
if ctx.Host != tt.expectedHost {
t.Errorf("Expected host '%s', got '%s'", tt.expectedHost, ctx.Host)
}
})
}
}
// TestIsHealthCheckRequest tests health check detection
func TestIsHealthCheckRequest(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
path string
expected bool
}{
{
name: "Health check path",
path: "/health",
expected: true,
},
{
name: "Health check subpath",
path: "/health/status",
expected: true,
},
{
name: "Health check with query params",
path: "/health?check=db",
expected: true,
},
{
name: "Not a health check",
path: "/api/users",
expected: false,
},
{
name: "Health-related path (matches prefix)",
path: "/healthiness",
expected: true, // HasPrefix behavior - this actually matches
},
{
name: "Root path",
path: "/",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com"+tt.path, nil)
result := processor.IsHealthCheckRequest(req)
if result != tt.expected {
t.Errorf("Expected IsHealthCheckRequest to return %v for path '%s', got %v", tt.expected, tt.path, result)
}
})
}
}
// TestIsEventStreamRequest tests event stream detection
func TestIsEventStreamRequest(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
acceptHeader string
expected bool
}{
{
name: "Event stream accept header",
acceptHeader: "text/event-stream",
expected: true,
},
{
name: "Event stream with other types",
acceptHeader: "text/html, text/event-stream, application/json",
expected: true,
},
{
name: "JSON accept header",
acceptHeader: "application/json",
expected: false,
},
{
name: "HTML accept header",
acceptHeader: "text/html,application/xhtml+xml",
expected: false,
},
{
name: "Empty accept header",
acceptHeader: "",
expected: false,
},
{
name: "Similar but not event stream",
acceptHeader: "text/event-source",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
if tt.acceptHeader != "" {
req.Header.Set("Accept", tt.acceptHeader)
}
result := processor.IsEventStreamRequest(req)
if result != tt.expected {
t.Errorf("Expected IsEventStreamRequest to return %v for accept header '%s', got %v", tt.expected, tt.acceptHeader, result)
}
})
}
}
// TestIsAjaxRequest tests AJAX request detection
func TestIsAjaxRequest(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
setupHeader func(*http.Request)
expected bool
}{
{
name: "XMLHttpRequest header",
setupHeader: func(req *http.Request) {
req.Header.Set("X-Requested-With", "XMLHttpRequest")
},
expected: true,
},
{
name: "JSON content type",
setupHeader: func(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
},
expected: true,
},
{
name: "JSON content type with charset",
setupHeader: func(req *http.Request) {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
},
expected: true,
},
{
name: "JSON accept header",
setupHeader: func(req *http.Request) {
req.Header.Set("Accept", "application/json")
},
expected: true,
},
{
name: "JSON accept with other types",
setupHeader: func(req *http.Request) {
req.Header.Set("Accept", "text/html, application/json, application/xml")
},
expected: true,
},
{
name: "Multiple AJAX indicators",
setupHeader: func(req *http.Request) {
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
},
expected: true,
},
{
name: "Regular HTML request",
setupHeader: func(req *http.Request) {
req.Header.Set("Accept", "text/html,application/xhtml+xml")
},
expected: false,
},
{
name: "Form submission",
setupHeader: func(req *http.Request) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
},
expected: false,
},
{
name: "No special headers",
setupHeader: func(req *http.Request) {},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("POST", "http://example.com/api", nil)
tt.setupHeader(req)
result := processor.IsAjaxRequest(req)
if result != tt.expected {
t.Errorf("Expected IsAjaxRequest to return %v, got %v", tt.expected, result)
}
})
}
}
// TestWaitForInitialization tests initialization waiting
func TestWaitForInitialization(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
t.Run("Initialization completes successfully", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
initComplete := make(chan struct{})
go func() {
time.Sleep(10 * time.Millisecond)
close(initComplete)
}()
err := processor.WaitForInitialization(req, initComplete)
if err != nil {
t.Errorf("Expected no error when initialization completes, got: %v", err)
}
})
t.Run("Request context cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
req := httptest.NewRequest("GET", "http://example.com/test", nil)
req = req.WithContext(ctx)
initComplete := make(chan struct{})
go func() {
time.Sleep(10 * time.Millisecond)
cancel()
}()
err := processor.WaitForInitialization(req, initComplete)
if err == nil {
t.Error("Expected error when request context is cancelled")
}
if !strings.Contains(err.Error(), "request cancelled") {
t.Errorf("Expected 'request cancelled' error, got: %v", err)
}
if len(logger.DebugCalls) == 0 {
t.Error("Expected debug log when request is cancelled")
}
})
t.Run("Initialization timeout", func(t *testing.T) {
if testing.Short() {
t.Skip("Skipping timeout test in short mode")
}
req := httptest.NewRequest("GET", "http://example.com/test", nil)
initComplete := make(chan struct{}) // Never closes
// Note: This test takes 30 seconds due to hardcoded timeout in implementation
start := time.Now()
err := processor.WaitForInitialization(req, initComplete)
duration := time.Since(start)
if err == nil {
t.Error("Expected timeout error")
}
if !strings.Contains(err.Error(), "timeout") {
t.Errorf("Expected timeout error, got: %v", err)
}
// The timeout should be around 30 seconds, allow some variance
if duration < 29*time.Second || duration > 31*time.Second {
t.Errorf("Expected timeout after ~30 seconds, but got %v", duration)
}
if len(logger.ErrorCalls) == 0 {
t.Error("Expected error log when timeout occurs")
}
})
}
// TestDetermineScheme tests scheme determination
func TestDetermineScheme(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
setup func(*http.Request)
expected string
}{
{
name: "X-Forwarded-Proto HTTPS",
setup: func(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "https")
},
expected: "https",
},
{
name: "X-Forwarded-Proto HTTP",
setup: func(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http")
},
expected: "http",
},
{
name: "TLS connection without header",
setup: func(req *http.Request) {
req.TLS = &tls.ConnectionState{}
},
expected: "https",
},
{
name: "No TLS, no header",
setup: func(req *http.Request) {
// No special setup
},
expected: "http",
},
{
name: "X-Forwarded-Proto takes precedence over TLS",
setup: func(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http")
req.TLS = &tls.ConnectionState{}
},
expected: "http",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
tt.setup(req)
result := processor.determineScheme(req)
if result != tt.expected {
t.Errorf("Expected scheme '%s', got '%s'", tt.expected, result)
}
})
}
}
// TestDetermineHost tests host determination
func TestDetermineHost(t *testing.T) {
logger := &MockLogger{}
processor := NewRequestProcessor(logger)
tests := []struct {
name string
setup func(*http.Request)
expected string
}{
{
name: "X-Forwarded-Host header present",
setup: func(req *http.Request) {
req.Header.Set("X-Forwarded-Host", "public.example.com")
},
expected: "public.example.com",
},
{
name: "No X-Forwarded-Host, use req.Host",
setup: func(req *http.Request) {
// No special setup, will use req.Host
},
expected: "example.com",
},
{
name: "Empty X-Forwarded-Host, fallback to req.Host",
setup: func(req *http.Request) {
req.Header.Set("X-Forwarded-Host", "")
},
expected: "example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
tt.setup(req)
result := processor.determineHost(req)
if result != tt.expected {
t.Errorf("Expected host '%s', got '%s'", tt.expected, result)
}
})
}
}
// TestBuildFullURL tests URL building
func TestBuildFullURL(t *testing.T) {
tests := []struct {
name string
scheme string
host string
path string
expected string
}{
{
name: "Basic URL construction",
scheme: "https",
host: "example.com",
path: "/callback",
expected: "https://example.com/callback",
},
{
name: "Path without leading slash",
scheme: "http",
host: "test.com",
path: "auth",
expected: "http://test.com/auth",
},
{
name: "Absolute HTTP URL in path",
scheme: "https",
host: "example.com",
path: "http://other.com/callback",
expected: "http://other.com/callback",
},
{
name: "Absolute HTTPS URL in path",
scheme: "http",
host: "example.com",
path: "https://secure.com/auth",
expected: "https://secure.com/auth",
},
{
name: "Root path",
scheme: "https",
host: "example.com:8080",
path: "/",
expected: "https://example.com:8080/",
},
{
name: "Empty path",
scheme: "https",
host: "example.com",
path: "",
expected: "https://example.com/",
},
{
name: "Path with query parameters",
scheme: "https",
host: "example.com",
path: "/callback?state=abc123",
expected: "https://example.com/callback?state=abc123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildFullURL(tt.scheme, tt.host, tt.path)
if result != tt.expected {
t.Errorf("Expected URL '%s', got '%s'", tt.expected, result)
}
})
}
}
// TestRequestContext tests the RequestContext struct
func TestRequestContext(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/test", nil)
rw := httptest.NewRecorder()
ctx := &RequestContext{
Writer: rw,
Request: req,
RedirectURL: "https://example.com/callback",
Scheme: "https",
Host: "example.com",
}
if ctx.Writer != rw {
t.Error("Expected Writer to be set correctly")
}
if ctx.Request != req {
t.Error("Expected Request to be set correctly")
}
if ctx.RedirectURL != "https://example.com/callback" {
t.Error("Expected RedirectURL to be set correctly")
}
if ctx.Scheme != "https" {
t.Error("Expected Scheme to be set correctly")
}
if ctx.Host != "example.com" {
t.Error("Expected Host to be set correctly")
}
}
+2 -2
View File
@@ -30,7 +30,7 @@ func (p *StringBuilderPool) Get() []byte {
// Put returns a string builder to the pool
func (p *StringBuilderPool) Put(buf []byte) {
if cap(buf) < 4096 { // Don't pool overly large buffers
if buf != nil && cap(buf) < 4096 { // Don't pool overly large buffers or nil buffers
buf = buf[:0] // Reset length but keep capacity
p.pool.Put(&buf)
}
@@ -63,7 +63,7 @@ func (p *ByteSlicePool) Get() []byte {
// Put returns a byte slice to the pool
func (p *ByteSlicePool) Put(buf []byte) {
if len(buf) == p.size {
if buf != nil && len(buf) == p.size {
p.pool.Put(&buf)
}
}
+510
View File
@@ -0,0 +1,510 @@
package optimization
import (
"runtime"
"sync"
"testing"
)
// TestNewStringBuilderPool tests StringBuilderPool creation
func TestNewStringBuilderPool(t *testing.T) {
pool := NewStringBuilderPool()
if pool == nil {
t.Error("Expected NewStringBuilderPool to return non-nil pool")
}
}
// TestStringBuilderPool tests StringBuilderPool functionality
func TestStringBuilderPool(t *testing.T) {
pool := NewStringBuilderPool()
// Test Get
buf1 := pool.Get()
if buf1 == nil {
t.Error("Expected Get to return non-nil buffer")
}
// Check initial capacity
if cap(buf1) != 256 {
t.Errorf("Expected initial capacity of 256, got %d", cap(buf1))
}
// Check initial length
if len(buf1) != 0 {
t.Errorf("Expected initial length of 0, got %d", len(buf1))
}
// Test Put and reuse
// Simulate some usage
buf1 = append(buf1, []byte("hello world")...)
pool.Put(buf1)
// Get another buffer - should reuse the one we just put
buf2 := pool.Get()
if buf2 == nil {
t.Error("Expected Get to return non-nil buffer after Put")
}
// Buffer should be reset to length 0 but keep capacity
if len(buf2) != 0 {
t.Errorf("Expected buffer length to be reset to 0, got %d", len(buf2))
}
// Should reuse the same underlying array
if cap(buf2) < 256 {
t.Errorf("Expected buffer capacity to be at least 256, got %d", cap(buf2))
}
}
// TestStringBuilderPoolLargeBuffer tests that large buffers are not pooled
func TestStringBuilderPoolLargeBuffer(t *testing.T) {
pool := NewStringBuilderPool()
// Get a buffer and make it very large
buf := pool.Get()
largeBuf := make([]byte, 5000) // Larger than 4096 threshold
buf = append(buf, largeBuf...)
// Put it back - should not be pooled due to size
pool.Put(buf)
// Get a new buffer - should be a fresh one with default capacity
newBuf := pool.Get()
if cap(newBuf) != 256 {
t.Errorf("Expected fresh buffer with capacity 256, got %d", cap(newBuf))
}
}
// TestNewByteSlicePool tests ByteSlicePool creation
func TestNewByteSlicePool(t *testing.T) {
size := 1024
pool := NewByteSlicePool(size)
if pool == nil {
t.Error("Expected NewByteSlicePool to return non-nil pool")
return
}
if pool.size != size {
t.Errorf("Expected pool size %d, got %d", size, pool.size)
}
}
// TestByteSlicePool tests ByteSlicePool functionality
func TestByteSlicePool(t *testing.T) {
size := 512
pool := NewByteSlicePool(size)
// Test Get
buf1 := pool.Get()
if buf1 == nil {
t.Error("Expected Get to return non-nil buffer")
}
// Check size
if len(buf1) != size {
t.Errorf("Expected buffer length %d, got %d", size, len(buf1))
}
// Test Put and reuse
pool.Put(buf1)
// Get another buffer - should reuse the one we just put
buf2 := pool.Get()
if buf2 == nil {
t.Error("Expected Get to return non-nil buffer after Put")
}
// Should be the same size
if len(buf2) != size {
t.Errorf("Expected buffer length %d, got %d", size, len(buf2))
}
}
// TestByteSlicePoolWrongSize tests that wrong-sized buffers are not pooled
func TestByteSlicePoolWrongSize(t *testing.T) {
size := 512
pool := NewByteSlicePool(size)
// Get a buffer
buf := pool.Get()
// Resize it (simulate usage that changes size)
buf = buf[:100] // Change length to 100
// Put it back - should not be pooled due to wrong size
pool.Put(buf)
// Get a new buffer - should be a fresh one with correct size
newBuf := pool.Get()
if len(newBuf) != size {
t.Errorf("Expected fresh buffer with length %d, got %d", size, len(newBuf))
}
}
// TestGlobalStringBuilderPool tests global string builder pool functions
func TestGlobalStringBuilderPool(t *testing.T) {
// Test GetStringBuilder
buf1 := GetStringBuilder()
if buf1 == nil {
t.Error("Expected GetStringBuilder to return non-nil buffer")
}
if cap(buf1) != 256 {
t.Errorf("Expected initial capacity of 256, got %d", cap(buf1))
}
// Test PutStringBuilder and reuse
buf1 = append(buf1, []byte("test data")...)
PutStringBuilder(buf1)
buf2 := GetStringBuilder()
if len(buf2) != 0 {
t.Errorf("Expected buffer length to be reset to 0, got %d", len(buf2))
}
}
// TestGlobalByteSlicePool tests global byte slice pool functions
func TestGlobalByteSlicePool(t *testing.T) {
// Test GetByteSlice
buf1 := GetByteSlice()
if buf1 == nil {
t.Error("Expected GetByteSlice to return non-nil buffer")
}
if len(buf1) != 2048 {
t.Errorf("Expected buffer length of 2048, got %d", len(buf1))
}
// Test PutByteSlice and reuse
PutByteSlice(buf1)
buf2 := GetByteSlice()
if len(buf2) != 2048 {
t.Errorf("Expected buffer length of 2048, got %d", len(buf2))
}
}
// TestPoolsConcurrency tests that pools work correctly under concurrent access
func TestPoolsConcurrency(t *testing.T) {
const numGoroutines = 100
const numOperations = 1000
// Test StringBuilderPool concurrency
t.Run("StringBuilderPool concurrency", func(t *testing.T) {
pool := NewStringBuilderPool()
var wg sync.WaitGroup
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
buf := pool.Get()
buf = append(buf, []byte("test")...)
pool.Put(buf)
}
}()
}
wg.Wait()
})
// Test ByteSlicePool concurrency
t.Run("ByteSlicePool concurrency", func(t *testing.T) {
pool := NewByteSlicePool(1024)
var wg sync.WaitGroup
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
buf := pool.Get()
// Simulate some work
for k := 0; k < len(buf); k++ {
buf[k] = byte(k % 256)
}
pool.Put(buf)
}
}()
}
wg.Wait()
})
// Test global pools concurrency
t.Run("Global pools concurrency", func(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
// Test string builder pool
strBuf := GetStringBuilder()
strBuf = append(strBuf, []byte("test")...)
PutStringBuilder(strBuf)
// Test byte slice pool
byteBuf := GetByteSlice()
byteBuf[0] = 42
PutByteSlice(byteBuf)
}
}()
}
wg.Wait()
})
}
// TestMemoryReuse tests that pools actually reuse memory
func TestMemoryReuse(t *testing.T) {
t.Run("StringBuilderPool memory reuse", func(t *testing.T) {
pool := NewStringBuilderPool()
// Get a buffer and note its address
buf1 := pool.Get()
addr1 := &buf1[0:1][0] // Get address of first element
// Put it back
pool.Put(buf1)
// Get another buffer - should be the same memory
buf2 := pool.Get()
addr2 := &buf2[0:1][0]
if addr1 != addr2 {
t.Error("Expected pool to reuse memory, but got different addresses")
}
})
t.Run("ByteSlicePool memory reuse", func(t *testing.T) {
pool := NewByteSlicePool(1024)
// Get a buffer and note its address
buf1 := pool.Get()
addr1 := &buf1[0]
// Put it back
pool.Put(buf1)
// Get another buffer - should be the same memory
buf2 := pool.Get()
addr2 := &buf2[0]
if addr1 != addr2 {
t.Error("Expected pool to reuse memory, but got different addresses")
}
})
}
// TestPoolEfficiency tests that pools reduce allocations
func TestPoolEfficiency(t *testing.T) {
if testing.Short() {
t.Skip("Skipping efficiency test in short mode")
}
t.Run("StringBuilderPool allocation reduction", func(t *testing.T) {
pool := NewStringBuilderPool()
// Test with pool
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
for i := 0; i < 10000; i++ {
buf := pool.Get()
buf = append(buf, []byte("test data for benchmarking")...)
pool.Put(buf)
}
runtime.GC()
runtime.ReadMemStats(&m2)
poolAllocs := m2.Mallocs - m1.Mallocs
// Test without pool (creating new buffers each time)
runtime.GC()
runtime.ReadMemStats(&m1)
for i := 0; i < 10000; i++ {
buf := make([]byte, 0, 256)
_ = append(buf, []byte("test data for benchmarking")...)
}
runtime.GC()
runtime.ReadMemStats(&m2)
directAllocs := m2.Mallocs - m1.Mallocs
t.Logf("Pool allocations: %d, Direct allocations: %d", poolAllocs, directAllocs)
// Pool should result in fewer allocations (though this test can be flaky)
if poolAllocs >= directAllocs {
t.Logf("Warning: Pool did not reduce allocations as expected (pool: %d, direct: %d)", poolAllocs, directAllocs)
}
})
}
// TestEdgeCases tests edge cases and error conditions
func TestEdgeCases(t *testing.T) {
t.Run("StringBuilderPool with nil buffer", func(t *testing.T) {
pool := NewStringBuilderPool()
// This should not panic
pool.Put(nil)
// Should still be able to get a buffer
buf := pool.Get()
if buf == nil {
t.Error("Expected Get to return non-nil buffer after putting nil")
}
})
t.Run("ByteSlicePool with nil buffer", func(t *testing.T) {
pool := NewByteSlicePool(1024)
// This should not panic
pool.Put(nil)
// Should still be able to get a buffer
buf := pool.Get()
if buf == nil {
t.Error("Expected Get to return non-nil buffer after putting nil")
}
})
t.Run("Zero-size ByteSlicePool", func(t *testing.T) {
pool := NewByteSlicePool(0)
buf := pool.Get()
if len(buf) != 0 {
t.Errorf("Expected zero-length buffer, got %d", len(buf))
}
// Should not panic
pool.Put(buf)
})
t.Run("Global pools with nil", func(t *testing.T) {
// These should not panic
PutStringBuilder(nil)
PutByteSlice(nil)
// Should still work
buf1 := GetStringBuilder()
buf2 := GetByteSlice()
if buf1 == nil || buf2 == nil {
t.Error("Expected global pools to work after putting nil")
}
})
}
// TestTypeAssertions tests that type assertions in pools work correctly
func TestTypeAssertions(t *testing.T) {
t.Run("StringBuilderPool type assertion", func(t *testing.T) {
pool := NewStringBuilderPool()
// This should not panic
buf := pool.Get()
if buf == nil {
t.Error("Expected non-nil buffer")
}
// Buffer should be usable
buf = append(buf, []byte("test")...)
if string(buf) != "test" {
t.Errorf("Expected 'test', got '%s'", string(buf))
}
})
t.Run("ByteSlicePool type assertion", func(t *testing.T) {
pool := NewByteSlicePool(10)
// This should not panic
buf := pool.Get()
if buf == nil {
t.Error("Expected non-nil buffer")
}
// Buffer should be usable
buf[0] = 42
if buf[0] != 42 {
t.Errorf("Expected 42, got %d", buf[0])
}
})
}
// BenchmarkStringBuilderPool benchmarks the string builder pool
func BenchmarkStringBuilderPool(b *testing.B) {
pool := NewStringBuilderPool()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := pool.Get()
buf = append(buf, []byte("benchmark test data")...)
pool.Put(buf)
}
})
}
// BenchmarkByteSlicePool benchmarks the byte slice pool
func BenchmarkByteSlicePool(b *testing.B) {
pool := NewByteSlicePool(1024)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := pool.Get()
buf[0] = 42
pool.Put(buf)
}
})
}
// BenchmarkGlobalPools benchmarks the global pools
func BenchmarkGlobalPools(b *testing.B) {
b.Run("StringBuilder", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := GetStringBuilder()
buf = append(buf, []byte("test")...)
PutStringBuilder(buf)
}
})
})
b.Run("ByteSlice", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := GetByteSlice()
buf[0] = 42
PutByteSlice(buf)
}
})
})
}
// BenchmarkDirectAllocation benchmarks direct allocation for comparison
func BenchmarkDirectAllocation(b *testing.B) {
b.Run("StringBuilder", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := make([]byte, 0, 256)
_ = append(buf, []byte("test")...)
}
})
})
b.Run("ByteSlice", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := make([]byte, 2048)
buf[0] = 42
}
})
})
}
+259
View File
@@ -142,6 +142,66 @@ func TestValidationFunctions(t *testing.T) {
valid: []string{"client123", "my-client_id", "123.456"},
invalid: []string{"", "client with spaces", "client@invalid"},
},
{
name: "ValidateURL",
function: ValidateURL,
valid: []string{"https://example.com", "https://sub.domain.org/path", "http://localhost", "https://example.com/path?query=value", "http://192.168.1.1"},
invalid: []string{"", "ftp://example.com", "not-a-url", "https://", "example.com", "http://localhost:8080"},
},
{
name: "ValidateScopes",
function: ValidateScopes,
valid: []string{"openid", "openid profile", "read write admin", "user_info"},
invalid: []string{"", "scope-with-dash", "scope@invalid", "scope with.dot", " "},
},
{
name: "ValidateSessionID",
function: ValidateSessionID,
valid: []string{"a1b2c3d4e5f6789012345678901234567890abcdef", "ABCDEF1234567890abcdef1234567890", "0123456789abcdef0123456789abcdef"},
invalid: []string{"", "too-short", "contains-invalid-chars!", "g123456789abcdef0123456789abcdef", "1234567890abcdef1234567890abcde"},
},
{
name: "ValidateCSRFToken",
function: ValidateCSRFToken,
valid: []string{"abc123", "ABC_123-xyz", "token-value_123", "_valid-token_"},
invalid: []string{"", "token with spaces", "token@invalid", "token.with.dots!", "token/with/slash"},
},
{
name: "ValidateNonce",
function: ValidateNonce,
valid: []string{"abc123", "ABC_123-xyz", "nonce-value_123", "_valid-nonce_"},
invalid: []string{"", "nonce with spaces", "nonce@invalid", "nonce.with.dots!", "nonce/with/slash"},
},
{
name: "ValidateCodeVerifier",
function: ValidateCodeVerifier,
valid: []string{"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"},
invalid: []string{"", "too-short", "short", "verifier with spaces", "verifier@invalid", "a"},
},
{
name: "ValidateAuthCode",
function: ValidateAuthCode,
valid: []string{"auth_code_123", "ABC.123-xyz/code+value=", "simple-code"},
invalid: []string{"", "code with spaces", "code@invalid"},
},
{
name: "ValidateRedirectURI",
function: ValidateRedirectURI,
valid: []string{"https://example.com/callback", "http://localhost:8080/auth", "https://app.example.org/oauth/callback", "http://127.0.0.1:3000"},
invalid: []string{"", "ftp://example.com", "not-a-url", "example.com/callback", "https://"},
},
{
name: "ValidateIPv4",
function: ValidateIPv4,
valid: []string{"192.168.1.1", "10.0.0.1", "127.0.0.1", "255.255.255.255", "0.0.0.0"},
invalid: []string{"", "256.1.1.1", "192.168.1", "192.168.1.1.1", "not-an-ip"},
},
{
name: "ValidateTenantID",
function: ValidateTenantID,
valid: []string{"12345678-1234-1234-1234-123456789abc", "ABCDEF12-3456-7890-ABCD-EF1234567890"},
invalid: []string{"", "not-a-uuid", "12345678-1234-1234-1234", "12345678-1234-1234-1234-123456789abcd", "123456781234123412341234567890ab"},
},
}
for _, tt := range tests {
@@ -223,3 +283,202 @@ func BenchmarkRegex_DirectCompile(b *testing.B) {
}
}
}
func TestRegexCache_Clear(t *testing.T) {
cache := NewRegexCache()
// Add some patterns to the cache
patterns := []string{`^test1$`, `^test2$`, `^test3$`}
for _, pattern := range patterns {
_, err := cache.Get(pattern)
if err != nil {
t.Fatalf("Failed to add pattern %s: %v", pattern, err)
}
}
// Verify cache has patterns
if cache.Size() != 3 {
t.Errorf("Expected cache size 3, got %d", cache.Size())
}
// Clear the cache
cache.Clear()
// Verify cache is empty
if cache.Size() != 0 {
t.Errorf("Expected cache size 0 after clear, got %d", cache.Size())
}
}
func TestIsBotUserAgent(t *testing.T) {
tests := []struct {
userAgent string
isBot bool
}{
{"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", true},
{"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", true},
{"facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", false},
{"crawler-bot/1.0", true},
{"spider-agent/2.0", true},
{"curl/7.68.0", true},
{"wget/1.20.3", true},
{"python-requests/2.25.1", true},
{"Go-http-client/1.1", true},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", false},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.userAgent, func(t *testing.T) {
result := IsBotUserAgent(tt.userAgent)
if result != tt.isBot {
t.Errorf("IsBotUserAgent(%q) = %v, want %v", tt.userAgent, result, tt.isBot)
}
})
}
}
func TestGetGlobalCache(t *testing.T) {
cache := GetGlobalCache()
if cache == nil {
t.Error("GetGlobalCache() should not return nil")
}
// Should return the same instance
cache2 := GetGlobalCache()
if cache != cache2 {
t.Error("GetGlobalCache() should return the same instance")
}
// Should have precompiled patterns
if cache.Size() == 0 {
t.Error("Global cache should have precompiled patterns")
}
}
func TestCompilePattern(t *testing.T) {
pattern := `^test_compile\d+$`
regex, err := CompilePattern(pattern)
if err != nil {
t.Fatalf("CompilePattern failed: %v", err)
}
if !regex.MatchString("test_compile123") {
t.Error("Compiled pattern should match 'test_compile123'")
}
if regex.MatchString("test_compile") {
t.Error("Compiled pattern should not match 'test_compile'")
}
// Test invalid pattern
_, err = CompilePattern(`[invalid`)
if err == nil {
t.Error("Expected error for invalid pattern")
}
}
func TestMustCompilePattern(t *testing.T) {
pattern := `^test_must_compile\d+$`
regex := MustCompilePattern(pattern)
if regex == nil {
t.Fatal("MustCompilePattern should not return nil")
}
if !regex.MatchString("test_must_compile456") {
t.Error("Compiled pattern should match 'test_must_compile456'")
}
// Test that it panics with invalid pattern
defer func() {
if r := recover(); r == nil {
t.Error("MustCompilePattern should panic with invalid pattern")
}
}()
MustCompilePattern(`[invalid`)
}
func TestAdditionalValidationEdgeCases(t *testing.T) {
// Test edge cases for ValidateURL
t.Run("ValidateURL_EdgeCases", func(t *testing.T) {
edgeCases := []struct {
url string
valid bool
}{
{"https://a.b", true},
{"http://localhost", true},
{"https://example.com/path?query=value#fragment", true},
{"http://192.168.0.1:8080/api", false},
{"https://", false},
{"http://", false},
{"https://example", true},
}
for _, tc := range edgeCases {
result := ValidateURL(tc.url)
if result != tc.valid {
t.Errorf("ValidateURL(%q) = %v, want %v", tc.url, result, tc.valid)
}
}
})
// Test edge cases for ValidateScopes
t.Run("ValidateScopes_EdgeCases", func(t *testing.T) {
edgeCases := []struct {
scopes string
valid bool
}{
{"a", true},
{"a b", true},
{"openid profile email", true},
{"user_profile", true},
{"read_all write_all", true},
{"scope-with-dash", false},
{"scope.with.dot", false},
{"scope@email", false},
{" scope", false},
{"scope ", false},
{"a b", true}, // pattern allows multiple spaces
}
for _, tc := range edgeCases {
result := ValidateScopes(tc.scopes)
if result != tc.valid {
t.Errorf("ValidateScopes(%q) = %v, want %v", tc.scopes, result, tc.valid)
}
}
})
// Test edge cases for ValidateSessionID
t.Run("ValidateSessionID_EdgeCases", func(t *testing.T) {
edgeCases := []struct {
sessionID string
valid bool
}{
{"12345678901234567890123456789012", true}, // 32 chars (min)
{"1234567890123456789012345678901", false}, // 31 chars (too short)
{string(make([]byte, 128)), false}, // 128 non-hex chars
{"abcdef1234567890ABCDEF1234567890" + string(make([]byte, 96)), false}, // 128+ chars with non-hex
}
// Generate valid 128-char hex string (max length)
validLongHex := ""
for i := 0; i < 128; i++ {
validLongHex += "a"
}
edgeCases = append(edgeCases, struct {
sessionID string
valid bool
}{validLongHex, true})
for _, tc := range edgeCases {
result := ValidateSessionID(tc.sessionID)
if result != tc.valid {
t.Errorf("ValidateSessionID(%q) = %v, want %v", tc.sessionID, result, tc.valid)
}
}
})
}
+3 -15
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"strings"
"time"
traefikoidc "github.com/lukaszraczylo/traefikoidc"
)
// Verifier handles token verification operations
@@ -30,7 +32,7 @@ type TokenCache interface {
// JWKCache interface for key management
type JWKCache interface {
GetJWKS(providerURL string) (*JWKS, error)
GetJWKS(providerURL string) (*traefikoidc.JWKSet, error)
}
// RateLimiter interface for request limiting
@@ -44,20 +46,6 @@ type Logger interface {
Errorf(format string, args ...interface{})
}
// JWKS represents JSON Web Key Set
type JWKS struct {
Keys []JWK `json:"keys"`
}
// JWK represents a JSON Web Key
type JWK struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
}
// JWT represents a parsed JWT token
type JWT struct {
Header map[string]interface{}
+457
View File
@@ -0,0 +1,457 @@
package token
import (
"strings"
"testing"
"time"
traefikoidc "github.com/lukaszraczylo/traefikoidc"
)
// Mock implementations for testing
type MockTokenCache struct {
data map[string]map[string]interface{}
}
func (m *MockTokenCache) Get(key string) (map[string]interface{}, bool) {
if m.data == nil {
return nil, false
}
value, exists := m.data[key]
return value, exists
}
func (m *MockTokenCache) Set(key string, claims map[string]interface{}, ttl time.Duration) {
if m.data == nil {
m.data = make(map[string]map[string]interface{})
}
m.data[key] = claims
}
type MockCache struct {
data map[string]interface{}
}
func (m *MockCache) Get(key string) (interface{}, bool) {
if m.data == nil {
return nil, false
}
value, exists := m.data[key]
return value, exists
}
func (m *MockCache) Set(key string, value interface{}, ttl time.Duration) {
if m.data == nil {
m.data = make(map[string]interface{})
}
m.data[key] = value
}
type MockJWKCache struct{}
func (m *MockJWKCache) GetJWKS(providerURL string) (*traefikoidc.JWKSet, error) {
return &traefikoidc.JWKSet{
Keys: []traefikoidc.JWK{
{
Kid: "test-key",
Kty: "RSA",
Use: "sig",
Alg: "RS256",
},
},
}, nil
}
type MockRateLimiter struct {
allow bool
}
func (m *MockRateLimiter) Allow() bool {
return m.allow
}
type MockLogger struct {
debugMessages []string
errorMessages []string
}
func (m *MockLogger) Debugf(format string, args ...interface{}) {
m.debugMessages = append(m.debugMessages, format)
}
func (m *MockLogger) Errorf(format string, args ...interface{}) {
m.errorMessages = append(m.errorMessages, format)
}
func TestNewVerifier(t *testing.T) {
tokenCache := &MockTokenCache{}
tokenBlacklist := &MockCache{}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: true}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
if verifier == nil {
t.Fatal("NewVerifier returned nil")
}
if verifier.tokenCache != tokenCache {
t.Error("TokenCache not set correctly")
}
if verifier.tokenBlacklist != tokenBlacklist {
t.Error("TokenBlacklist not set correctly")
}
// Note: Interface comparison would require reflecting on the actual implementation
// For now, we just check that the field was set to something non-nil
if verifier.jwkCache == nil {
t.Error("JWKCache not set correctly")
}
if verifier.limiter != limiter {
t.Error("RateLimiter not set correctly")
}
if verifier.logger != logger {
t.Error("Logger not set correctly")
}
}
func TestVerifierBasicFunctionality(t *testing.T) {
tokenCache := &MockTokenCache{}
tokenBlacklist := &MockCache{}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: true}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
// Test that the verifier was created successfully
if verifier == nil {
t.Fatal("Expected non-nil verifier")
}
}
func TestJWKSStructure(t *testing.T) {
jwks := &traefikoidc.JWKSet{
Keys: []traefikoidc.JWK{
{
Kid: "test-key-1",
Kty: "RSA",
Use: "sig",
Alg: "RS256",
},
{
Kid: "test-key-2",
Kty: "RSA",
Use: "sig",
Alg: "RS256",
},
},
}
if len(jwks.Keys) != 2 {
t.Errorf("Expected 2 keys, got %d", len(jwks.Keys))
}
if jwks.Keys[0].Kid != "test-key-1" {
t.Errorf("Expected Kid 'test-key-1', got '%s'", jwks.Keys[0].Kid)
}
if jwks.Keys[1].Kid != "test-key-2" {
t.Errorf("Expected Kid 'test-key-2', got '%s'", jwks.Keys[1].Kid)
}
}
func TestJWKStructure(t *testing.T) {
jwk := traefikoidc.JWK{
Kid: "test-key",
Kty: "RSA",
Use: "sig",
Alg: "RS256",
N: "test-modulus",
E: "test-exponent",
}
if jwk.Kid != "test-key" {
t.Errorf("Expected Kid 'test-key', got '%s'", jwk.Kid)
}
if jwk.Kty != "RSA" {
t.Errorf("Expected Kty 'RSA', got '%s'", jwk.Kty)
}
if jwk.Use != "sig" {
t.Errorf("Expected Use 'sig', got '%s'", jwk.Use)
}
if jwk.Alg != "RS256" {
t.Errorf("Expected Alg 'RS256', got '%s'", jwk.Alg)
}
}
func TestVerifyToken(t *testing.T) {
tests := []struct {
name string
token string
clientID string
jwksURL string
issuerURL string
rateLimitAllow bool
cacheData map[string]map[string]interface{}
blacklistData map[string]interface{}
expectedError string
}{
{
name: "Empty token",
token: "",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
expectedError: "invalid JWT format: token is empty",
},
{
name: "Invalid JWT format - too few parts",
token: "header.payload",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
expectedError: "invalid JWT format: expected JWT with 3 parts, got 2 parts",
},
{
name: "Invalid JWT format - too many parts",
token: "header.payload.signature.extra",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
expectedError: "invalid JWT format: expected JWT with 3 parts, got 4 parts",
},
{
name: "Token too short",
token: "a.b.c",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
expectedError: "token too short to be valid JWT",
},
{
name: "Blacklisted token",
token: "valid.format.token",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
blacklistData: map[string]interface{}{"valid.format.token": true},
expectedError: "token is blacklisted",
},
{
name: "Cached token - success",
token: "valid.format.token",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: true,
cacheData: map[string]map[string]interface{}{"valid.format.token": {"sub": "user123"}},
expectedError: "",
},
{
name: "Rate limit exceeded",
token: "valid.format.token",
clientID: "test-client",
jwksURL: "https://example.com/jwks",
issuerURL: "https://example.com",
rateLimitAllow: false,
expectedError: "rate limit exceeded",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tokenCache := &MockTokenCache{data: tt.cacheData}
tokenBlacklist := &MockCache{data: tt.blacklistData}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: tt.rateLimitAllow}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
err := verifier.VerifyToken(tt.token, tt.clientID, tt.jwksURL, tt.issuerURL)
if tt.expectedError == "" {
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
} else {
if err == nil {
t.Errorf("Expected error containing '%s', got nil", tt.expectedError)
} else if !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("Expected error containing '%s', got: %v", tt.expectedError, err)
}
}
})
}
}
func TestParseJWT(t *testing.T) {
tokenCache := &MockTokenCache{}
tokenBlacklist := &MockCache{}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: true}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
// Test parseJWT with a valid format token
jwt, err := verifier.parseJWT("header.payload.signature")
if err != nil {
t.Errorf("Expected no error parsing JWT, got: %v", err)
}
if jwt == nil {
t.Error("Expected non-nil JWT object")
return
}
if jwt.Header == nil {
t.Error("Expected non-nil Header map")
}
if jwt.Claims == nil {
t.Error("Expected non-nil Claims map")
}
}
func TestVerifyJWTSignatureAndClaims(t *testing.T) {
tokenCache := &MockTokenCache{}
tokenBlacklist := &MockCache{}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: true}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
jwt := &JWT{
Header: map[string]interface{}{"alg": "RS256"},
Claims: map[string]interface{}{"sub": "user123", "exp": float64(time.Now().Add(time.Hour).Unix())},
}
// Test signature verification (currently returns nil - placeholder)
err := verifier.verifyJWTSignatureAndClaims(jwt, "test.token.here", "client-id", "https://example.com/jwks", "https://example.com")
if err != nil {
t.Errorf("Expected no error from placeholder verification, got: %v", err)
}
}
func TestCacheVerifiedToken(t *testing.T) {
tokenCache := &MockTokenCache{}
tokenBlacklist := &MockCache{}
jwkCache := &MockJWKCache{}
limiter := &MockRateLimiter{allow: true}
logger := &MockLogger{}
verifier := NewVerifier(tokenCache, tokenBlacklist, jwkCache, limiter, logger)
tests := []struct {
name string
token string
claims map[string]interface{}
expected bool
}{
{
name: "Valid expiration time",
token: "test-token-1",
claims: map[string]interface{}{"exp": float64(time.Now().Add(time.Hour).Unix())},
expected: true,
},
{
name: "Expired token",
token: "test-token-2",
claims: map[string]interface{}{"exp": float64(time.Now().Add(-time.Hour).Unix())},
expected: false,
},
{
name: "No expiration claim",
token: "test-token-3",
claims: map[string]interface{}{"sub": "user123"},
expected: false,
},
{
name: "Invalid expiration type",
token: "test-token-4",
claims: map[string]interface{}{"exp": "invalid"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear cache before test
tokenCache.data = make(map[string]map[string]interface{})
verifier.cacheVerifiedToken(tt.token, tt.claims)
_, exists := tokenCache.Get(tt.token)
if exists != tt.expected {
t.Errorf("Expected cache existence: %v, got: %v", tt.expected, exists)
}
})
}
}
func TestMockInterfaces(t *testing.T) {
// Test MockTokenCache
tokenCache := &MockTokenCache{}
claims := map[string]interface{}{"sub": "user123", "exp": 1234567890}
tokenCache.Set("test-token", claims, time.Hour)
retrieved, exists := tokenCache.Get("test-token")
if !exists {
t.Error("Expected token to exist in cache")
}
if retrieved["sub"] != "user123" {
t.Errorf("Expected sub 'user123', got '%v'", retrieved["sub"])
}
// Test MockCache
cache := &MockCache{}
cache.Set("test-key", "test-value", time.Hour)
value, exists := cache.Get("test-key")
if !exists {
t.Error("Expected key to exist in cache")
}
if value != "test-value" {
t.Errorf("Expected 'test-value', got '%v'", value)
}
// Test MockRateLimiter
limiter := &MockRateLimiter{allow: true}
if !limiter.Allow() {
t.Error("Expected rate limiter to allow request")
}
limiter.allow = false
if limiter.Allow() {
t.Error("Expected rate limiter to deny request")
}
// Test MockLogger
logger := &MockLogger{}
logger.Debugf("test debug message")
logger.Errorf("test error message")
if len(logger.debugMessages) != 1 {
t.Errorf("Expected 1 debug message, got %d", len(logger.debugMessages))
}
if len(logger.errorMessages) != 1 {
t.Errorf("Expected 1 error message, got %d", len(logger.errorMessages))
}
}
+425
View File
@@ -1,6 +1,7 @@
package utils
import (
"os"
"reflect"
"testing"
)
@@ -128,3 +129,427 @@ func TestBuildFullURL(t *testing.T) {
}
}
}
func TestIsTestMode(t *testing.T) {
// This test is challenging because IsTestMode() depends on runtime conditions.
// We'll test what we can control via environment variables.
tests := []struct {
name string
setup func()
cleanup func()
expected bool
}{
{
name: "suppress diagnostic logs enabled",
setup: func() {
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "1")
},
cleanup: func() {
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
},
expected: true,
},
{
name: "GO_TEST environment variable set",
setup: func() {
os.Setenv("GO_TEST", "1")
},
cleanup: func() {
os.Unsetenv("GO_TEST")
},
expected: true,
},
{
name: "normal runtime conditions",
setup: func() {
// Disable runtime stack check to test fallback behavior
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", "1")
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "")
os.Setenv("GO_TEST", "")
},
cleanup: func() {
os.Unsetenv("DISABLE_RUNTIME_STACK_CHECK")
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
os.Unsetenv("GO_TEST")
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup test environment
tt.setup()
defer tt.cleanup()
result := IsTestMode()
// Note: Some test conditions may still return true due to runtime.Stack
// detecting testing context, so we check the expected behavior when possible
if tt.name == "suppress diagnostic logs enabled" || tt.name == "GO_TEST environment variable set" {
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
}
})
}
// Test that IsTestMode() returns true when called from a test context
// (which it should, since we're in a test right now)
result := IsTestMode()
if !result {
t.Log("Note: IsTestMode() returned false in test context, which may be expected depending on runtime conditions")
}
}
func TestIsTestModeEdgeCases(t *testing.T) {
// Test with various environment variable combinations
tests := []struct {
name string
env map[string]string
}{
{
name: "all env vars empty",
env: map[string]string{
"SUPPRESS_DIAGNOSTIC_LOGS": "",
"GO_TEST": "",
"DISABLE_RUNTIME_STACK_CHECK": "",
},
},
{
name: "mixed env vars",
env: map[string]string{
"SUPPRESS_DIAGNOSTIC_LOGS": "0",
"GO_TEST": "true",
"DISABLE_RUNTIME_STACK_CHECK": "1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save original environment
original := make(map[string]string)
for key := range tt.env {
original[key] = os.Getenv(key)
}
// Set test environment
for key, value := range tt.env {
os.Setenv(key, value)
}
// Test IsTestMode (result may vary based on runtime conditions)
result := IsTestMode()
_ = result // We just want to ensure it doesn't panic
// Restore original environment
for key, value := range original {
if value == "" {
os.Unsetenv(key)
} else {
os.Setenv(key, value)
}
}
})
}
}
func TestIsTestModeDetectionMethods(t *testing.T) {
// Test that calling IsTestMode in a test context returns true
// This should cover most of the function branches since we're in a test
result := IsTestMode()
// In a test context, IsTestMode should return true
if !result {
t.Log("IsTestMode returned false in test context - this may be due to environment settings")
}
// Test with explicit environment manipulation to force different paths
originalSuppressDiag := os.Getenv("SUPPRESS_DIAGNOSTIC_LOGS")
originalGoTest := os.Getenv("GO_TEST")
originalDisableStack := os.Getenv("DISABLE_RUNTIME_STACK_CHECK")
defer func() {
// Restore original environment
if originalSuppressDiag == "" {
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
} else {
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", originalSuppressDiag)
}
if originalGoTest == "" {
os.Unsetenv("GO_TEST")
} else {
os.Setenv("GO_TEST", originalGoTest)
}
if originalDisableStack == "" {
os.Unsetenv("DISABLE_RUNTIME_STACK_CHECK")
} else {
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", originalDisableStack)
}
}()
// Test various combinations to exercise different code paths
testCases := []struct {
name string
suppressDiag string
goTest string
disableStack string
expectTrue bool
}{
{
name: "suppress_diagnostic_logs_1",
suppressDiag: "1",
goTest: "",
disableStack: "",
expectTrue: true,
},
{
name: "go_test_1",
suppressDiag: "",
goTest: "1",
disableStack: "",
expectTrue: true,
},
{
name: "runtime_detection_allowed",
suppressDiag: "",
goTest: "",
disableStack: "",
expectTrue: true, // Should detect test context from runtime stack
},
{
name: "runtime_detection_disabled",
suppressDiag: "",
goTest: "",
disableStack: "1",
expectTrue: false, // May still be true due to os.Args detection
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", tc.suppressDiag)
os.Setenv("GO_TEST", tc.goTest)
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", tc.disableStack)
result := IsTestMode()
// For environment variable cases, we can assert the expected result
if tc.name == "suppress_diagnostic_logs_1" || tc.name == "go_test_1" {
if result != tc.expectTrue {
t.Errorf("Expected %v, got %v for case %s", tc.expectTrue, result, tc.name)
}
}
// For runtime detection cases, result may vary based on actual runtime conditions
})
}
}
func TestUtilsPackageComplete(t *testing.T) {
// Test edge cases to improve coverage
// Test CreateStringMap with empty slice
emptyResult := CreateStringMap([]string{})
if len(emptyResult) != 0 {
t.Errorf("Expected empty map, got %v", emptyResult)
}
// Test CreateCaseInsensitiveStringMap with empty slice
emptyInsensitiveResult := CreateCaseInsensitiveStringMap([]string{})
if len(emptyInsensitiveResult) != 0 {
t.Errorf("Expected empty map, got %v", emptyInsensitiveResult)
}
// Test DeduplicateScopes with empty slice
emptyScopes := DeduplicateScopes([]string{})
if len(emptyScopes) != 0 {
t.Errorf("Expected empty slice, got %v", emptyScopes)
}
// Test MergeScopes with nil slices
nilResult := MergeScopes(nil, nil)
if len(nilResult) != 0 {
t.Errorf("Expected empty slice, got %v", nilResult)
}
// Test KeysFromMap with empty map
emptyMapKeys := KeysFromMap(map[string]struct{}{})
if len(emptyMapKeys) != 0 {
t.Errorf("Expected empty slice, got %v", emptyMapKeys)
}
// Test BuildFullURL with empty values
emptyURL := BuildFullURL("", "", "")
expected := "://"
if emptyURL != expected {
t.Errorf("Expected '%s', got '%s'", expected, emptyURL)
}
}
func TestIsTestModeOsArgsDetection(t *testing.T) {
// Save original os.Args
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
// Test with different os.Args[0] values that should trigger test mode
testCases := []struct {
name string
args0 string
expected bool
}{
{
name: "Binary with .test suffix",
args0: "/path/to/myapp.test",
expected: true,
},
{
name: "Binary with go_build_ prefix",
args0: "/tmp/go_build_myapp",
expected: true,
},
{
name: "Binary with test in name",
args0: "/path/to/test_binary",
expected: true,
},
{
name: "Binary with __debug_bin",
args0: "/path/to/__debug_bin123",
expected: true,
},
{
name: "Regular binary name",
args0: "/path/to/myapp",
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up environment to avoid interference from other detection methods
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "")
os.Setenv("GO_TEST", "")
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", "1") // Disable runtime stack check
defer func() {
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
os.Unsetenv("GO_TEST")
os.Unsetenv("DISABLE_RUNTIME_STACK_CHECK")
}()
// Set os.Args
os.Args = []string{tc.args0}
result := IsTestMode()
if result != tc.expected {
t.Errorf("For args[0] = '%s': expected %v, got %v", tc.args0, tc.expected, result)
}
})
}
}
func TestIsTestModeArgsFlagDetection(t *testing.T) {
// Save original os.Args
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
testCases := []struct {
name string
args []string
expected bool
}{
{
name: "Args contain -test flag",
args: []string{"/path/to/app", "-test.v", "true"},
expected: true,
},
{
name: "Args contain -test.timeout",
args: []string{"/path/to/app", "-test.timeout", "30s"},
expected: true,
},
{
name: "Args without test flags",
args: []string{"/path/to/app", "-verbose", "-config", "file.conf"},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up environment to avoid interference
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "")
os.Setenv("GO_TEST", "")
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", "1")
defer func() {
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
os.Unsetenv("GO_TEST")
os.Unsetenv("DISABLE_RUNTIME_STACK_CHECK")
}()
// Ensure args[0] doesn't trigger detection by itself
if len(tc.args) > 0 {
tc.args[0] = "/regular/app/name"
}
os.Args = tc.args
result := IsTestMode()
if result != tc.expected {
t.Errorf("For args = %v: expected %v, got %v", tc.args, tc.expected, result)
}
})
}
}
func TestIsTestModeRuntimeCompiler(t *testing.T) {
// This test verifies that the runtime.Compiler check works
// We can't easily change runtime.Compiler, but we can test the logic path
// Set up environment to isolate this test
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "")
os.Setenv("GO_TEST", "")
os.Setenv("DISABLE_RUNTIME_STACK_CHECK", "1")
defer func() {
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
os.Unsetenv("GO_TEST")
os.Unsetenv("DISABLE_RUNTIME_STACK_CHECK")
}()
// Test with args that should trigger test mode when runtime.Compiler == "gc"
os.Args = []string{"some_test_binary", "arg1"}
result := IsTestMode()
// Since runtime.Compiler is "gc" in most cases and os.Args[0] contains "test",
// this should return true
if !result {
t.Log("Note: This test may vary depending on the actual runtime.Compiler value")
}
}
func TestIsTestModeYaegiCompiler(t *testing.T) {
// Test the yaegi compiler detection
// We can't change runtime.Compiler directly, but we can verify the GO_TEST path
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
// Test that GO_TEST=1 triggers test mode regardless of other conditions
os.Setenv("GO_TEST", "1")
os.Setenv("SUPPRESS_DIAGNOSTIC_LOGS", "")
defer func() {
os.Unsetenv("GO_TEST")
os.Unsetenv("SUPPRESS_DIAGNOSTIC_LOGS")
}()
// Use a non-test-like binary name
os.Args = []string{"/regular/binary/name"}
result := IsTestMode()
if !result {
t.Error("Expected true when GO_TEST=1 is set")
}
}