Files
traefikoidc/docs/TESTING.md
lukaszraczylo 2d1b04c637 review fixes apr 2026 (#130)
* Multiple fixes

- refresh coordinator dedup + memory pressure wire
- middleware sse consolidation + timer leak + claim cache
- universal cache sync backfill + isDebug gate
- lazy background task race
- memory monitor stw cached + refresh() api

* fix(auth): suppress OIDC redirects on non-navigation requests

- [x] Add isNonNavigationRequest using Sec-Fetch-Mode and Accept headers
- [x] Add comprehensive TestIsNonNavigationRequest
- [x] Update ServeHTTP to 401 non-navigation and AJAX requests

Fixes #129

* feat(config): add custom CA and insecure skip verify for OIDC TLS

- [x] Add CACertPath, CACertPEM, InsecureSkipVerify to Config
- [x] Implement loadCACertPool for CA bundle loading
- [x] Update HTTPClientConfig with RootCAs and InsecureSkipVerify
- [x] Apply CA pool and skip verify to pooled HTTP clients
- [x] Enhance configKey to distinguish TLS configs
- [x] Add comprehensive ca_cert_test.go

Fixes #125

* feat(oidc): add custom CA certificate support for private OIDC providers

- [x] Add caCertPath, caCertPEM, insecureSkipVerify config options
- [x] Update traefik.yml with new OIDC client config fields
- [x] Add configuration schema descriptions for new options
- [x] Update README table and add Custom CA Certificates section

* Fix the documentation.

* test(redis): add oversized argument rejection test

- [x] Add TestRedisConn_RejectOversizedArgumentBytes
- [x] Import strings package

* Dependencies cleanup
2026-04-19 10:12:00 +01:00

11 KiB

Testing Guide

Comprehensive testing infrastructure for traefikoidc.

Overview

Metric Value
Test files 110
Lines of test code ~72,000
Code coverage 71.0%
Race conditions None (all pass with -race)

Running Tests

# Run all tests
go test ./...

# Run with race detection
go test -race ./...

# Run with coverage
go test -cover ./...

# Run specific test suite
go test -v -run "TokenValidationSuite" .

# Run edge case tests
go test -v -run "ClockSkewEdgeCasesSuite|UnicodeClaimsSuite" .

Test Infrastructure

Directory Structure

internal/testutil/
├── compat.go              # Re-exports for main package access
├── mocks/
│   ├── interfaces.go      # JWKCache, TokenExchanger, TokenVerifier, etc.
│   ├── session.go         # SessionManager, SessionData
│   ├── cache.go           # Cache, TokenCache, Blacklist
│   └── interfaces_test.go # Mock verification tests
├── fixtures/
│   └── tokens.go          # JWT token generation fixtures
└── servers/
    ├── oidc.go            # Mock OIDC server factory
    └── oidc_test.go       # Server tests

Test Suites

Suite File Description
TokenValidationSuite token_validation_suite_test.go Token validation happy path and error cases
JWKCacheTestSuite token_validation_suite_test.go JWK cache behavior tests
TokenExchangerTestSuite token_validation_suite_test.go Token exchange scenarios
ClockSkewEdgeCasesSuite edge_cases_suite_test.go Expiry boundary testing
UnicodeClaimsSuite edge_cases_suite_test.go Unicode/emoji handling in claims
LargeClaimsSuite edge_cases_suite_test.go Large data handling (100s of claims)
URLPathEdgeCasesSuite edge_cases_suite_test.go URL parsing edge cases
ConcurrencyEdgeCasesSuite edge_cases_suite_test.go Concurrent token validation
ExampleTestSuite testutil_example_test.go Example demonstrating patterns
AuthFlowBehaviourSuite auth_flow_behaviour_test.go Authentication flow behavior tests
SessionBehaviourSuite session_behaviour_test.go Session management behavior tests
EnhancedMocksSuite enhanced_mocks_suite_test.go Enhanced mock usage demonstration

Mock Types

The project provides two mocking patterns:

State-Based Mocks (Basic)

Located in main_test.go, mocks_test.go. Simple mocks that store data in struct fields.

Mock Interface Description
MockJWKCache JWKCacheInterface Simple state-based mock with JWKS/Err fields
MockTokenVerifier TokenVerifier Function-based mock for token verification
MockTokenExchanger TokenExchanger Function-based mock for token exchange
MockOAuthProvider http.Handler Full HTTP handler mock for OAuth provider simulation
MockSessionManager SessionManager State-based mock for session management
MockHTTPClient N/A Mock HTTP client with customizable responses

Usage:

mock := &MockJWKCache{
    JWKS: &JWKSet{Keys: []JWK{jwk}},
    Err:  nil,
}
tOidc := &TraefikOidc{
    jwkCache: mock,
    // ...
}

Enhanced State-Based Mocks (with Call Tracking)

Located in enhanced_mocks_test.go. State-based mocks with built-in call tracking and assertion helpers.

Mock Interface Description
EnhancedMockJWKCache JWKCacheInterface State-based with call tracking
EnhancedMockTokenVerifier TokenVerifier State-based with call tracking
EnhancedMockTokenExchanger TokenExchanger State-based with call tracking
EnhancedMockCacheInterface CacheInterface Functional cache with call tracking

Usage:

mock := &EnhancedMockJWKCache{
    JWKS: &JWKSet{Keys: []JWK{jwk}},
}

// Make calls
result, err := mock.GetJWKS(ctx, "https://example.com/jwks", nil)

// Verify calls were made
mock.AssertGetJWKSCalled(t)
mock.AssertGetJWKSCalledWith(t, "https://example.com/jwks")
mock.AssertGetJWKSCallCount(t, 1)

// Access call details
s.Equal(1, mock.GetJWKSCallCount())

Features:

  • Track all calls with parameters and timestamps
  • Built-in assertion helpers using testify
  • Thread-safe for concurrent tests
  • Reset() method to clear state between tests
  • LastCall() to inspect most recent call

Testify-Based Mocks

Located in testify_mocks_test.go. Mocks using testify's .On()/.Return() pattern for behavior verification.

Mock Interface Description
TestifyJWKCache JWKCacheInterface Testify mock with .On()/.Return()
TestifyTokenVerifier TokenVerifier Testify mock for token verification
TestifyTokenExchanger TokenExchanger Testify mock for token exchange
TestifyCacheInterface CacheInterface Testify mock for cache operations
TestifyHTTPClient N/A Testify mock for HTTP client
TestifyRoundTripper http.RoundTripper Testify mock for HTTP transport

Usage:

mock := &TestifyJWKCache{}
mock.On("GetJWKS", mock.Anything, "https://example.com/jwks", mock.Anything).
    Return(&JWKSet{Keys: []JWK{jwk}}, nil)

// After test
mock.AssertExpectations(t)

Testutil Package Mocks

Located in internal/testutil/mocks/. Generic mocks for testing the test infrastructure itself.

import "github.com/lukaszraczylo/traefikoidc/internal/testutil"

mock := testutil.NewJWKCacheMock()
mock.On("GetJWKS", mock.Anything, mock.Anything, mock.Anything).
    Return(&mocks.JWKSet{Keys: []mocks.JWK{{Kty: "RSA"}}}, nil)

Choosing the Right Mock

Use Case Recommended Mock
Simple return values only Basic state-based (MockJWKCache)
Return values + verify calls made Enhanced state-based (EnhancedMockJWKCache)
Complex call expectations Testify-based (TestifyJWKCache)
Verify call order/sequence Testify-based
HTTP endpoint simulation MockOAuthProvider
New testify suite tests Enhanced or Testify-based

Decision Guide:

  1. Basic State-Based: Use when you only need to control return values and don't care about verifying interactions.

  2. Enhanced State-Based: Use when you want to verify calls were made with specific parameters, but prefer simpler setup than testify's .On()/.Return() pattern.

  3. Testify-Based: Use when you need complex behavior like different returns per call, strict call ordering, or detailed expectation matching.

Token Fixtures

The testutil.TokenFixture generates JWT tokens for testing:

fixture, err := testutil.NewTokenFixture()

// Valid token with default claims
token, _ := fixture.ValidToken(nil)

// Token with custom claims
token, _ := fixture.ValidToken(map[string]interface{}{
    "email": "test@example.com",
    "roles": []string{"admin"},
})

// Expired token
token, _ := fixture.ExpiredToken()

// Token with specific roles/groups
token, _ := fixture.TokenWithRoles([]string{"admin", "user"})
token, _ := fixture.TokenWithGroups([]string{"developers"})

// Token with clock skew
token, _ := fixture.TokenWithSkew(-2 * time.Minute)  // expired 2 min ago
token, _ := fixture.TokenWithSkew(5 * time.Minute)   // expires in 5 min

// Token missing specific claims
token, _ := fixture.TokenMissingClaim("email", "sub")

// Malformed token
token := fixture.MalformedToken()  // "not.a.valid.jwt"

// Get JWKS for verification
jwks := fixture.GetJWKS()

Mock OIDC Server

The testutil.OIDCServer provides a fully functional mock OIDC provider:

// Default configuration
server := testutil.NewOIDCServer(nil)
defer server.Close()

// Custom configuration
config := testutil.DefaultServerConfig()
config.Issuer = "https://custom-issuer.com"
config.TokenError = &testutil.OIDCError{
    Error:       "invalid_grant",
    Description: "Authorization code expired",
}
server := testutil.NewOIDCServer(config)

// Provider-specific configurations
googleConfig := testutil.GoogleServerConfig()
azureConfig := testutil.AzureServerConfig()
auth0Config := testutil.Auth0ServerConfig()
keycloakConfig := testutil.KeycloakServerConfig()

// Behavior configurations
slowConfig := testutil.SlowServerConfig(100 * time.Millisecond)
rateLimitedConfig := testutil.RateLimitedServerConfig(5)  // Limit after 5 requests

Server Endpoints

Endpoint Description
/.well-known/openid-configuration OIDC discovery document
/authorize Authorization endpoint
/token Token exchange endpoint
/jwks JSON Web Key Set
/userinfo User information endpoint
/introspect Token introspection
/revoke Token revocation
/logout End session endpoint

Request Tracking

server := testutil.NewOIDCServer(nil)

// Make requests...

count := server.GetRequestCount()
requests := server.GetRequests()
server.Reset()  // Clear tracking

Writing Test Suites

Basic Suite Structure

type MyTestSuite struct {
    suite.Suite

    fixture *testutil.TokenFixture
    tOidc   *TraefikOidc
}

func (s *MyTestSuite) SetupSuite() {
    var err error
    s.fixture, err = testutil.NewTokenFixture()
    s.Require().NoError(err)
}

func (s *MyTestSuite) SetupTest() {
    // Per-test setup
    s.tOidc = &TraefikOidc{
        issuerURL: s.fixture.Issuer,
        // ...
    }
}

func (s *MyTestSuite) TearDownTest() {
    // Per-test cleanup
}

func (s *MyTestSuite) TestSomething() {
    token, err := s.fixture.ValidToken(nil)
    s.Require().NoError(err)

    err = s.tOidc.VerifyToken(token)
    s.NoError(err)
}

func TestMyTestSuite(t *testing.T) {
    suite.Run(t, new(MyTestSuite))
}

Table-Driven Tests

func (s *MyTestSuite) TestClockSkewEdgeCases() {
    testCases := []struct {
        name       string
        skew       time.Duration
        shouldPass bool
    }{
        {"valid_token", 5 * time.Minute, true},
        {"expired_within_tolerance", -1 * time.Minute, true},
        {"expired_beyond_tolerance", -10 * time.Minute, false},
    }

    for _, tc := range testCases {
        s.Run(tc.name, func() {
            token, err := s.fixture.TokenWithSkew(tc.skew)
            s.Require().NoError(err)

            err = s.tOidc.VerifyToken(token)
            if tc.shouldPass {
                s.NoError(err)
            } else {
                s.Error(err)
            }
        })
    }
}

Test Categories

Happy Path Tests

Test the expected successful scenarios:

  • Valid token verification
  • Successful token exchange
  • Session creation and retrieval
  • Cache operations

Error Case Tests

Test failure scenarios:

  • Expired tokens
  • Invalid signatures
  • Wrong issuer/audience
  • Network failures
  • Rate limiting

Edge Case Tests

Test boundary conditions:

  • Clock skew tolerance boundaries
  • Unicode/emoji in claims
  • Very large claim values
  • Concurrent access
  • Special characters in URLs

Best Practices

  1. Use fixtures for token generation - Don't manually construct JWTs
  2. Use mock servers for integration tests - Test against realistic OIDC behavior
  3. Always run with -race - Catch concurrency issues early
  4. Use testify assertions - Better error messages and cleaner code
  5. Clean up resources - Use t.Cleanup() or TearDownTest()
  6. Test edge cases systematically - Use table-driven tests