mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
2d1b04c637
* 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
391 lines
11 KiB
Markdown
391 lines
11 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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:**
|
|
```go
|
|
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:**
|
|
```go
|
|
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:**
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
server := testutil.NewOIDCServer(nil)
|
|
|
|
// Make requests...
|
|
|
|
count := server.GetRequestCount()
|
|
requests := server.GetRequests()
|
|
server.Reset() // Clear tracking
|
|
```
|
|
|
|
## Writing Test Suites
|
|
|
|
### Basic Suite Structure
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|