mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-13 04:07:37 +00:00
Cleanup [dec2025] (#101)
* Cleanup excessive comments. * Remove leftovers hanging around from previous refactor * Improve test coverage
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
# Testing Guide
|
||||
|
||||
Comprehensive testing infrastructure for traefikoidc.
|
||||
|
||||
## Overview
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Test files | 99 |
|
||||
| Lines of test code | ~65,500 |
|
||||
| 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": "[email protected]",
|
||||
"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
|
||||
Reference in New Issue
Block a user