package traefikoidc import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "net/http/httptest" "runtime" "sync" "sync/atomic" "testing" "time" "github.com/lukaszraczylo/traefikoidc/session/chunking" ) // GlobalTestCleanup tracks and cleans up test resources type GlobalTestCleanup struct { servers []*httptest.Server tasks []*BackgroundTask caches []interface{ Close() } mu sync.Mutex } var globalCleanup = &GlobalTestCleanup{} // RegisterServer registers an HTTP test server for cleanup func (g *GlobalTestCleanup) RegisterServer(server *httptest.Server) { g.mu.Lock() defer g.mu.Unlock() g.servers = append(g.servers, server) } // RegisterTask registers a background task for cleanup func (g *GlobalTestCleanup) RegisterTask(task *BackgroundTask) { g.mu.Lock() defer g.mu.Unlock() g.tasks = append(g.tasks, task) } // RegisterCache registers a cache for cleanup func (g *GlobalTestCleanup) RegisterCache(cache interface{ Close() }) { g.mu.Lock() defer g.mu.Unlock() g.caches = append(g.caches, cache) } // CleanupAll cleans up all registered resources with timeout protection func (g *GlobalTestCleanup) CleanupAll() { g.mu.Lock() defer g.mu.Unlock() // Close servers first for _, server := range g.servers { if server != nil { server.Close() } } g.servers = nil // Stop background tasks with timeout var wg sync.WaitGroup for _, task := range g.tasks { if task != nil { wg.Add(1) // Stop each task in a goroutine with timeout to prevent deadlock go func(t *BackgroundTask) { defer wg.Done() // Give each task up to 1 second to stop done := make(chan struct{}) go func() { t.Stop() close(done) }() select { case <-done: // Task stopped successfully case <-time.After(1 * time.Second): // Task didn't stop in time - log warning but continue runtime.GC() // Force GC to help clean up leaked resources } }(task) } } // Wait for all task cleanup goroutines to complete wg.Wait() g.tasks = nil // Close caches for _, cache := range g.caches { if cache != nil { cache.Close() } } g.caches = nil // Clean up the global cache manager as part of the global cleanup // Use a timeout to prevent hanging cleanupDone := make(chan struct{}) go func() { _ = CleanupGlobalCacheManager() // Safe to ignore: cleanup in test infrastructure close(cleanupDone) }() select { case <-cleanupDone: // Cleanup completed successfully case <-time.After(5 * time.Second): // Cleanup timed out, but continue runtime.GC() // Force GC to help clean up } // Reset all global singletons to prevent state pollution between tests ResetGlobalMemoryMonitor() ResetGlobalTaskRegistry() ResetGlobalMemoryOptimizations() ResetSingletonNoOpLogger() // Reset global session counters to prevent overflow in memory calculations ResetGlobalSessionCounters() // Reset global session counters in chunking package as well // Note: This calls the function in session/chunking package resetChunkingGlobalSessionCounters() // Give background tasks time to finish cleanup time.Sleep(100 * time.Millisecond) runtime.GC() runtime.GC() // Double GC to ensure cleanup } // TestCleanupHelper provides automatic cleanup for tests with goroutine leak detection func TestCleanupHelper(t *testing.T) { // Record initial goroutine count initialGoroutines := runtime.NumGoroutine() t.Cleanup(func() { // Clean up all resources globalCleanup.CleanupAll() // Check for goroutine leaks after cleanup CheckGoroutineLeaks(t, initialGoroutines) }) } // CheckGoroutineLeaks detects and reports goroutine leaks func CheckGoroutineLeaks(t *testing.T, initialCount int) { // Give goroutines time to clean up time.Sleep(50 * time.Millisecond) runtime.GC() runtime.GC() finalCount := runtime.NumGoroutine() growth := finalCount - initialCount // Allow for small growth (up to 2 goroutines) as some tests may have legitimate background work if growth > 2 { t.Errorf("Potential goroutine leak detected: started with %d, ended with %d (growth: %d)", initialCount, finalCount, growth) // Print stack traces to help debug the leak buf := make([]byte, 1<<16) stackSize := runtime.Stack(buf, true) t.Logf("Goroutine stack traces:\n%s", buf[:stackSize]) } } // ForceGoroutineCleanup aggressively tries to clean up leaked goroutines func ForceGoroutineCleanup() { // Multiple GC passes to ensure cleanup for i := 0; i < 3; i++ { runtime.GC() time.Sleep(10 * time.Millisecond) } } // GetTestDuration returns an appropriate duration based on test mode func GetTestDuration(normal time.Duration) time.Duration { if testing.Short() { // In short mode, reduce all durations by 10x return normal / 10 } return normal } // UnifiedMockSession provides a comprehensive mock for the Session interface type UnifiedMockSession struct { data map[string]interface{} callCounts map[string]int64 errors map[string]error delays map[string]time.Duration destroyCount int64 mu sync.RWMutex destroyed bool } // NewUnifiedMockSession creates a new mock session with default behavior func NewUnifiedMockSession() *UnifiedMockSession { return &UnifiedMockSession{ data: make(map[string]interface{}), callCounts: make(map[string]int64), errors: make(map[string]error), delays: make(map[string]time.Duration), } } // SetError configures the mock to return an error for specific method calls func (m *UnifiedMockSession) SetError(method string, err error) { m.mu.Lock() defer m.mu.Unlock() m.errors[method] = err } // SetDelay configures the mock to add delay for specific method calls func (m *UnifiedMockSession) SetDelay(method string, delay time.Duration) { m.mu.Lock() defer m.mu.Unlock() m.delays[method] = delay } // GetCallCount returns the number of times a method was called func (m *UnifiedMockSession) GetCallCount(method string) int64 { m.mu.RLock() defer m.mu.RUnlock() return m.callCounts[method] } func (m *UnifiedMockSession) recordCall(method string) { m.mu.Lock() m.callCounts[method]++ m.mu.Unlock() } func (m *UnifiedMockSession) checkError(method string) error { m.mu.RLock() defer m.mu.RUnlock() if err, exists := m.errors[method]; exists { return err } return nil } func (m *UnifiedMockSession) applyDelay(method string) { m.mu.RLock() delay, exists := m.delays[method] m.mu.RUnlock() if exists && delay > 0 { time.Sleep(delay) } } // Session interface implementation func (m *UnifiedMockSession) Get(key string) (interface{}, bool) { m.recordCall("Get") if err := m.checkError("Get"); err != nil { return nil, false } m.applyDelay("Get") m.mu.RLock() defer m.mu.RUnlock() val, exists := m.data[key] return val, exists } func (m *UnifiedMockSession) Set(key string, value interface{}) { m.recordCall("Set") if err := m.checkError("Set"); err != nil { return } m.applyDelay("Set") m.mu.Lock() defer m.mu.Unlock() m.data[key] = value } func (m *UnifiedMockSession) Delete(key string) { m.recordCall("Delete") if err := m.checkError("Delete"); err != nil { return } m.applyDelay("Delete") m.mu.Lock() defer m.mu.Unlock() delete(m.data, key) } func (m *UnifiedMockSession) Destroy() error { m.recordCall("Destroy") if err := m.checkError("Destroy"); err != nil { return err } m.applyDelay("Destroy") m.mu.Lock() defer m.mu.Unlock() if m.destroyed { return fmt.Errorf("session already destroyed") } m.destroyed = true atomic.AddInt64(&m.destroyCount, 1) // Clear data to help with memory leak detection for k := range m.data { delete(m.data, k) } return nil } func (m *UnifiedMockSession) IsDestroyed() bool { m.mu.RLock() defer m.mu.RUnlock() return m.destroyed } func (m *UnifiedMockSession) GetDestroyCount() int64 { return atomic.LoadInt64(&m.destroyCount) } // UnifiedMockTokenVerifier provides a comprehensive mock for token verification type UnifiedMockTokenVerifier struct { validTokens map[string]bool tokenMetadata map[string]map[string]interface{} callCounts map[string]int64 errors map[string]error delays map[string]time.Duration verificationFunc func(string) error mu sync.RWMutex } // NewUnifiedMockTokenVerifier creates a new mock token verifier func NewUnifiedMockTokenVerifier() *UnifiedMockTokenVerifier { return &UnifiedMockTokenVerifier{ validTokens: make(map[string]bool), tokenMetadata: make(map[string]map[string]interface{}), callCounts: make(map[string]int64), errors: make(map[string]error), delays: make(map[string]time.Duration), } } // SetTokenValid configures whether a token should be considered valid func (m *UnifiedMockTokenVerifier) SetTokenValid(token string, valid bool) { m.mu.Lock() defer m.mu.Unlock() m.validTokens[token] = valid } // SetTokenMetadata configures metadata for a token func (m *UnifiedMockTokenVerifier) SetTokenMetadata(token string, metadata map[string]interface{}) { m.mu.Lock() defer m.mu.Unlock() m.tokenMetadata[token] = metadata } // SetVerificationFunc allows custom verification logic func (m *UnifiedMockTokenVerifier) SetVerificationFunc(fn func(string) error) { m.mu.Lock() defer m.mu.Unlock() m.verificationFunc = fn } // SetError configures the mock to return an error for specific method calls func (m *UnifiedMockTokenVerifier) SetError(method string, err error) { m.mu.Lock() defer m.mu.Unlock() m.errors[method] = err } // GetCallCount returns the number of times a method was called func (m *UnifiedMockTokenVerifier) GetCallCount(method string) int64 { m.mu.RLock() defer m.mu.RUnlock() return m.callCounts[method] } func (m *UnifiedMockTokenVerifier) recordCall(method string) { m.mu.Lock() m.callCounts[method]++ m.mu.Unlock() } func (m *UnifiedMockTokenVerifier) VerifyToken(token string) error { m.recordCall("VerifyToken") if err := m.errors["VerifyToken"]; err != nil { return err } if delay := m.delays["VerifyToken"]; delay > 0 { time.Sleep(delay) } m.mu.RLock() defer m.mu.RUnlock() if m.verificationFunc != nil { return m.verificationFunc(token) } if valid, exists := m.validTokens[token]; exists && valid { return nil } return fmt.Errorf("invalid token") } // UnifiedMockTokenCache provides a comprehensive mock for token caching type UnifiedMockTokenCache struct { cache map[string]TestCacheEntry callCounts map[string]int64 errors map[string]error delays map[string]time.Duration hitRate float64 mu sync.RWMutex } // TestCacheEntry represents a cached token entry for testing type TestCacheEntry struct { ExpiresAt time.Time Metadata map[string]interface{} Token string } // NewUnifiedMockTokenCache creates a new mock token cache func NewUnifiedMockTokenCache() *UnifiedMockTokenCache { return &UnifiedMockTokenCache{ cache: make(map[string]TestCacheEntry), callCounts: make(map[string]int64), errors: make(map[string]error), delays: make(map[string]time.Duration), hitRate: 1.0, // Default to 100% hit rate } } // SetError configures the mock to return an error for specific method calls func (m *UnifiedMockTokenCache) SetError(method string, err error) { m.mu.Lock() defer m.mu.Unlock() m.errors[method] = err } // SetHitRate configures the cache hit rate (0.0 to 1.0) func (m *UnifiedMockTokenCache) SetHitRate(rate float64) { m.mu.Lock() defer m.mu.Unlock() m.hitRate = rate } // GetCallCount returns the number of times a method was called func (m *UnifiedMockTokenCache) GetCallCount(method string) int64 { m.mu.RLock() defer m.mu.RUnlock() return m.callCounts[method] } func (m *UnifiedMockTokenCache) recordCall(method string) { m.mu.Lock() m.callCounts[method]++ m.mu.Unlock() } func (m *UnifiedMockTokenCache) Get(key string) (string, bool) { m.recordCall("Get") if err := m.errors["Get"]; err != nil { return "", false } if delay := m.delays["Get"]; delay > 0 { time.Sleep(delay) } m.mu.RLock() defer m.mu.RUnlock() // Simulate cache miss based on hit rate if m.hitRate < 1.0 { // Simple random check (in real tests, you might want deterministic behavior) if float64(len(key)%100)/100.0 > m.hitRate { return "", false } } entry, exists := m.cache[key] if !exists { return "", false } if time.Now().After(entry.ExpiresAt) { return "", false } return entry.Token, true } func (m *UnifiedMockTokenCache) Set(key, token string, expiry time.Time) { m.recordCall("Set") if delay := m.delays["Set"]; delay > 0 { time.Sleep(delay) } m.mu.Lock() defer m.mu.Unlock() m.cache[key] = TestCacheEntry{ Token: token, ExpiresAt: expiry, Metadata: make(map[string]interface{}), } } func (m *UnifiedMockTokenCache) Delete(key string) { m.recordCall("Delete") m.mu.Lock() defer m.mu.Unlock() delete(m.cache, key) } func (m *UnifiedMockTokenCache) Clear() { m.recordCall("Clear") m.mu.Lock() defer m.mu.Unlock() for k := range m.cache { delete(m.cache, k) } } // TableTestCase represents a standardized test case structure type TableTestCase struct { Input interface{} Expected interface{} ExpectedError error Setup func(*testing.T) error Teardown func(*testing.T) error Name string Description string SkipReason string Tags []string Timeout time.Duration Parallel bool } // MemoryLeakTestCase represents a test case specifically for memory leak detection type MemoryLeakTestCase struct { Operation func() error Setup func() error Teardown func() error Name string Description string Iterations int MaxGoroutineGrowth int MaxMemoryGrowthMB float64 Timeout time.Duration GCBetweenRuns bool } // TestSuiteRunner provides utilities for running table-driven tests type TestSuiteRunner struct { beforeEach func(*testing.T) afterEach func(*testing.T) timeout time.Duration parallelTests bool } // NewTestSuiteRunner creates a new test suite runner func NewTestSuiteRunner() *TestSuiteRunner { return &TestSuiteRunner{ timeout: 30 * time.Second, } } // SetParallel enables or disables parallel test execution func (r *TestSuiteRunner) SetParallel(parallel bool) { r.parallelTests = parallel } // SetTimeout sets the default timeout for tests func (r *TestSuiteRunner) SetTimeout(timeout time.Duration) { r.timeout = timeout } // SetBeforeEach sets a function to run before each test func (r *TestSuiteRunner) SetBeforeEach(fn func(*testing.T)) { r.beforeEach = fn } // SetAfterEach sets a function to run after each test func (r *TestSuiteRunner) SetAfterEach(fn func(*testing.T)) { r.afterEach = fn } // RunTests executes a table of test cases func (r *TestSuiteRunner) RunTests(t *testing.T, tests []TableTestCase) { for _, test := range tests { test := test // Capture loop variable if test.SkipReason != "" { t.Skip(test.SkipReason) continue } testFunc := func(t *testing.T) { if r.beforeEach != nil { r.beforeEach(t) } if r.afterEach != nil { defer r.afterEach(t) } timeout := test.Timeout if timeout == 0 { timeout = r.timeout } done := make(chan bool, 1) var testErr error go func() { defer func() { if r := recover(); r != nil { testErr = fmt.Errorf("test panicked: %v", r) } done <- true }() if test.Setup != nil { if err := test.Setup(t); err != nil { testErr = fmt.Errorf("setup failed: %w", err) return } } if test.Teardown != nil { defer func() { if err := test.Teardown(t); err != nil { t.Errorf("teardown failed: %v", err) } }() } // Execute the actual test logic here // This would be filled in by specific test implementations }() select { case <-done: if testErr != nil { t.Error(testErr) } case <-time.After(timeout): t.Errorf("test timed out after %v", timeout) } } if test.Parallel || r.parallelTests { t.Run(test.Name, func(t *testing.T) { t.Parallel() testFunc(t) }) } else { t.Run(test.Name, testFunc) } } } // RunMemoryLeakTests executes memory leak test cases func (r *TestSuiteRunner) RunMemoryLeakTests(t *testing.T, tests []MemoryLeakTestCase) { for _, test := range tests { test := test // Capture loop variable t.Run(test.Name, func(t *testing.T) { if test.Setup != nil { if err := test.Setup(); err != nil { t.Fatalf("setup failed: %v", err) } } if test.Teardown != nil { defer func() { if err := test.Teardown(); err != nil { t.Errorf("teardown failed: %v", err) } }() } // Record initial state runtime.GC() initialGoroutines := runtime.NumGoroutine() var initialMem runtime.MemStats runtime.ReadMemStats(&initialMem) // Run the operation multiple times for i := 0; i < test.Iterations; i++ { if test.Operation != nil { if err := test.Operation(); err != nil { t.Errorf("iteration %d failed: %v", i, err) return } } if test.GCBetweenRuns { runtime.GC() } } // Force garbage collection and check final state runtime.GC() runtime.GC() // Double GC to ensure cleanup finalGoroutines := runtime.NumGoroutine() var finalMem runtime.MemStats runtime.ReadMemStats(&finalMem) // Check goroutine growth goroutineGrowth := finalGoroutines - initialGoroutines if test.MaxGoroutineGrowth >= 0 && goroutineGrowth > test.MaxGoroutineGrowth { t.Errorf("goroutine leak detected: started with %d, ended with %d (growth: %d, max allowed: %d)", initialGoroutines, finalGoroutines, goroutineGrowth, test.MaxGoroutineGrowth) } // Check memory growth // #nosec G115 -- memory stats are within int64 range for practical purposes memoryGrowthBytes := int64(finalMem.Alloc) - int64(initialMem.Alloc) memoryGrowthMB := float64(memoryGrowthBytes) / (1024 * 1024) if test.MaxMemoryGrowthMB >= 0 && memoryGrowthMB > test.MaxMemoryGrowthMB { t.Errorf("memory leak detected: memory grew by %.2f MB (max allowed: %.2f MB)", memoryGrowthMB, test.MaxMemoryGrowthMB) } t.Logf("Memory test completed: goroutines %d->%d (Δ%d), memory %.2f MB growth", initialGoroutines, finalGoroutines, goroutineGrowth, memoryGrowthMB) }) } } // TestDataFactory provides utilities for generating test data type TestDataFactory struct{} // NewTestDataFactory creates a new test data factory func NewTestDataFactory() *TestDataFactory { return &TestDataFactory{} } // GenerateRandomString generates a random string of specified length func (f *TestDataFactory) GenerateRandomString(length int) string { if length <= 0 { return "" } if length == 1 { return "a" // Return a simple character for length 1 } bytes := make([]byte, (length+1)/2) // Ensure we have enough bytes if _, err := rand.Read(bytes); err != nil { return fmt.Sprintf("test-string-%d", time.Now().UnixNano())[:length] } encoded := hex.EncodeToString(bytes) if len(encoded) >= length { return encoded[:length] } return encoded } // GenerateTestToken generates a test JWT-like token func (f *TestDataFactory) GenerateTestToken() string { header := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" payload := "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" signature := f.GenerateRandomString(32) return fmt.Sprintf("%s.%s.%s", header, payload, signature) } // GenerateTestHTTPRequest generates a test HTTP request func (f *TestDataFactory) GenerateTestHTTPRequest() *http.Request { req, _ := http.NewRequest("GET", "http://example.com/test", nil) req.Header.Set("User-Agent", "test-agent") req.Header.Set("Authorization", "Bearer "+f.GenerateTestToken()) return req } // GenerateTestSession generates a test session with random data func (f *TestDataFactory) GenerateTestSession() *UnifiedMockSession { session := NewUnifiedMockSession() session.Set("user_id", f.GenerateRandomString(16)) session.Set("email", fmt.Sprintf("user%s@example.com", f.GenerateRandomString(8))) session.Set("created_at", time.Now()) return session } // EdgeCaseGenerator provides utilities for generating comprehensive edge cases type EdgeCaseGenerator struct { factory *TestDataFactory } // NewEdgeCaseGenerator creates a new edge case generator func NewEdgeCaseGenerator() *EdgeCaseGenerator { return &EdgeCaseGenerator{ factory: NewTestDataFactory(), } } // GenerateStringEdgeCases generates edge cases for string inputs func (g *EdgeCaseGenerator) GenerateStringEdgeCases() []string { return []string{ "", // Empty string " ", // Single space " ", // Multiple spaces "\t", // Tab "\n", // Newline "\r\n", // Windows newline "a", // Single character g.factory.GenerateRandomString(1), // Random single char g.factory.GenerateRandomString(1000), // Long string g.factory.GenerateRandomString(10000), // Very long string "特殊字符", // Unicode characters "🚀🎯📊", // Emojis "'DROP TABLE users;", // SQL injection attempt "", // XSS attempt "../../etc/passwd", // Path traversal attempt string([]byte{0, 1, 2, 255}), // Binary data } } // GenerateIntegerEdgeCases generates edge cases for integer inputs func (g *EdgeCaseGenerator) GenerateIntegerEdgeCases() []int { return []int{ 0, 1, -1, 42, -42, 2147483647, // max int32 -2147483648, // min int32 1000000, -1000000, } } // GenerateTimeEdgeCases generates edge cases for time inputs func (g *EdgeCaseGenerator) GenerateTimeEdgeCases() []time.Time { now := time.Now() return []time.Time{ {}, // Zero time now, // Current time now.Add(-time.Hour), // One hour ago now.Add(time.Hour), // One hour from now now.Add(-24 * time.Hour), // One day ago now.Add(24 * time.Hour), // One day from now now.Add(-365 * 24 * time.Hour), // One year ago now.Add(365 * 24 * time.Hour), // One year from now time.Unix(0, 0), // Unix epoch time.Unix(1<<63-62135596801, 0), // Max time } } // GenerateHTTPRequestEdgeCases generates edge cases for HTTP requests func (g *EdgeCaseGenerator) GenerateHTTPRequestEdgeCases() []*http.Request { cases := make([]*http.Request, 0) // Basic cases req1, _ := http.NewRequest("GET", "http://example.com", nil) cases = append(cases, req1) // Request with headers req2, _ := http.NewRequest("POST", "https://api.example.com/endpoint", nil) req2.Header.Set("Content-Type", "application/json") req2.Header.Set("Authorization", "Bearer "+g.factory.GenerateTestToken()) cases = append(cases, req2) // Request with query parameters req3, _ := http.NewRequest("GET", "http://example.com/search?q=test&limit=10", nil) cases = append(cases, req3) // Request with unusual headers req4, _ := http.NewRequest("GET", "http://example.com", nil) req4.Header.Set("X-Custom-Header", g.factory.GenerateRandomString(1000)) req4.Header.Set("User-Agent", "") cases = append(cases, req4) return cases } // PerformanceTestHelper provides utilities for performance testing type PerformanceTestHelper struct { samples []time.Duration mu sync.Mutex } // NewPerformanceTestHelper creates a new performance test helper func NewPerformanceTestHelper() *PerformanceTestHelper { return &PerformanceTestHelper{ samples: make([]time.Duration, 0), } } // Measure measures the execution time of a function func (h *PerformanceTestHelper) Measure(fn func()) time.Duration { start := time.Now() fn() duration := time.Since(start) h.mu.Lock() h.samples = append(h.samples, duration) h.mu.Unlock() return duration } // GetAverageTime returns the average execution time func (h *PerformanceTestHelper) GetAverageTime() time.Duration { h.mu.Lock() defer h.mu.Unlock() if len(h.samples) == 0 { return 0 } var total time.Duration for _, sample := range h.samples { total += sample } return total / time.Duration(len(h.samples)) } // GetPercentile returns the nth percentile of execution times func (h *PerformanceTestHelper) GetPercentile(percentile float64) time.Duration { h.mu.Lock() defer h.mu.Unlock() if len(h.samples) == 0 { return 0 } // Simple percentile calculation (could be improved with sorting) index := int(float64(len(h.samples)) * percentile / 100.0) if index >= len(h.samples) { index = len(h.samples) - 1 } return h.samples[index] } // Reset clears all performance samples func (h *PerformanceTestHelper) Reset() { h.mu.Lock() defer h.mu.Unlock() h.samples = h.samples[:0] } // resetChunkingGlobalSessionCounters resets the global session counters // in the chunking package to prevent test interference func resetChunkingGlobalSessionCounters() { chunking.ResetGlobalSessionCounters() }