Cleanup [dec2025] (#101)

* Cleanup excessive comments.

* Remove leftovers hanging around from previous refactor

* Improve test coverage
This commit is contained in:
2025-12-09 01:38:02 +00:00
committed by GitHub
parent 9126c74723
commit c474bbafd6
172 changed files with 25136 additions and 52201 deletions
+509
View File
@@ -0,0 +1,509 @@
package servers
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/lukaszraczylo/traefikoidc/internal/testutil/fixtures"
)
// OIDCServerConfig configures the mock OIDC server behavior
type OIDCServerConfig struct {
// Identity
Issuer string
// Discovery
ScopesSupported []string
ResponseTypesSupported []string
GrantTypesSupported []string
ClaimsSupported []string
TokenEndpointAuthMethods []string
// Token fixture for signing
TokenFixture *fixtures.TokenFixture
// Token endpoint behavior
TokenResponse map[string]interface{}
TokenError *OIDCError
TokenDelay time.Duration
RefreshResponse map[string]interface{}
RefreshError *OIDCError
// JWKS behavior
JWKSResponse map[string]interface{}
JWKSError *OIDCError
JWKSDelay time.Duration
// Introspection behavior
IntrospectionResponse map[string]interface{}
IntrospectionError *OIDCError
// Userinfo behavior
UserinfoResponse map[string]interface{}
UserinfoError *OIDCError
// Simulation flags
SimulateTimeout bool
TimeoutDuration time.Duration
RateLimitAfter int
FailAfterN int
FailWithStatus int
}
// OIDCError represents an OAuth error response
type OIDCError struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
// OIDCServer is a configurable mock OIDC provider
type OIDCServer struct {
*httptest.Server
Config *OIDCServerConfig
RequestCount int32
mu sync.Mutex
requests []*http.Request
}
// NewOIDCServer creates a new mock OIDC server
func NewOIDCServer(config *OIDCServerConfig) *OIDCServer {
if config == nil {
config = DefaultConfig()
}
server := &OIDCServer{
Config: config,
}
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/openid-configuration", server.handleDiscovery)
mux.HandleFunc("/token", server.handleToken)
mux.HandleFunc("/jwks", server.handleJWKS)
mux.HandleFunc("/authorize", server.handleAuthorize)
mux.HandleFunc("/userinfo", server.handleUserinfo)
mux.HandleFunc("/revoke", server.handleRevoke)
mux.HandleFunc("/introspect", server.handleIntrospect)
mux.HandleFunc("/logout", server.handleLogout)
server.Server = httptest.NewServer(mux)
// Update issuer to use actual server URL if not set
if config.Issuer == "" {
config.Issuer = server.URL
}
return server
}
// NewTLSServer creates a new mock OIDC server with TLS
func NewTLSServer(config *OIDCServerConfig) *OIDCServer {
if config == nil {
config = DefaultConfig()
}
server := &OIDCServer{
Config: config,
}
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/openid-configuration", server.handleDiscovery)
mux.HandleFunc("/token", server.handleToken)
mux.HandleFunc("/jwks", server.handleJWKS)
mux.HandleFunc("/authorize", server.handleAuthorize)
mux.HandleFunc("/userinfo", server.handleUserinfo)
mux.HandleFunc("/revoke", server.handleRevoke)
mux.HandleFunc("/introspect", server.handleIntrospect)
mux.HandleFunc("/logout", server.handleLogout)
server.Server = httptest.NewTLSServer(mux)
if config.Issuer == "" {
config.Issuer = server.URL
}
return server
}
// GetRequestCount returns the number of requests received
func (s *OIDCServer) GetRequestCount() int {
return int(atomic.LoadInt32(&s.RequestCount))
}
// GetRequests returns all recorded requests
func (s *OIDCServer) GetRequests() []*http.Request {
s.mu.Lock()
defer s.mu.Unlock()
return s.requests
}
// Reset clears request tracking
func (s *OIDCServer) Reset() {
atomic.StoreInt32(&s.RequestCount, 0)
s.mu.Lock()
s.requests = nil
s.mu.Unlock()
}
func (s *OIDCServer) recordRequest(r *http.Request) {
atomic.AddInt32(&s.RequestCount, 1)
s.mu.Lock()
s.requests = append(s.requests, r)
s.mu.Unlock()
}
func (s *OIDCServer) shouldFail() bool {
count := int(atomic.LoadInt32(&s.RequestCount))
if s.Config.FailAfterN > 0 && count > s.Config.FailAfterN {
return true
}
if s.Config.RateLimitAfter > 0 && count > s.Config.RateLimitAfter {
return true
}
return false
}
func (s *OIDCServer) handleDiscovery(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
if s.Config.SimulateTimeout {
time.Sleep(s.Config.TimeoutDuration)
return
}
discovery := map[string]interface{}{
"issuer": s.Config.Issuer,
"authorization_endpoint": s.Config.Issuer + "/authorize",
"token_endpoint": s.Config.Issuer + "/token",
"userinfo_endpoint": s.Config.Issuer + "/userinfo",
"jwks_uri": s.Config.Issuer + "/jwks",
"revocation_endpoint": s.Config.Issuer + "/revoke",
"introspection_endpoint": s.Config.Issuer + "/introspect",
"end_session_endpoint": s.Config.Issuer + "/logout",
"scopes_supported": s.Config.ScopesSupported,
"response_types_supported": s.Config.ResponseTypesSupported,
"grant_types_supported": s.Config.GrantTypesSupported,
"claims_supported": s.Config.ClaimsSupported,
"token_endpoint_auth_methods_supported": s.Config.TokenEndpointAuthMethods,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(discovery) // #nosec G104 - test server, error handling not critical
}
func (s *OIDCServer) handleToken(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
if s.Config.SimulateTimeout {
time.Sleep(s.Config.TimeoutDuration)
return
}
if s.Config.TokenDelay > 0 {
time.Sleep(s.Config.TokenDelay)
}
if s.shouldFail() {
status := http.StatusTooManyRequests
if s.Config.FailWithStatus > 0 {
status = s.Config.FailWithStatus
}
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(OIDCError{Error: "rate_limited"}) // #nosec G104
return
}
if s.Config.TokenError != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(s.Config.TokenError) // #nosec G104
return
}
_ = r.ParseForm() // #nosec G104
grantType := r.FormValue("grant_type")
var response map[string]interface{}
if grantType == "refresh_token" {
if s.Config.RefreshError != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(s.Config.RefreshError) // #nosec G104
return
}
response = s.Config.RefreshResponse
} else {
response = s.Config.TokenResponse
}
if response == nil {
response = s.defaultTokenResponse()
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response) // #nosec G104
}
func (s *OIDCServer) handleJWKS(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
if s.Config.SimulateTimeout {
time.Sleep(s.Config.TimeoutDuration)
return
}
if s.Config.JWKSDelay > 0 {
time.Sleep(s.Config.JWKSDelay)
}
if s.Config.JWKSError != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(s.Config.JWKSError) // #nosec G104
return
}
response := s.Config.JWKSResponse
if response == nil && s.Config.TokenFixture != nil {
response = s.Config.TokenFixture.GetJWKS()
}
if response == nil {
response = map[string]interface{}{"keys": []interface{}{}}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response) // #nosec G104
}
func (s *OIDCServer) handleAuthorize(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
// In real flow, this would redirect with code
// For testing, we return a simple page
state := r.URL.Query().Get("state")
redirectURI := r.URL.Query().Get("redirect_uri")
// Validate redirect URI to prevent open redirect vulnerability
if !isValidRedirectURI(redirectURI, s.URL) {
http.Error(w, "invalid redirect_uri", http.StatusBadRequest)
return
}
redirectURL := fmt.Sprintf("%s?code=test-auth-code&state=%s", redirectURI, state)
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func (s *OIDCServer) handleUserinfo(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
if s.Config.UserinfoError != nil {
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(s.Config.UserinfoError) // #nosec G104
return
}
response := s.Config.UserinfoResponse
if response == nil {
response = map[string]interface{}{
"sub": "test-subject",
"email": "[email protected]",
"name": "Test User",
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response) // #nosec G104
}
func (s *OIDCServer) handleRevoke(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
w.WriteHeader(http.StatusOK)
}
func (s *OIDCServer) handleIntrospect(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
if s.Config.IntrospectionError != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(s.Config.IntrospectionError) // #nosec G104
return
}
response := s.Config.IntrospectionResponse
if response == nil {
response = map[string]interface{}{
"active": true,
"sub": "test-subject",
"client_id": "test-client",
"exp": time.Now().Add(time.Hour).Unix(),
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response) // #nosec G104
}
func (s *OIDCServer) handleLogout(w http.ResponseWriter, r *http.Request) {
s.recordRequest(r)
postLogoutRedirect := r.URL.Query().Get("post_logout_redirect_uri")
if postLogoutRedirect != "" {
// Validate post-logout redirect URI to prevent open redirect vulnerability
if !isValidRedirectURI(postLogoutRedirect, s.URL) {
http.Error(w, "invalid post_logout_redirect_uri", http.StatusBadRequest)
return
}
http.Redirect(w, r, postLogoutRedirect, http.StatusFound)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Logged out")) // #nosec G104
}
func (s *OIDCServer) defaultTokenResponse() map[string]interface{} {
var idToken string
if s.Config.TokenFixture != nil {
idToken, _ = s.Config.TokenFixture.ValidToken(nil)
} else {
idToken = "mock-id-token"
}
return map[string]interface{}{
"access_token": "mock-access-token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mock-refresh-token",
"id_token": idToken,
}
}
// isValidRedirectURI validates that a redirect URI is safe to use.
// It ensures the URI is either:
// 1. A relative path (no scheme or host)
// 2. Points to the same host as the test server (localhost)
// 3. Points to a common test domain (127.0.0.1, localhost)
// This prevents open redirect vulnerabilities in the test server.
func isValidRedirectURI(redirectURI, serverURL string) bool {
if redirectURI == "" {
return false
}
parsed, err := url.Parse(redirectURI)
if err != nil {
return false
}
// Allow relative paths (no scheme means relative)
if parsed.Scheme == "" && parsed.Host == "" {
return true
}
// Parse the server URL to get its host
serverParsed, err := url.Parse(serverURL)
if err != nil {
return false
}
// Allow same host as test server
if parsed.Host == serverParsed.Host {
return true
}
// Allow common localhost variations used in tests
host := strings.ToLower(parsed.Hostname())
allowedHosts := []string{
"localhost",
"127.0.0.1",
"[::1]",
"example.com", // Common test domain
"myapp.com", // Common test domain
"test.example.com",
}
for _, allowed := range allowedHosts {
if host == allowed {
return true
}
}
return false
}
// DefaultConfig returns a default server configuration
func DefaultConfig() *OIDCServerConfig {
return &OIDCServerConfig{
ScopesSupported: []string{"openid", "profile", "email", "offline_access"},
ResponseTypesSupported: []string{"code", "token", "id_token"},
GrantTypesSupported: []string{"authorization_code", "refresh_token"},
ClaimsSupported: []string{"sub", "email", "name", "groups", "roles"},
TokenEndpointAuthMethods: []string{"client_secret_basic", "client_secret_post"},
TimeoutDuration: 30 * time.Second,
}
}
// GoogleConfig returns a Google-like server configuration
func GoogleConfig() *OIDCServerConfig {
config := DefaultConfig()
config.Issuer = "https://accounts.google.com"
config.ScopesSupported = []string{"openid", "profile", "email"}
// Google doesn't support offline_access, uses access_type=offline instead
return config
}
// AzureConfig returns an Azure AD-like server configuration
func AzureConfig() *OIDCServerConfig {
config := DefaultConfig()
config.Issuer = "https://login.microsoftonline.com/common/v2.0"
config.ScopesSupported = []string{"openid", "profile", "email", "offline_access"}
return config
}
// Auth0Config returns an Auth0-like server configuration
func Auth0Config() *OIDCServerConfig {
config := DefaultConfig()
config.ScopesSupported = []string{"openid", "profile", "email", "offline_access"}
return config
}
// KeycloakConfig returns a Keycloak-like server configuration
func KeycloakConfig() *OIDCServerConfig {
config := DefaultConfig()
config.ScopesSupported = []string{"openid", "profile", "email", "offline_access", "roles", "groups"}
return config
}
// SlowServerConfig returns a configuration that simulates slow responses
func SlowServerConfig(delay time.Duration) *OIDCServerConfig {
config := DefaultConfig()
config.TokenDelay = delay
config.JWKSDelay = delay
return config
}
// RateLimitedConfig returns a configuration that rate limits after N requests
func RateLimitedConfig(afterN int) *OIDCServerConfig {
config := DefaultConfig()
config.RateLimitAfter = afterN
return config
}
// FailingConfig returns a configuration that fails after N requests
func FailingConfig(afterN int, status int) *OIDCServerConfig {
config := DefaultConfig()
config.FailAfterN = afterN
config.FailWithStatus = status
return config
}
// TimeoutConfig returns a configuration that simulates timeouts
func TimeoutConfig(duration time.Duration) *OIDCServerConfig {
config := DefaultConfig()
config.SimulateTimeout = true
config.TimeoutDuration = duration
return config
}
+396
View File
@@ -0,0 +1,396 @@
package servers
import (
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/lukaszraczylo/traefikoidc/internal/testutil/fixtures"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewOIDCServer(t *testing.T) {
t.Run("creates server with default config", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
assert.NotNil(t, server)
assert.NotEmpty(t, server.URL)
})
t.Run("creates server with custom config", func(t *testing.T) {
config := &OIDCServerConfig{
Issuer: "https://custom-issuer.com",
ScopesSupported: []string{"openid", "custom"},
}
server := NewOIDCServer(config)
defer server.Close()
assert.NotNil(t, server)
assert.Equal(t, "https://custom-issuer.com", server.Config.Issuer)
})
}
func TestDiscoveryEndpoint(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.Get(server.URL + "/.well-known/openid-configuration")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var discovery map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&discovery)
require.NoError(t, err)
assert.Equal(t, server.URL, discovery["issuer"])
assert.Contains(t, discovery["token_endpoint"], "/token")
assert.Contains(t, discovery["jwks_uri"], "/jwks")
assert.Contains(t, discovery["authorization_endpoint"], "/authorize")
}
func TestTokenEndpoint(t *testing.T) {
t.Run("returns default token response", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"authorization_code"},
"code": {"test-code"},
})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var tokenResp map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
require.NoError(t, err)
assert.NotEmpty(t, tokenResp["access_token"])
assert.NotEmpty(t, tokenResp["refresh_token"])
})
t.Run("returns configured error", func(t *testing.T) {
config := DefaultConfig()
config.TokenError = &OIDCError{
Error: "invalid_grant",
Description: "The authorization code is invalid",
}
server := NewOIDCServer(config)
defer server.Close()
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"authorization_code"},
"code": {"test-code"},
})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var errResp OIDCError
err = json.NewDecoder(resp.Body).Decode(&errResp)
require.NoError(t, err)
assert.Equal(t, "invalid_grant", errResp.Error)
})
t.Run("handles refresh token grant", func(t *testing.T) {
config := DefaultConfig()
config.RefreshResponse = map[string]interface{}{
"access_token": "new-access-token",
"expires_in": 3600,
}
server := NewOIDCServer(config)
defer server.Close()
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"refresh_token"},
"refresh_token": {"test-refresh-token"},
})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var tokenResp map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
require.NoError(t, err)
assert.Equal(t, "new-access-token", tokenResp["access_token"])
})
}
func TestJWKSEndpoint(t *testing.T) {
t.Run("returns empty JWKS without fixture", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.Get(server.URL + "/jwks")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var jwks map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&jwks)
require.NoError(t, err)
assert.Contains(t, jwks, "keys")
})
t.Run("returns JWKS from fixture", func(t *testing.T) {
fixture, err := fixtures.NewTokenFixture()
require.NoError(t, err)
config := DefaultConfig()
config.TokenFixture = fixture
server := NewOIDCServer(config)
defer server.Close()
resp, err := http.Get(server.URL + "/jwks")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var jwks map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&jwks)
require.NoError(t, err)
keys, ok := jwks["keys"].([]interface{})
require.True(t, ok)
assert.Len(t, keys, 1)
})
}
func TestUserinfoEndpoint(t *testing.T) {
t.Run("returns default userinfo", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.Get(server.URL + "/userinfo")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var userinfo map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&userinfo)
require.NoError(t, err)
assert.NotEmpty(t, userinfo["sub"])
assert.NotEmpty(t, userinfo["email"])
})
t.Run("returns configured userinfo", func(t *testing.T) {
config := DefaultConfig()
config.UserinfoResponse = map[string]interface{}{
"sub": "custom-sub",
"email": "[email protected]",
"name": "Custom User",
}
server := NewOIDCServer(config)
defer server.Close()
resp, err := http.Get(server.URL + "/userinfo")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var userinfo map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&userinfo)
require.NoError(t, err)
assert.Equal(t, "[email protected]", userinfo["email"])
})
}
func TestIntrospectionEndpoint(t *testing.T) {
t.Run("returns active token", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.PostForm(server.URL+"/introspect", map[string][]string{
"token": {"test-token"},
})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var introspection map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&introspection)
require.NoError(t, err)
assert.Equal(t, true, introspection["active"])
})
}
func TestRevocationEndpoint(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
resp, err := http.PostForm(server.URL+"/revoke", map[string][]string{
"token": {"test-token"},
})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestLogoutEndpoint(t *testing.T) {
t.Run("returns OK without redirect", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(server.URL + "/logout")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("redirects with post_logout_redirect_uri", func(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(server.URL + "/logout?post_logout_redirect_uri=https://example.com/logged-out")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
assert.Equal(t, "https://example.com/logged-out", resp.Header.Get("Location"))
})
}
func TestRequestTracking(t *testing.T) {
server := NewOIDCServer(nil)
defer server.Close()
assert.Equal(t, 0, server.GetRequestCount())
http.Get(server.URL + "/.well-known/openid-configuration")
assert.Equal(t, 1, server.GetRequestCount())
http.Get(server.URL + "/jwks")
assert.Equal(t, 2, server.GetRequestCount())
requests := server.GetRequests()
assert.Len(t, requests, 2)
server.Reset()
assert.Equal(t, 0, server.GetRequestCount())
assert.Len(t, server.GetRequests(), 0)
}
func TestRateLimiting(t *testing.T) {
config := RateLimitedConfig(2)
server := NewOIDCServer(config)
defer server.Close()
// First 2 requests should succeed
for i := 0; i < 2; i++ {
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"authorization_code"},
"code": {"test-code"},
})
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
// Third request should be rate limited
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"authorization_code"},
"code": {"test-code"},
})
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
}
func TestSlowServer(t *testing.T) {
config := SlowServerConfig(100 * time.Millisecond)
server := NewOIDCServer(config)
defer server.Close()
start := time.Now()
resp, err := http.PostForm(server.URL+"/token", map[string][]string{
"grant_type": {"authorization_code"},
"code": {"test-code"},
})
elapsed := time.Since(start)
require.NoError(t, err)
resp.Body.Close()
assert.GreaterOrEqual(t, elapsed.Milliseconds(), int64(100))
}
func TestProviderConfigs(t *testing.T) {
t.Run("GoogleConfig", func(t *testing.T) {
config := GoogleConfig()
assert.Equal(t, "https://accounts.google.com", config.Issuer)
assert.NotContains(t, config.ScopesSupported, "offline_access")
})
t.Run("AzureConfig", func(t *testing.T) {
config := AzureConfig()
assert.Contains(t, config.Issuer, "microsoftonline.com")
assert.Contains(t, config.ScopesSupported, "offline_access")
})
t.Run("Auth0Config", func(t *testing.T) {
config := Auth0Config()
assert.Contains(t, config.ScopesSupported, "offline_access")
})
t.Run("KeycloakConfig", func(t *testing.T) {
config := KeycloakConfig()
assert.Contains(t, config.ScopesSupported, "roles")
assert.Contains(t, config.ScopesSupported, "groups")
})
}
func TestTimeoutConfig(t *testing.T) {
config := TimeoutConfig(50 * time.Millisecond)
server := NewOIDCServer(config)
defer server.Close()
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
start := time.Now()
resp, err := client.Get(server.URL + "/.well-known/openid-configuration")
elapsed := time.Since(start)
// Either timeout or empty response
if err == nil {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
// With timeout simulation, response body may be empty
assert.True(t, len(body) == 0 || elapsed >= 50*time.Millisecond)
}
}