package traefikoidc import ( "net/http/httptest" "sync" "testing" ) // testWriter is an io.Writer that writes to test log // lint:ignore U1000 Kept for potential future use /* type testWriter struct { t *testing.T } func (w *testWriter) Write(p []byte) (n int, err error) { w.t.Log(string(p)) return len(p), nil } */ // Test helper adapters for the new test files // resetGlobalState resets all global singletons to prevent test interference //nolint:unused // Kept for potential future use in integration tests /* func resetGlobalState() { // Reset global task registry first to stop all background tasks ResetGlobalTaskRegistry() // Give tasks a moment to stop time.Sleep(10 * time.Millisecond) // Reset and cleanup replay cache - this should work now that tasks are stopped cleanupReplayCache() // Reset memory pools memoryPoolMutex.Lock() globalMemoryPools = nil memoryPoolOnce = sync.Once{} memoryPoolMutex.Unlock() // The universal cache manager is a singleton that persists across tests // Don't reset it as it causes issues } */ // testCleanup provides comprehensive cleanup for tests to prevent goroutine leaks type testCleanup struct { t *testing.T caches []CacheInterface servers []*httptest.Server oidcs []*TraefikOidc mu sync.Mutex } // newTestCleanup creates a new test cleanup helper that automatically registers cleanup func newTestCleanup(t *testing.T) *testCleanup { tc := &testCleanup{ t: t, caches: make([]CacheInterface, 0), servers: make([]*httptest.Server, 0), oidcs: make([]*TraefikOidc, 0), } // Register cleanup to run even if test panics t.Cleanup(func() { tc.cleanupAll() }) return tc } // addCache registers a cache for cleanup func (tc *testCleanup) addCache(c CacheInterface) CacheInterface { tc.mu.Lock() defer tc.mu.Unlock() tc.caches = append(tc.caches, c) return c } // addTokenCache registers a token cache for cleanup func (tc *testCleanup) addTokenCache(c *TokenCache) *TokenCache { tc.mu.Lock() defer tc.mu.Unlock() // TokenCache cleanup is handled by the global manager // No need to manually close as it's a singleton return c } // addOIDC registers a TraefikOidc instance for cleanup // //lint:ignore U1000 Kept for potential future use func (tc *testCleanup) addOIDC(o *TraefikOidc) *TraefikOidc { tc.mu.Lock() defer tc.mu.Unlock() tc.oidcs = append(tc.oidcs, o) return o } // cleanupAll cleans up all registered resources func (tc *testCleanup) cleanupAll() { tc.mu.Lock() defer tc.mu.Unlock() // Close all caches for _, c := range tc.caches { if c != nil { c.Close() } } // Close all servers for _, s := range tc.servers { if s != nil { s.Close() } } // Close all OIDC instances for _, o := range tc.oidcs { if o != nil { // Close caches within the OIDC instance if o.tokenCache != nil && o.tokenCache.cache != nil { o.tokenCache.cache.Close() } if o.tokenBlacklist != nil { o.tokenBlacklist.Close() } // Call Close if it exists o.Close() } } // Reset global state - commented out as resetGlobalState is unused // resetGlobalState() } // createTestConfig creates a config with all required fields populated for testing //nolint:unused // Kept for potential future use in integration tests /* func createTestConfig() *Config { config := CreateConfig() config.ProviderURL = "https://test-provider.com" config.ClientID = "test-client-id" config.ClientSecret = "test-client-secret" config.SessionEncryptionKey = "test-encryption-key-32-characters" config.CallbackURL = "/oauth2/callback" return config } */ // setupTestOIDCMiddleware creates a test OIDC middleware instance with mock servers //nolint:unused // Kept for potential future use in integration tests /* func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httptest.Server) { // Reset global state to ensure test isolation resetGlobalState() // Create mock OIDC server var serverURL string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/.well-known/openid-configuration": w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "issuer": serverURL, "authorization_endpoint": serverURL + "/auth", "token_endpoint": serverURL + "/token", "userinfo_endpoint": serverURL + "/userinfo", "jwks_uri": serverURL + "/keys", "revocation_endpoint": serverURL + "/revoke", }) case "/keys": w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "keys": [{ "kty": "RSA", "kid": "test-key-id", "use": "sig", "n": "test-n-value", "e": "AQAB" }] }`)) case "/token": w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "access_token": "test-access-token", "id_token": "` + ValidIDToken + `", "refresh_token": "test-refresh-token", "token_type": "bearer", "expires_in": 3600 }`)) default: w.WriteHeader(http.StatusNotFound) } })) serverURL = server.URL // Create middleware bypassing validation like main tests do // Create a logger that outputs to test log logger := &Logger{ logError: log.New(&testWriter{t}, "ERROR: ", 0), logInfo: log.New(&testWriter{t}, "INFO: ", 0), logDebug: log.New(&testWriter{t}, "DEBUG: ", 0), } sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, "", "", logger) // Create next handler nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) // Set default paths callbackPath := config.CallbackURL if callbackPath == "" { callbackPath = "/oauth2/callback" } logoutPath := config.LogoutURL if logoutPath == "" { logoutPath = callbackPath + "/logout" } // Set default post logout redirect URI to match the actual implementation postLogoutRedirectURI := config.PostLogoutRedirectURI if postLogoutRedirectURI == "" { postLogoutRedirectURI = "/" // Default to root path like the actual implementation } // Use test URLs that won't be blocked by validation testIssuerURL := "https://test-provider.example.com" testAuthURL := testIssuerURL + "/auth" testTokenURL := testIssuerURL + "/token" testJWKSURL := testIssuerURL + "/keys" // Create WaitGroup for background goroutines var wg sync.WaitGroup // Create context with cancel for proper cleanup ctx, cancel := context.WithCancel(context.Background()) // Create TraefikOidc instance directly oidc := &TraefikOidc{ next: nextHandler, issuerURL: testIssuerURL, clientID: config.ClientID, audience: config.ClientID, clientSecret: config.ClientSecret, redirURLPath: callbackPath, logoutURLPath: logoutPath, postLogoutRedirectURI: postLogoutRedirectURI, limiter: rate.NewLimiter(rate.Every(time.Second), 10), tokenBlacklist: NewCache(), tokenCache: NewTokenCache(), logger: logger, excludedURLs: make(map[string]struct{}), httpClient: &http.Client{}, authURL: testAuthURL, tokenURL: testTokenURL, jwksURL: testJWKSURL, initComplete: make(chan struct{}), sessionManager: sessionManager, extractClaimsFunc: extractClaims, enablePKCE: config.EnablePKCE, refreshGracePeriod: time.Duration(config.RefreshGracePeriodSeconds) * time.Second, revocationURL: config.RevocationURL, endSessionURL: config.OIDCEndSessionURL, scopes: config.Scopes, forceHTTPS: config.ForceHTTPS, allowedUserDomains: make(map[string]struct{}), jwkCache: NewJWKCache(), metadataCache: NewMetadataCache(nil), ctx: ctx, cancelFunc: cancel, goroutineWG: &wg, providerURL: serverURL, } // Process excluded URLs for _, url := range config.ExcludedURLs { oidc.excludedURLs[url] = struct{}{} } // Set default excluded URLs oidc.excludedURLs["/favicon"] = struct{}{} oidc.excludedURLs["/favicon.ico"] = struct{}{} // Close init channel close(oidc.initComplete) // Set verifiers oidc.tokenVerifier = oidc oidc.jwtVerifier = oidc oidc.tokenExchanger = oidc // Set tokenExchanger to self // Set default refresh grace period if not set or negative if config.RefreshGracePeriodSeconds <= 0 { oidc.refreshGracePeriod = 60 * time.Second } // Set authentication initiation function oidc.initiateAuthenticationFunc = func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) { // Generate CSRF token and nonce csrfToken := uuid.NewString() nonce := uuid.NewString() // Store in session session.SetCSRF(csrfToken) session.SetNonce(nonce) // Store the original path session.SetIncomingPath(req.URL.RequestURI()) // Handle PKCE if enabled var codeChallenge string if oidc.enablePKCE { verifier, _ := generateCodeVerifier() session.SetCodeVerifier(verifier) codeChallenge = deriveCodeChallenge(verifier) } // Save session session.Save(req, rw) // Build auth URL authURL := oidc.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge) // Redirect http.Redirect(rw, req, authURL, http.StatusFound) } // Set scopes if not set if len(oidc.scopes) == 0 { oidc.scopes = []string{"openid", "profile", "email"} } return oidc, server } */ // createMockJWT creates a mock JWT token for testing - adapter for existing tests //nolint:unused // Kept for potential future use in integration tests /* func createMockJWT(t *testing.T, sub, email string) string { return ValidIDToken } */ // createTestSession creates a properly initialized SessionData for testing func createTestSession() *SessionData { // Create a minimal session manager for testing logger := newNoOpLogger() sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, logger) // Create a test request req := httptest.NewRequest("GET", "/", nil) // Get a session from the manager session, _ := sessionManager.GetSession(req) return session } // injectSessionIntoRequest saves the session and adds the resulting cookies to the request //nolint:unused // Kept for potential future use in integration tests /* func injectSessionIntoRequest(t *testing.T, req *http.Request, session *SessionData) { // Create a response recorder to capture cookies rec := httptest.NewRecorder() // Save the session (this sets cookies) if err := session.Save(req, rec); err != nil { t.Fatalf("Failed to save session: %v", err) } // Add the cookies to the request for _, cookie := range rec.Result().Cookies() { req.AddCookie(cookie) } } */