package traefikoidc import ( "encoding/json" "fmt" "net" "net/http" "os" "runtime" "runtime/debug" "testing" "time" ) // isRaceDetectorEnabled returns true if the Go race detector is enabled. // This is determined by checking the build info for the race build tag. func isRaceDetectorEnabled() bool { info, ok := debug.ReadBuildInfo() if !ok { return false } for _, setting := range info.Settings { if setting.Key == "-race" && setting.Value == "true" { return true } } // Alternative method: check if GORACE environment variable is set return os.Getenv("GORACE") != "" } func TestProfilingManager(t *testing.T) { logger := NewLogger("debug") pm := NewProfilingManager(logger) // Test taking a snapshot snapshot, err := pm.TakeSnapshot() if err != nil { t.Fatalf("Failed to take snapshot: %v", err) } if snapshot == nil { t.Fatal("Snapshot is nil") } if snapshot.RuntimeStats.Alloc == 0 { t.Error("Runtime stats Alloc should not be zero") } if snapshot.Timestamp.IsZero() { t.Error("Snapshot timestamp should not be zero") } } func TestMemoryTestOrchestrator(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } logger := NewLogger("debug") config := LeakDetectionConfig{ EnableLeakDetection: true, LeakThresholdMB: 10, } mto := NewMemoryTestOrchestrator(config, logger) // Test registering a component sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", "", 0, logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } profiler := NewSessionPoolProfiler(sessionManager, logger) mto.RegisterComponent("session_pool", profiler) // Test getting leak analysis (should return false initially since no checks have been performed) _, exists := mto.GetLeakAnalysis("session_pool") if exists { t.Error("Should not have leak analysis before any checks are performed") } // Perform a manual leak check baseline, err := profiler.TakeSnapshot() if err != nil { t.Fatalf("Failed to take baseline snapshot: %v", err) } time.Sleep(10 * time.Millisecond) // Small delay // Manually trigger leak check with baseline baselineSnapshots := make(map[string]*MemorySnapshot) baselineSnapshots["session_pool"] = baseline mto.performLeakCheck(baselineSnapshots) // Now test getting leak analysis analysis, exists := mto.GetLeakAnalysis("session_pool") if !exists { t.Error("Should have leak analysis after performing checks") } if analysis == nil { t.Error("Leak analysis should not be nil after checks") } } func TestComponentProfilers(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } logger := NewLogger("debug") // Test Session Pool Profiler sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", "", 0, logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } spp := NewSessionPoolProfiler(sessionManager, logger) snapshot, err := spp.TakeSnapshot() if err != nil { t.Fatalf("Failed to take session pool snapshot: %v", err) } if snapshot == nil { t.Fatal("Session pool snapshot is nil") } // Test Cache Memory Profiler cache := NewCache() cmp := NewCacheMemoryProfiler(cache, logger) snapshot, err = cmp.TakeSnapshot() if err != nil { t.Fatalf("Failed to take cache snapshot: %v", err) } if snapshot == nil { t.Fatal("Cache snapshot is nil") } // Test HTTP Client Profiler httpClient := CreateDefaultHTTPClient() hcp := NewHTTPClientProfiler(httpClient, logger) snapshot, err = hcp.TakeSnapshot() if err != nil { t.Fatalf("Failed to take HTTP client snapshot: %v", err) } if snapshot == nil { t.Fatal("HTTP client snapshot is nil") } // Token Compression Profiler removed - use internal/pool statistics instead t.Log("Token compression profiler deprecated - use internal/pool stats") } func TestLeakAnalysis(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } logger := NewLogger("debug") pm := NewProfilingManager(logger) // Create baseline snapshot baseline, err := pm.TakeSnapshot() if err != nil { t.Fatalf("Failed to create baseline: %v", err) } // Wait a bit and create current snapshot time.Sleep(10 * time.Millisecond) current, err := pm.TakeSnapshot() if err != nil { t.Fatalf("Failed to create current snapshot: %v", err) } // Test leak analysis analysis := pm.AnalyzeLeaks(baseline, current) if analysis == nil { t.Fatal("Leak analysis is nil") } // Analysis should not have leaks for normal operation if analysis.HasLeak { t.Logf("Leak detected: %s", analysis.LeakDescription) // This is acceptable as the test environment may have varying memory usage } } func TestGlobalInstances(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } // Test global profiling manager gpm := GetGlobalProfilingManager() if gpm == nil { t.Fatal("Global profiling manager is nil") } // Test global test orchestrator gto := GetGlobalTestOrchestrator() if gto == nil { t.Fatal("Global test orchestrator is nil") } // Test that they're singletons gpm2 := GetGlobalProfilingManager() if gpm != gpm2 { t.Error("Global profiling manager should be singleton") } gto2 := GetGlobalTestOrchestrator() if gto != gto2 { t.Error("Global test orchestrator should be singleton") } } func TestProfilingConfig(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } config := ProfilingConfig{ EnableHeapProfiling: true, EnableGoroutineProfiling: true, SnapshotInterval: 30 * time.Second, LeakThresholdMB: 50, MaxSnapshots: 100, EnableContinuousMonitoring: true, MonitoringInterval: 60 * time.Second, } if !config.EnableHeapProfiling { t.Error("Heap profiling should be enabled") } if !config.EnableGoroutineProfiling { t.Error("Goroutine profiling should be enabled") } if config.LeakThresholdMB != 50 { t.Errorf("Expected leak threshold 50, got %d", config.LeakThresholdMB) } } func TestLeakDetectionConfig(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode") } config := LeakDetectionConfig{ EnableLeakDetection: true, LeakThresholdMB: 50, GoroutineLeakThreshold: 10, SessionPoolThreshold: 100, CacheMemoryThreshold: 20 * 1024 * 1024, HTTPClientThreshold: 50, TokenCompressionThreshold: 2 * 1024 * 1024, } if !config.EnableLeakDetection { t.Error("Leak detection should be enabled") } if config.LeakThresholdMB != 50 { t.Errorf("Expected leak threshold 50, got %d", config.LeakThresholdMB) } if config.CacheMemoryThreshold != 20*1024*1024 { t.Errorf("Expected cache threshold 20MB, got %d", config.CacheMemoryThreshold) } } // ProviderMetadataProfiler monitors provider metadata fetching and caching operations type ProviderMetadataProfiler struct { metadataCache *MetadataCache httpClient *http.Client logger *Logger providerURL string } // NewProviderMetadataProfiler creates a new provider metadata profiler func NewProviderMetadataProfiler(metadataCache *MetadataCache, httpClient *http.Client, providerURL string, logger *Logger) *ProviderMetadataProfiler { if logger == nil { logger = newNoOpLogger() } return &ProviderMetadataProfiler{ metadataCache: metadataCache, httpClient: httpClient, providerURL: providerURL, logger: logger, } } // TakeSnapshot captures current memory statistics for metadata operations func (pmp *ProviderMetadataProfiler) TakeSnapshot() (*MemorySnapshot, error) { snapshot := &MemorySnapshot{ Timestamp: time.Now(), CustomMetrics: make(map[string]interface{}), } // Capture runtime memory statistics runtime.ReadMemStats(&snapshot.RuntimeStats) // Add metadata-specific metrics snapshot.CustomMetrics["metadata_cache_size"] = 1 // Placeholder for cache size snapshot.CustomMetrics["metadata_fetch_count"] = 0 // Placeholder for fetch count snapshot.CustomMetrics["background_goroutines"] = runtime.NumGoroutine() return snapshot, nil } // StartProfiling begins profiling (no-op for metadata profiler) func (pmp *ProviderMetadataProfiler) StartProfiling(config ProfilingConfig) error { return nil } // StopProfiling ends profiling func (pmp *ProviderMetadataProfiler) StopProfiling() (*MemorySnapshot, error) { return pmp.TakeSnapshot() } // GetCurrentStats returns current memory statistics func (pmp *ProviderMetadataProfiler) GetCurrentStats() *runtime.MemStats { stats := &runtime.MemStats{} runtime.ReadMemStats(stats) return stats } // AnalyzeLeaks analyzes metadata operations for memory leaks func (pmp *ProviderMetadataProfiler) AnalyzeLeaks(baseline, current *MemorySnapshot) *LeakAnalysis { analysis := &LeakAnalysis{ SuspectedLeaks: make([]string, 0), Recommendations: make([]string, 0), } if baseline == nil || current == nil { analysis.LeakDescription = "Insufficient metadata data" return analysis } // Check for memory leaks memoryIncrease := current.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc if memoryIncrease > 5*1024*1024 { // 5MB threshold for metadata operations analysis.HasLeak = true analysis.SuspectedLeaks = append(analysis.SuspectedLeaks, "Metadata operations memory usage increased significantly") analysis.Recommendations = append(analysis.Recommendations, "Check for metadata cache not being cleaned up properly") } // Check for goroutine leaks goroutineIncrease := current.CustomMetrics["background_goroutines"].(int) - baseline.CustomMetrics["background_goroutines"].(int) if goroutineIncrease > 2 { // Allow some variance analysis.HasLeak = true analysis.SuspectedLeaks = append(analysis.SuspectedLeaks, fmt.Sprintf("Goroutine count increased by %d during metadata operations", goroutineIncrease)) analysis.Recommendations = append(analysis.Recommendations, "Check for background goroutines not being cleaned up") } return analysis } // TestProviderMetadataMemoryLeakDetection tests for memory leaks in provider metadata operations func TestProviderMetadataMemoryLeakDetection(t *testing.T) { if testing.Short() { t.Skip("Skipping provider metadata memory leak detection test in short mode") } // Reset singleton cache manager to ensure clean state ResetUniversalCacheManagerForTesting() defer ResetUniversalCacheManagerForTesting() // Clean up after test logger := NewLogger("debug") strictMode := os.Getenv("STRICT_MEMORY_TEST") == "true" if strictMode { t.Log("Running in strict memory test mode - will fail on detected leaks") } else { t.Log("Running in lenient memory test mode - will log warnings instead of failing") } config := LeakDetectionConfig{ EnableLeakDetection: true, LeakThresholdMB: 10, } mto := NewMemoryTestOrchestrator(config, logger) // Create mock HTTP server for metadata endpoint with failure simulation requestCount := 0 serverFailures := 0 mockServer := &http.Server{ Addr: "localhost:0", // Let system assign port Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ if r.URL.Path == "/.well-known/openid-configuration" { // Simulate occasional failures to test cache extension if requestCount%4 == 0 { // Fail every 4th request serverFailures++ w.WriteHeader(http.StatusInternalServerError) return } metadata := ProviderMetadata{ Issuer: "https://mock-provider.com", AuthURL: "https://mock-provider.com/auth", TokenURL: "https://mock-provider.com/token", JWKSURL: "https://mock-provider.com/jwks", RevokeURL: "https://mock-provider.com/revoke", EndSessionURL: "https://mock-provider.com/logout", } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "max-age=3600") // 1 hour cache hint json.NewEncoder(w).Encode(metadata) } else { http.NotFound(w, r) } }), } // Start mock server listener, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Failed to create listener: %v", err) } go mockServer.Serve(listener) defer mockServer.Close() providerURL := fmt.Sprintf("http://%s", listener.Addr().String()) httpClient := CreateDefaultHTTPClient() // Create metadata cache metadataCache := NewMetadataCacheWithLogger(nil, logger) // Create profiler profiler := NewProviderMetadataProfiler(metadataCache, httpClient, providerURL, logger) mto.RegisterComponent("provider_metadata", profiler) // Take initial baseline baseline, err := profiler.TakeSnapshot() if err != nil { t.Fatalf("Failed to take baseline snapshot: %v", err) } initialGoroutines := runtime.NumGoroutine() // Phase 1: Simulate periodic metadata fetching with some failures t.Log("Phase 1: Testing periodic fetching with occasional failures...") for i := 0; i < 20; i++ { _, err := metadataCache.GetMetadata(providerURL, httpClient, logger) if err != nil { t.Logf("Metadata fetch %d failed (expected for cache extension testing): %v", i+1, err) } else { t.Logf("Metadata fetch %d succeeded", i+1) } time.Sleep(100 * time.Millisecond) } // Wait for background cleanup (normally every 5 minutes) time.Sleep(300 * time.Millisecond) // Take intermediate snapshot intermediate, err := profiler.TakeSnapshot() if err != nil { t.Fatalf("Failed to take intermediate snapshot: %v", err) } // Phase 2: Continue with more fetches to test sustained operation // Adjust iterations based on race detector presence to avoid timeouts var phase2Iterations int var sleepDuration time.Duration if isRaceDetectorEnabled() { // With race detector: reduce iterations significantly to stay well within timeout phase2Iterations = 100 sleepDuration = 100 * time.Millisecond // Slightly longer sleep to reduce CPU contention t.Log("Phase 2: Testing sustained operation with 100 iterations (race detector enabled)...") } else { // Without race detector: use original values for thorough testing phase2Iterations = 1000 sleepDuration = 50 * time.Millisecond t.Log("Phase 2: Testing sustained operation with 1000 iterations...") } for i := 20; i < 20+phase2Iterations; i++ { _, err := metadataCache.GetMetadata(providerURL, httpClient, logger) if err != nil { t.Logf("Metadata fetch %d failed: %v", i+1, err) } time.Sleep(sleepDuration) } // Take final snapshot current, err := profiler.TakeSnapshot() if err != nil { t.Fatalf("Failed to take current snapshot: %v", err) } finalGoroutines := runtime.NumGoroutine() // Analyze for leaks analysis := profiler.AnalyzeLeaks(baseline, current) // Assertions for memory leaks if analysis.HasLeak { if strictMode { t.Errorf("Memory leak detected in provider metadata operations: %s", analysis.LeakDescription) for _, leak := range analysis.SuspectedLeaks { t.Errorf("Suspected leak: %s", leak) } } else { t.Logf("Memory leak warning in provider metadata operations: %s", analysis.LeakDescription) for _, leak := range analysis.SuspectedLeaks { t.Logf("Suspected leak: %s", leak) } } for _, rec := range analysis.Recommendations { t.Logf("Recommendation: %s", rec) } } // Check total memory growth totalMemoryIncrease := current.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc if totalMemoryIncrease > 20*1024*1024 { // 20MB threshold for entire test if strictMode { t.Errorf("Total memory usage increased by %.2f MB during metadata operations", float64(totalMemoryIncrease)/(1024*1024)) } else { t.Logf("Total memory usage increased by %.2f MB during metadata operations", float64(totalMemoryIncrease)/(1024*1024)) } } // Check for gradual memory growth patterns intermediateMemoryIncrease := intermediate.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc if intermediateMemoryIncrease > 10*1024*1024 { // 10MB threshold for first phase if strictMode { t.Errorf("Memory usage increased by %.2f MB during first phase of metadata operations", float64(intermediateMemoryIncrease)/(1024*1024)) } else { t.Logf("Memory usage increased by %.2f MB during first phase of metadata operations", float64(intermediateMemoryIncrease)/(1024*1024)) } } // Check goroutine count stability goroutineIncrease := finalGoroutines - initialGoroutines if goroutineIncrease > 5 { // Allow some variance for test environment if strictMode { t.Errorf("Goroutine count increased by %d during metadata operations (initial: %d, final: %d)", goroutineIncrease, initialGoroutines, finalGoroutines) } else { t.Logf("Goroutine count increased by %d during metadata operations (initial: %d, final: %d)", goroutineIncrease, initialGoroutines, finalGoroutines) } } // Phase 3: Test cache extension behavior on persistent failures t.Log("Phase 3: Testing cache extension on persistent failures...") // Stop mock server to simulate provider unavailability mockServer.Close() // Try multiple fetches after server shutdown postShutdownFailures := 0 for i := 0; i < 5; i++ { _, err = metadataCache.GetMetadata(providerURL, httpClient, logger) if err != nil { postShutdownFailures++ t.Logf("Expected failure %d after server shutdown: %v", i+1, err) } else { t.Logf("Unexpected success %d after server shutdown - cache extension working", i+1) } time.Sleep(200 * time.Millisecond) } if postShutdownFailures == 0 { if strictMode { t.Error("Expected some metadata fetches to fail after server shutdown") } else { t.Log("Warning: No metadata fetches failed after server shutdown - cache extension may not be working as expected") } } // Phase 4: Test background goroutine lifecycle and cleanup t.Log("Phase 4: Testing background goroutine lifecycle...") // Wait longer to allow background cleanup to run time.Sleep(GetTestDuration(1 * time.Second)) // Take final snapshot after cleanup finalAfterCleanup, err := profiler.TakeSnapshot() if err != nil { t.Fatalf("Failed to take final snapshot after cleanup: %v", err) } // Check if memory decreased after cleanup if finalAfterCleanup.RuntimeStats.Alloc < current.RuntimeStats.Alloc { memoryDecrease := current.RuntimeStats.Alloc - finalAfterCleanup.RuntimeStats.Alloc t.Logf("Memory decreased by %.2f MB after cleanup phase", float64(memoryDecrease)/(1024*1024)) } // Clean up resources // The cache manager cleanup is handled by the defer at the beginning of the test t.Logf("Test completed: %d total requests, %d server failures, %d post-shutdown failures", requestCount, serverFailures, postShutdownFailures) t.Logf("Memory usage: baseline=%.2f MB, intermediate=%.2f MB, final=%.2f MB", float64(baseline.RuntimeStats.Alloc)/(1024*1024), float64(intermediate.RuntimeStats.Alloc)/(1024*1024), float64(current.RuntimeStats.Alloc)/(1024*1024)) } // TestMemoryPoolLeakDetection tests for memory leaks in memory pool operations func TestMemoryPoolLeakDetection(t *testing.T) { t.Skip("Deprecated - memory pool profilers removed. Use internal/pool statistics instead") }