package traefikoidc import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "runtime" "runtime/debug" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================================= // Test Framework and Types // ============================================================================= // MemoryLeakFixesTestSuite provides comprehensive memory leak testing using unified infrastructure type MemoryLeakFixesTestSuite struct { runner *TestSuiteRunner factory *TestDataFactory edgeGen *EdgeCaseGenerator perfTest *PerformanceTestHelper logger *Logger } // NewMemoryLeakFixesTestSuite creates a new test suite for memory leak fixes func NewMemoryLeakFixesTestSuite() *MemoryLeakFixesTestSuite { return &MemoryLeakFixesTestSuite{ runner: NewTestSuiteRunner(), factory: NewTestDataFactory(), edgeGen: NewEdgeCaseGenerator(), perfTest: NewPerformanceTestHelper(), logger: GetSingletonNoOpLogger(), } } // MemoryTestCase defines a memory leak test scenario type MemoryTestCase struct { setup func(*MemoryTestFramework) error execute func(*MemoryTestFramework) error validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats) cleanup func(*MemoryTestFramework) error name string component string scenario string iterations int concurrency int } // MemoryTestFramework provides common test infrastructure for memory tests type MemoryTestFramework struct { cache CacheInterface ctx context.Context t *testing.T plugin *TraefikOidc logger *Logger cancel context.CancelFunc servers []*httptest.Server configs []*Config } // NewMemoryTestFramework creates a new test framework instance func NewMemoryTestFramework(t *testing.T) *MemoryTestFramework { ctx, cancel := context.WithCancel(context.Background()) return &MemoryTestFramework{ t: t, logger: NewLogger("debug"), ctx: ctx, cancel: cancel, servers: make([]*httptest.Server, 0), configs: make([]*Config, 0), } } // Cleanup releases all framework resources func (tf *MemoryTestFramework) Cleanup() { if tf.cancel != nil { tf.cancel() } if tf.plugin != nil { tf.plugin.Close() } if tf.cache != nil { tf.cache.Close() } for _, server := range tf.servers { server.Close() } } // ConsolidatedMemorySnapshot captures memory statistics at a point in time type ConsolidatedMemorySnapshot struct { Timestamp time.Time Description string Alloc uint64 TotalAlloc uint64 Sys uint64 Goroutines int NumGC uint32 } // VerifyNoGoroutineLeaks checks for goroutine leaks func VerifyNoGoroutineLeaks(t *testing.T, baseline int, tolerance int, description string) { time.Sleep(100 * time.Millisecond) current := runtime.NumGoroutine() leaked := current - baseline if leaked > tolerance { t.Errorf("Goroutine leak detected in %s: baseline=%d, current=%d, leaked=%d (tolerance=%d)", description, baseline, current, leaked, tolerance) } } // TakeConsolidatedMemorySnapshot captures current memory state func TakeConsolidatedMemorySnapshot(description string) ConsolidatedMemorySnapshot { runtime.GC() runtime.GC() debug.FreeOSMemory() var m runtime.MemStats runtime.ReadMemStats(&m) return ConsolidatedMemorySnapshot{ Timestamp: time.Now(), Alloc: m.Alloc, TotalAlloc: m.TotalAlloc, Sys: m.Sys, NumGC: m.NumGC, Goroutines: runtime.NumGoroutine(), Description: description, } } // ============================================================================= // Optimized Cache Lifecycle Tests // ============================================================================= func TestOptimizedCacheLifecycleManagement(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Basic cache lifecycle", Description: "Test basic cache creation, use, and cleanup", Operation: func() error { cache := NewOptimizedCache() if cache == nil { return fmt.Errorf("cache creation failed") } cache.Set("test", "value", time.Minute) val, found := cache.Get("test") if !found || val != "value" { return fmt.Errorf("cache operation failed") } cache.Close() return nil }, Iterations: 10, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, { Name: "Cache with multiple entries", Description: "Test cache with multiple entries and cleanup", Operation: func() error { cache := NewOptimizedCache() defer cache.Close() for i := 0; i < 100; i++ { key := fmt.Sprintf("key-%d", i) cache.Set(key, fmt.Sprintf("value-%d", i), time.Minute) } for i := 0; i < 100; i++ { key := fmt.Sprintf("key-%d", i) _, found := cache.Get(key) if !found { return fmt.Errorf("cache entry missing: %s", key) } } return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 5.0, GCBetweenRuns: true, Timeout: 15 * time.Second, }, { Name: "Cache with expiring entries", Description: "Test cache cleanup of expired entries", Operation: func() error { cache := NewOptimizedCache() defer cache.Close() for i := 0; i < 50; i++ { key := fmt.Sprintf("short-key-%d", i) cache.Set(key, "short-value", 50*time.Millisecond) } time.Sleep(GetTestDuration(100 * time.Millisecond)) for i := 0; i < 50; i++ { key := fmt.Sprintf("cleanup-key-%d", i) cache.Set(key, "new-value", time.Minute) } return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 2.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Chunk Manager Tests // ============================================================================= func TestChunkManagerBoundedSessions(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []TableTestCase{ { Name: "Basic chunk manager initialization", Description: "Verify chunk manager is properly initialized with bounds", Setup: func(t *testing.T) error { return nil }, Teardown: func(t *testing.T) error { return nil }, }, { Name: "Session limits enforcement", Description: "Verify session limits are properly enforced", Setup: func(t *testing.T) error { return nil }, Teardown: func(t *testing.T) error { return nil }, }, } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { if test.Setup != nil { err := test.Setup(t) require.NoError(t, err) } if test.Teardown != nil { defer func() { err := test.Teardown(t) assert.NoError(t, err) }() } logger := GetSingletonNoOpLogger() cm := NewChunkManager(logger) assert.Equal(t, 1000, cm.maxSessions) assert.Equal(t, 24*time.Hour, cm.sessionTTL) assert.NotNil(t, cm.sessionMap) assert.Equal(t, 0, len(cm.sessionMap)) }) } leakTests := []MemoryLeakTestCase{ { Name: "Session map memory management", Description: "Verify session map doesn't leak memory with bounded sessions", Operation: func() error { logger := GetSingletonNoOpLogger() cm := NewChunkManager(logger) if cm == nil { return fmt.Errorf("chunk manager creation failed") } for i := 0; i < 100; i++ { sessionID := fmt.Sprintf("session-%d", i) _ = sessionID } return nil }, Iterations: 10, MaxGoroutineGrowth: 1, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 5 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, leakTests) } // ============================================================================= // Provider Registry Tests // ============================================================================= func TestProviderRegistryBoundedCache(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []TableTestCase{ { Name: "Registry bounds validation", Description: "Validate registry bounds pattern for future implementation", Input: 1000, Expected: true, Setup: func(t *testing.T) error { return nil }, Teardown: func(t *testing.T) error { return nil }, }, } edgeCases := suite.edgeGen.GenerateIntegerEdgeCases() for _, maxSize := range edgeCases { if maxSize > 0 { tests = append(tests, TableTestCase{ Name: fmt.Sprintf("Registry bounds edge case - size %d", maxSize), Description: "Test registry bounds with edge case values", Input: maxSize, Expected: maxSize > 0, }) } } suite.runner.RunTests(t, tests) leakTests := []MemoryLeakTestCase{ { Name: "Provider registry memory pattern", Description: "Test memory pattern for bounded provider registry", Operation: func() error { maxCacheSize := 1000 cacheCount := 0 cache := make(map[string]interface{}) for i := 0; i < maxCacheSize*2; i++ { key := fmt.Sprintf("provider-%d", i) if cacheCount < maxCacheSize { cache[key] = fmt.Sprintf("config-%d", i) cacheCount++ } } if len(cache) > maxCacheSize { return fmt.Errorf("cache exceeded bounds: %d > %d", len(cache), maxCacheSize) } return nil }, Iterations: 5, MaxGoroutineGrowth: 0, MaxMemoryGrowthMB: 2.0, GCBetweenRuns: true, Timeout: 5 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, leakTests) } // ============================================================================= // Error Recovery Lifecycle Tests // ============================================================================= func TestErrorRecoveryLifecycleManagement(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Basic background task lifecycle", Description: "Test background task creation, execution, and cleanup", Operation: func() error { logger := GetSingletonNoOpLogger() taskFunc := func() {} task := NewBackgroundTask("test-health-check", 100*time.Millisecond, taskFunc, logger) task.Start() time.Sleep(GetTestDuration(50 * time.Millisecond)) task.Stop() time.Sleep(GetTestDuration(200 * time.Millisecond)) return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, { Name: "Multiple background tasks", Description: "Test multiple background tasks lifecycle management", Operation: func() error { logger := GetSingletonNoOpLogger() tasks := make([]*BackgroundTask, 0, 3) for i := 0; i < 3; i++ { taskName := fmt.Sprintf("test-task-%d", i) taskFunc := func() {} task := NewBackgroundTask(taskName, 50*time.Millisecond, taskFunc, logger) tasks = append(tasks, task) task.Start() } time.Sleep(GetTestDuration(100 * time.Millisecond)) for _, task := range tasks { task.Stop() } time.Sleep(GetTestDuration(200 * time.Millisecond)) return nil }, Iterations: 3, MaxGoroutineGrowth: 3, MaxMemoryGrowthMB: 1.5, GCBetweenRuns: true, Timeout: 15 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Background Task Shutdown Tests // ============================================================================= func TestBackgroundTaskProperShutdown(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Basic background task shutdown", Description: "Test basic background task execution and proper shutdown", Operation: func() error { var wg sync.WaitGroup logger := GetSingletonNoOpLogger() callCount := 0 taskFunc := func() { callCount++ } task := NewBackgroundTask("test-task", 50*time.Millisecond, taskFunc, logger, &wg) task.Start() time.Sleep(GetTestDuration(150 * time.Millisecond)) if callCount == 0 { return fmt.Errorf("task should have executed at least once") } task.Stop() wg.Wait() time.Sleep(GetTestDuration(100 * time.Millisecond)) return nil }, Iterations: 10, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 15 * time.Second, }, { Name: "High frequency background task", Description: "Test background task with high execution frequency", Operation: func() error { var wg sync.WaitGroup logger := GetSingletonNoOpLogger() callCount := 0 taskFunc := func() { callCount++ } task := NewBackgroundTask("high-freq-task", 10*time.Millisecond, taskFunc, logger, &wg) task.Start() time.Sleep(GetTestDuration(100 * time.Millisecond)) task.Stop() wg.Wait() time.Sleep(GetTestDuration(50 * time.Millisecond)) return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Metadata Cache Tests // ============================================================================= func TestMetadataCacheResourceCleanup(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Basic metadata cache cleanup", Description: "Test metadata cache creation and cleanup", Operation: func() error { var wg sync.WaitGroup cache := NewMetadataCache(&wg) if cache == nil { return fmt.Errorf("cache creation failed") } time.Sleep(GetTestDuration(50 * time.Millisecond)) cache.Close() time.Sleep(GetTestDuration(100 * time.Millisecond)) return nil }, Iterations: 10, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, { Name: "Multiple metadata caches", Description: "Test multiple metadata cache instances cleanup", Operation: func() error { var wg sync.WaitGroup caches := make([]*MetadataCache, 0, 3) for i := 0; i < 3; i++ { cache := NewMetadataCache(&wg) if cache == nil { return fmt.Errorf("cache creation failed for instance %d", i) } caches = append(caches, cache) } time.Sleep(GetTestDuration(50 * time.Millisecond)) for _, cache := range caches { cache.Close() } time.Sleep(GetTestDuration(100 * time.Millisecond)) return nil }, Iterations: 3, MaxGoroutineGrowth: 3, MaxMemoryGrowthMB: 2.0, GCBetweenRuns: true, Timeout: 15 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Secure Data Cleanup Tests // ============================================================================= func TestSecureDataCleanup(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []TableTestCase{ { Name: "Basic sensitive data cleanup", Description: "Test basic sensitive data storage and cleanup", Input: []byte("secret-token-data"), Expected: true, Setup: func(t *testing.T) error { return nil }, Teardown: func(t *testing.T) error { return nil }, }, } stringEdgeCases := suite.edgeGen.GenerateStringEdgeCases() for i, testString := range stringEdgeCases { if len(testString) > 0 { tests = append(tests, TableTestCase{ Name: fmt.Sprintf("Sensitive data edge case %d", i), Description: "Test secure cleanup with edge case data", Input: []byte(testString), Expected: true, }) } } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { if test.Setup != nil { err := test.Setup(t) require.NoError(t, err) } if test.Teardown != nil { defer func() { err := test.Teardown(t) assert.NoError(t, err) }() } cache := NewOptimizedCache() defer cache.Close() sensitiveData := test.Input.([]byte) cache.Set("token", sensitiveData, time.Minute) val, found := cache.Get("token") assert.True(t, found) assert.Equal(t, sensitiveData, val) cache.Close() }) } leakTests := []MemoryLeakTestCase{ { Name: "Secure data cleanup memory management", Description: "Test memory management for secure data cleanup operations", Operation: func() error { cache := NewOptimizedCache() defer cache.Close() for i := 0; i < 50; i++ { key := fmt.Sprintf("sensitive-key-%d", i) sensitiveData := []byte(fmt.Sprintf("secret-data-%d-%s", i, suite.factory.GenerateRandomString(64))) cache.Set(key, sensitiveData, time.Minute) } for i := 0; i < 50; i++ { key := fmt.Sprintf("sensitive-key-%d", i) _, found := cache.Get(key) if !found { return fmt.Errorf("sensitive data not found for key: %s", key) } } cache.Close() return nil }, Iterations: 5, MaxGoroutineGrowth: 1, MaxMemoryGrowthMB: 2.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, leakTests) } // ============================================================================= // Memory Growth Prevention Tests // ============================================================================= func TestMemoryGrowthPrevention(t *testing.T) { if testing.Short() { t.Skip("Skipping memory growth prevention test in short mode") } config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Multiple cache memory growth prevention", Description: "Test memory growth with multiple cache instances", Operation: func() error { caches := make([]*OptimizedCache, 10) for i := 0; i < 10; i++ { caches[i] = NewOptimizedCache() for j := 0; j < 100; j++ { caches[i].Set(fmt.Sprintf("key-%d-%d", i, j), "value", time.Minute) } } for _, cache := range caches { cache.Close() } runtime.GC() time.Sleep(GetTestDuration(100 * time.Millisecond)) runtime.GC() return nil }, Iterations: 3, MaxGoroutineGrowth: 5, MaxMemoryGrowthMB: 50.0, GCBetweenRuns: true, Timeout: 30 * time.Second, }, { Name: "Large dataset memory growth prevention", Description: "Test memory growth with large datasets", Operation: func() error { cache := NewOptimizedCache() defer cache.Close() for i := 0; i < 1000; i++ { key := fmt.Sprintf("large-key-%d", i) value := suite.factory.GenerateRandomString(1024) cache.Set(key, value, time.Minute) } for i := 0; i < 500; i++ { key := fmt.Sprintf("temp-key-%d", i) cache.Set(key, "temp-value", 10*time.Millisecond) } time.Sleep(GetTestDuration(50 * time.Millisecond)) for i := 0; i < 100; i++ { key := fmt.Sprintf("cleanup-trigger-%d", i) cache.Get(key) } return nil }, Iterations: 2, MaxGoroutineGrowth: 3, MaxMemoryGrowthMB: 100.0, GCBetweenRuns: true, Timeout: 45 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Goroutine Leak Prevention Tests // ============================================================================= func TestGoroutineLeakPrevention(t *testing.T) { if testing.Short() { t.Skip("Skipping goroutine leak prevention test in short mode") } config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "Concurrent cache goroutine management", Description: "Test goroutine management with concurrent cache operations", Operation: func() error { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() cache := NewOptimizedCache() defer cache.Close() for j := 0; j < 10; j++ { cache.Set(fmt.Sprintf("key-%d", j), "value", time.Minute) time.Sleep(time.Millisecond) } }(i) } wg.Wait() time.Sleep(GetTestDuration(500 * time.Millisecond)) runtime.GC() return nil }, Iterations: 3, MaxGoroutineGrowth: 5, MaxMemoryGrowthMB: 10.0, GCBetweenRuns: true, Timeout: 30 * time.Second, }, { Name: "Mixed component goroutine management", Description: "Test goroutine management with mixed component types", Operation: func() error { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { defer wg.Done() cache := NewOptimizedCache() defer cache.Close() cache.Set("mixed-key", "mixed-value", time.Minute) }(i) wg.Add(1) go func(i int) { defer wg.Done() logger := GetSingletonNoOpLogger() taskFunc := func() {} task := NewBackgroundTask(fmt.Sprintf("mixed-task-%d", i), 50*time.Millisecond, taskFunc, logger) task.Start() time.Sleep(GetTestDuration(25 * time.Millisecond)) task.Stop() }(i) wg.Add(1) go func(i int) { defer wg.Done() var localWG sync.WaitGroup cache := NewMetadataCache(&localWG) time.Sleep(GetTestDuration(25 * time.Millisecond)) cache.Close() }(i) } wg.Wait() time.Sleep(GetTestDuration(500 * time.Millisecond)) runtime.GC() return nil }, Iterations: 2, MaxGoroutineGrowth: 8, MaxMemoryGrowthMB: 10.0, GCBetweenRuns: true, Timeout: 30 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Lazy Background Task Tests // ============================================================================= func TestLazyBackgroundTask(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "LazyBackgroundTask delayed start", Description: "Test that lazy background task doesn't start until StartIfNeeded is called", Operation: func() error { logger := GetSingletonNoOpLogger() callCount := 0 taskFunc := func() { callCount++ } task := NewLazyBackgroundTask("lazy-test", 50*time.Millisecond, taskFunc, logger) time.Sleep(GetTestDuration(100 * time.Millisecond)) if callCount != 0 { return fmt.Errorf("task should not have executed before StartIfNeeded") } task.StartIfNeeded() time.Sleep(GetTestDuration(150 * time.Millisecond)) if callCount < 2 { return fmt.Errorf("task should have executed at least twice after starting") } task.Stop() time.Sleep(GetTestDuration(100 * time.Millisecond)) return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, { Name: "LazyBackgroundTask multiple StartIfNeeded calls", Description: "Test that multiple StartIfNeeded calls only start task once", Operation: func() error { logger := GetSingletonNoOpLogger() execCount := 0 taskFunc := func() { execCount++ } task := NewLazyBackgroundTask("lazy-multiple", 50*time.Millisecond, taskFunc, logger) task.StartIfNeeded() task.StartIfNeeded() task.StartIfNeeded() time.Sleep(GetTestDuration(100 * time.Millisecond)) if execCount < 1 { return fmt.Errorf("task should have executed at least once") } if !task.started { return fmt.Errorf("task should be marked as started") } task.Stop() return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 1.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Lazy Cache Tests // ============================================================================= func TestLazyCache(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } suite := NewMemoryLeakFixesTestSuite() tests := []MemoryLeakTestCase{ { Name: "LazyCache basic operations", Description: "Test NewLazyCache with basic cache operations", Operation: func() error { cache := NewLazyCache() if cache == nil { return fmt.Errorf("NewLazyCache returned nil") } cache.Set("key1", "value1", time.Minute) val, found := cache.Get("key1") if !found || val != "value1" { return fmt.Errorf("cache operation failed") } return nil }, Iterations: 10, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 2.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, { Name: "LazyCacheWithLogger operations", Description: "Test NewLazyCacheWithLogger with custom logger", Operation: func() error { logger := GetSingletonNoOpLogger() cache := NewLazyCacheWithLogger(logger) if cache == nil { return fmt.Errorf("NewLazyCacheWithLogger returned nil") } for i := 0; i < 50; i++ { key := fmt.Sprintf("lazy-key-%d", i) cache.Set(key, i, time.Minute) } for i := 0; i < 50; i++ { key := fmt.Sprintf("lazy-key-%d", i) val, found := cache.Get(key) if !found || val != i { return fmt.Errorf("cache value mismatch for %s", key) } } return nil }, Iterations: 5, MaxGoroutineGrowth: 2, MaxMemoryGrowthMB: 3.0, GCBetweenRuns: true, Timeout: 10 * time.Second, }, } suite.runner.RunMemoryLeakTests(t, tests) } // ============================================================================= // Optimized Middleware Config Tests // ============================================================================= func TestOptimizedMiddlewareConfig(t *testing.T) { t.Run("DefaultOptimizedConfig", func(t *testing.T) { config := DefaultOptimizedConfig() assert.NotNil(t, config) assert.True(t, config.DelayBackgroundTasks) assert.True(t, config.ReducedCleanupIntervals) assert.True(t, config.AggressiveConnectionCleanup) assert.True(t, config.MinimalCacheSize) }) t.Run("CustomOptimizedConfig", func(t *testing.T) { config := &OptimizedMiddlewareConfig{ DelayBackgroundTasks: false, ReducedCleanupIntervals: true, AggressiveConnectionCleanup: false, MinimalCacheSize: true, } assert.False(t, config.DelayBackgroundTasks) assert.True(t, config.ReducedCleanupIntervals) assert.False(t, config.AggressiveConnectionCleanup) assert.True(t, config.MinimalCacheSize) }) } // ============================================================================= // Cleanup Idle Connections Tests // ============================================================================= func TestCleanupIdleConnections(t *testing.T) { config := GetTestConfig() if config.ShouldSkipTest(t, TestTypeLeakDetection) { return } t.Run("CleanupIdleConnections basic", func(t *testing.T) { client := &http.Client{ Transport: &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, }, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 50*time.Millisecond, stopChan) time.Sleep(150 * time.Millisecond) close(stopChan) time.Sleep(100 * time.Millisecond) }) t.Run("CleanupIdleConnections stop immediately", func(t *testing.T) { client := &http.Client{ Transport: &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, }, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 100*time.Millisecond, stopChan) time.Sleep(10 * time.Millisecond) close(stopChan) time.Sleep(50 * time.Millisecond) }) t.Run("CleanupIdleConnections with nil transport", func(t *testing.T) { client := &http.Client{ Transport: nil, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 50*time.Millisecond, stopChan) time.Sleep(100 * time.Millisecond) close(stopChan) time.Sleep(50 * time.Millisecond) }) } // ============================================================================= // Unit Tests (Non-Leak Detection) // ============================================================================= func TestNewLazyBackgroundTaskUnit(t *testing.T) { logger := GetSingletonNoOpLogger() callCount := 0 taskFunc := func() { callCount++ } task := NewLazyBackgroundTask("test-task", 50*time.Millisecond, taskFunc, logger) require.NotNil(t, task) assert.NotNil(t, task.BackgroundTask) assert.False(t, task.started) time.Sleep(100 * time.Millisecond) assert.Equal(t, 0, callCount, "task should not execute before StartIfNeeded") if task.started { task.Stop() } } func TestLazyBackgroundTaskStartIfNeededUnit(t *testing.T) { logger := GetSingletonNoOpLogger() callCount := 0 var mu sync.Mutex taskFunc := func() { mu.Lock() callCount++ mu.Unlock() } task := NewLazyBackgroundTask("test-start", 30*time.Millisecond, taskFunc, logger) require.NotNil(t, task) task.StartIfNeeded() assert.True(t, task.started) time.Sleep(100 * time.Millisecond) mu.Lock() firstCount := callCount mu.Unlock() assert.Greater(t, firstCount, 0, "task should execute after StartIfNeeded") task.StartIfNeeded() task.StartIfNeeded() task.Stop() } func TestLazyBackgroundTaskStopUnit(t *testing.T) { logger := GetSingletonNoOpLogger() callCount := 0 var mu sync.Mutex taskFunc := func() { mu.Lock() callCount++ mu.Unlock() } task := NewLazyBackgroundTask("test-stop", 30*time.Millisecond, taskFunc, logger) require.NotNil(t, task) task.StartIfNeeded() time.Sleep(100 * time.Millisecond) mu.Lock() countAfterStart := callCount mu.Unlock() assert.Greater(t, countAfterStart, 0) task.Stop() assert.False(t, task.started) time.Sleep(100 * time.Millisecond) mu.Lock() countAfterStop := callCount mu.Unlock() assert.LessOrEqual(t, countAfterStop, countAfterStart+1, "task should stop executing") } func TestNewLazyCacheUnit(t *testing.T) { cache := NewLazyCache() require.NotNil(t, cache) cache.Set("test-key", "test-value", time.Minute) val, found := cache.Get("test-key") assert.True(t, found) assert.Equal(t, "test-value", val) } func TestNewLazyCacheWithLoggerUnit(t *testing.T) { logger := GetSingletonNoOpLogger() cache := NewLazyCacheWithLogger(logger) require.NotNil(t, cache) for i := 0; i < 10; i++ { key := "key-" + string(rune('0'+i)) cache.Set(key, i, time.Minute) } for i := 0; i < 10; i++ { key := "key-" + string(rune('0'+i)) val, found := cache.Get(key) assert.True(t, found, "should find key %s", key) assert.Equal(t, i, val, "should get correct value for key %s", key) } } func TestNewLazyCacheWithLoggerNilUnit(t *testing.T) { cache := NewLazyCacheWithLogger(nil) require.NotNil(t, cache) cache.Set("nil-test", "value", time.Minute) val, found := cache.Get("nil-test") assert.True(t, found) assert.Equal(t, "value", val) } func TestCleanupIdleConnectionsUnit(t *testing.T) { t.Run("basic cleanup cycle", func(t *testing.T) { client := &http.Client{ Transport: &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, }, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 40*time.Millisecond, stopChan) time.Sleep(100 * time.Millisecond) close(stopChan) time.Sleep(50 * time.Millisecond) }) t.Run("immediate stop", func(t *testing.T) { client := &http.Client{ Transport: &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, }, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 100*time.Millisecond, stopChan) time.Sleep(10 * time.Millisecond) close(stopChan) time.Sleep(50 * time.Millisecond) }) t.Run("nil transport", func(t *testing.T) { client := &http.Client{ Transport: nil, } stopChan := make(chan struct{}) go CleanupIdleConnections(client, 40*time.Millisecond, stopChan) time.Sleep(80 * time.Millisecond) close(stopChan) time.Sleep(50 * time.Millisecond) }) } func TestDefaultOptimizedConfigUnit(t *testing.T) { config := DefaultOptimizedConfig() require.NotNil(t, config) assert.True(t, config.DelayBackgroundTasks) assert.True(t, config.ReducedCleanupIntervals) assert.True(t, config.AggressiveConnectionCleanup) assert.True(t, config.MinimalCacheSize) } // ============================================================================= // Consolidated Memory Leak Tests // ============================================================================= func TestMemoryLeakConsolidated(t *testing.T) { baselineGoroutines := runtime.NumGoroutine() defer func() { VerifyNoGoroutineLeaks(t, baselineGoroutines, 20, "TestMemoryLeakConsolidated") }() testCases := []MemoryTestCase{ { name: "cache_basic_lifecycle", component: "cache", scenario: "lifecycle", iterations: 10, concurrency: 1, setup: func(tf *MemoryTestFramework) error { return nil }, execute: func(tf *MemoryTestFramework) error { cache := NewCache() defer cache.Close() for i := 0; i < 100; i++ { key := fmt.Sprintf("key-%d", i) cache.Set(key, "value", time.Minute) cache.Get(key) } return nil }, validateLeak: func(t *testing.T, before, after runtime.MemStats) { allocDiff := int64(after.Alloc) - int64(before.Alloc) if allocDiff > 1024*1024 { t.Errorf("Memory leak detected: %d bytes allocated", allocDiff) } }, cleanup: func(tf *MemoryTestFramework) error { return nil }, }, { name: "cache_concurrent_access", component: "cache", scenario: "concurrent", iterations: 5, concurrency: 10, setup: func(tf *MemoryTestFramework) error { tf.cache = NewCache() return nil }, execute: func(tf *MemoryTestFramework) error { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < 100; j++ { key := fmt.Sprintf("key-%d-%d", id, j) tf.cache.Set(key, "value", time.Second) tf.cache.Get(key) } }(i) } wg.Wait() return nil }, validateLeak: func(t *testing.T, before, after runtime.MemStats) { allocDiff := int64(after.Alloc) - int64(before.Alloc) if allocDiff > 5*1024*1024 { t.Errorf("Memory leak in concurrent cache: %d bytes", allocDiff) } }, cleanup: func(tf *MemoryTestFramework) error { if tf.cache != nil { tf.cache.Close() tf.cache = nil } return nil }, }, { name: "session_manager_lifecycle", component: "session", scenario: "lifecycle", iterations: 5, concurrency: 1, setup: func(tf *MemoryTestFramework) error { return nil }, execute: func(tf *MemoryTestFramework) error { sm, err := NewSessionManager( "test-encryption-key-32-bytes-long-enough", false, "", "", 0, tf.logger, ) if err != nil { return err } defer func() {}() for i := 0; i < 50; i++ { req := httptest.NewRequest("GET", "/", nil) _, _ = sm.GetSession(req) } return nil }, validateLeak: func(t *testing.T, before, after runtime.MemStats) { allocDiff := int64(after.Alloc) - int64(before.Alloc) if allocDiff > 2*1024*1024 { t.Errorf("Session manager memory leak: %d bytes", allocDiff) } }, cleanup: func(tf *MemoryTestFramework) error { return nil }, }, { name: "buffer_pool_memory", component: "pool", scenario: "stress", iterations: 5, concurrency: 10, setup: func(tf *MemoryTestFramework) error { return nil }, execute: func(tf *MemoryTestFramework) error { pool := NewBufferPool(4096) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100; j++ { buf := pool.Get() buf.WriteString("test data") pool.Put(buf) } }() } wg.Wait() return nil }, validateLeak: func(t *testing.T, before, after runtime.MemStats) { allocDiff := int64(after.Alloc) - int64(before.Alloc) if allocDiff > 1024*1024 { t.Errorf("Buffer pool memory leak: %d bytes", allocDiff) } }, cleanup: func(tf *MemoryTestFramework) error { return nil }, }, { name: "gzip_pool_memory", component: "pool", scenario: "stress", iterations: 3, concurrency: 5, setup: func(tf *MemoryTestFramework) error { return nil }, execute: func(tf *MemoryTestFramework) error { pool := NewGzipWriterPool() var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 50; j++ { w := pool.Get() var buf bytes.Buffer w.Reset(&buf) w.Write([]byte("test compression data")) w.Close() pool.Put(w) } }() } wg.Wait() return nil }, validateLeak: func(t *testing.T, before, after runtime.MemStats) { allocDiff := int64(after.Alloc) - int64(before.Alloc) if allocDiff > 2*1024*1024 { t.Errorf("Gzip pool memory leak: %d bytes", allocDiff) } }, cleanup: func(tf *MemoryTestFramework) error { return nil }, }, } for _, tc := range testCases { tc := tc t.Run(fmt.Sprintf("%s_%s_%s", tc.component, tc.scenario, tc.name), func(t *testing.T) { if testing.Short() && tc.scenario == "longrunning" { t.Skip("Skipping long-running test in short mode") } for iteration := 0; iteration < tc.iterations; iteration++ { framework := NewMemoryTestFramework(t) defer framework.Cleanup() if tc.setup != nil { require.NoError(t, tc.setup(framework)) } runtime.GC() runtime.GC() debug.FreeOSMemory() var before runtime.MemStats runtime.ReadMemStats(&before) err := tc.execute(framework) require.NoError(t, err) if tc.cleanup != nil { require.NoError(t, tc.cleanup(framework)) } runtime.GC() runtime.GC() debug.FreeOSMemory() var after runtime.MemStats runtime.ReadMemStats(&after) tc.validateLeak(t, before, after) } }) } } // ============================================================================= // Goroutine Leak Tests // ============================================================================= func TestGoroutineLeaks(t *testing.T) { testCases := []struct { test func(t *testing.T) name string }{ { name: "cache_no_leak", test: func(t *testing.T) { baseline := runtime.NumGoroutine() cache := NewCache() for i := 0; i < 100; i++ { cache.Set(fmt.Sprintf("key-%d", i), "value", time.Second) } cache.Close() time.Sleep(100 * time.Millisecond) VerifyNoGoroutineLeaks(t, baseline, 2, "cache operations") }, }, { name: "session_manager_no_leak", test: func(t *testing.T) { baseline := runtime.NumGoroutine() sm, err := NewSessionManager( "test-encryption-key-32-bytes-long-enough", false, "", "", 0, NewLogger("error"), ) require.NoError(t, err) if sm != nil { sm.Shutdown() } time.Sleep(100 * time.Millisecond) VerifyNoGoroutineLeaks(t, baseline, 2, "session manager") }, }, { name: "plugin_no_leak", test: func(t *testing.T) { baseline := runtime.NumGoroutine() config := CreateConfig() config.ProviderURL = "https://accounts.google.com" config.SessionEncryptionKey = "test-encryption-key-32-bytes-long" config.ClientID = "test-client" config.ClientSecret = "test-secret" handler, err := New(context.Background(), nil, config, "test") require.NoError(t, err) plugin := handler.(*TraefikOidc) plugin.Close() time.Sleep(500 * time.Millisecond) VerifyNoGoroutineLeaks(t, baseline, 10, "plugin lifecycle") }, }, } for _, tc := range testCases { t.Run(tc.name, tc.test) } } // ============================================================================= // Memory Thresholds Tests // ============================================================================= func TestMemoryThresholds(t *testing.T) { thresholds := map[string]uint64{ "cache_1000_items": 10 * 1024 * 1024, "session_100_sessions": 5 * 1024 * 1024, "plugin_initialization": 20 * 1024 * 1024, "buffer_pool_usage": 2 * 1024 * 1024, } t.Run("cache_memory_threshold", func(t *testing.T) { var before, after runtime.MemStats runtime.GC() runtime.ReadMemStats(&before) cache := NewCache() for i := 0; i < 1000; i++ { cache.Set(fmt.Sprintf("key-%d", i), fmt.Sprintf("value-%d", i), time.Hour) } runtime.GC() runtime.ReadMemStats(&after) cache.Close() var memUsed uint64 if after.Alloc >= before.Alloc { memUsed = after.Alloc - before.Alloc } else { memUsed = 0 } threshold := thresholds["cache_1000_items"] assert.LessOrEqual(t, memUsed, threshold, "Cache memory usage %d exceeds threshold %d", memUsed, threshold) }) t.Run("session_memory_threshold", func(t *testing.T) { var before, after runtime.MemStats runtime.GC() runtime.ReadMemStats(&before) sm, _ := NewSessionManager( "test-encryption-key-32-bytes-long-enough", false, "", "", 0, NewLogger("error"), ) for i := 0; i < 100; i++ { req := httptest.NewRequest("GET", "/", nil) _, _ = sm.GetSession(req) } runtime.GC() runtime.ReadMemStats(&after) var memUsed uint64 if after.Alloc >= before.Alloc { memUsed = after.Alloc - before.Alloc } else { memUsed = 0 } threshold := thresholds["session_100_sessions"] assert.LessOrEqual(t, memUsed, threshold, "Session memory usage %d exceeds threshold %d", memUsed, threshold) }) }