Files
traefikoidc/test_helpers_adapter_test.go
T
lukaszraczylo e64fc7f730 Add redis support for distributed caching (#83)
* Add redis support for distributed caching

* Move towards the self-provided Redis connection pool and RESP protocol implementation.
Official redis client library won't work with yaegi.

* fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* ... and another all nighter.

* fixup! ... and another all nighter.

* fixup! fixup! ... and another all nighter.

* fixup! fixup! fixup! ... and another all nighter.

* Resolve issue #85 by adding ability to set custom claims in JWT tokens

* Remove redundant validation in auth middleware ( issue #89 )

* Add ability to set cookie prefix for session cookies ( #87 )

* fixup! Add ability to set cookie prefix for session cookies ( #87 )

* Add ability to set cookie max age - issue #91

* Potential fix for code scanning alert no. 10: Size computation for allocation may overflow

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fixup! Merge main into 0.8.0-redis: resolve conflicts

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-30 02:18:46 +00:00

381 lines
11 KiB
Go

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)
}
}
*/