mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
fixup! fixup! fixup! Further pursue of perfection.
This commit is contained in:
@@ -0,0 +1,736 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mockLegacySettings implements LegacySettings for testing
|
||||
type mockLegacySettings struct {
|
||||
issuerURL string
|
||||
authURL string
|
||||
scopes []string
|
||||
pkceEnabled bool
|
||||
clientID string
|
||||
refreshGracePeriod time.Duration
|
||||
overrideScopes bool
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) GetIssuerURL() string {
|
||||
return m.issuerURL
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) GetAuthURL() string {
|
||||
return m.authURL
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) GetScopes() []string {
|
||||
return m.scopes
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) IsPKCEEnabled() bool {
|
||||
return m.pkceEnabled
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) GetClientID() string {
|
||||
return m.clientID
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) GetRefreshGracePeriod() time.Duration {
|
||||
return m.refreshGracePeriod
|
||||
}
|
||||
|
||||
func (m *mockLegacySettings) IsOverrideScopes() bool {
|
||||
return m.overrideScopes
|
||||
}
|
||||
|
||||
func TestNewAdapter(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
authURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
scopes: []string{"openid", "email"},
|
||||
pkceEnabled: true,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
if adapter == nil {
|
||||
t.Fatal("expected non-nil adapter")
|
||||
}
|
||||
|
||||
if adapter.provider != provider {
|
||||
t.Error("expected provider to be set correctly")
|
||||
}
|
||||
|
||||
if adapter.legacySettings != settings {
|
||||
t.Error("expected legacy settings to be set correctly")
|
||||
}
|
||||
|
||||
if adapter.tokenVerifier != verifier {
|
||||
t.Error("expected token verifier to be set correctly")
|
||||
}
|
||||
|
||||
if adapter.tokenCache != cache {
|
||||
t.Error("expected token cache to be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_GetType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider OIDCProvider
|
||||
expectedType ProviderType
|
||||
}{
|
||||
{
|
||||
name: "Google provider",
|
||||
provider: NewGoogleProvider(),
|
||||
expectedType: ProviderTypeGoogle,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
provider: NewAzureProvider(),
|
||||
expectedType: ProviderTypeAzure,
|
||||
},
|
||||
{
|
||||
name: "Generic provider",
|
||||
provider: NewGenericProvider(),
|
||||
expectedType: ProviderTypeGeneric,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings := &mockLegacySettings{clientID: "test-client"}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
adapter := NewAdapter(tt.provider, settings, verifier, cache)
|
||||
providerType := adapter.GetType()
|
||||
|
||||
if providerType != tt.expectedType {
|
||||
t.Errorf("expected provider type %d, got %d", tt.expectedType, providerType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_ValidateTokens(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{
|
||||
refreshGracePeriod: time.Minute * 5,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"valid-token": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *mockSession
|
||||
expectedResult *ValidationResult
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid authenticated session",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "valid-token",
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthenticated with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthenticated without refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
},
|
||||
expectedResult: &ValidationResult{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := adapter.ValidateTokens(tt.session)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("expected Authenticated %t, got %t", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("expected NeedsRefresh %t, got %t", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("expected IsExpired %t, got %t", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_BuildAuthURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider OIDCProvider
|
||||
settings *mockLegacySettings
|
||||
redirectURL string
|
||||
state string
|
||||
nonce string
|
||||
codeChallenge string
|
||||
expectedSubstrs []string
|
||||
unexpectedSubstrs []string
|
||||
}{
|
||||
{
|
||||
name: "Google provider without override scopes",
|
||||
provider: NewGoogleProvider(),
|
||||
settings: &mockLegacySettings{
|
||||
clientID: "google-client-id",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
authURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
scopes: []string{"openid", "email", "profile"},
|
||||
pkceEnabled: true,
|
||||
overrideScopes: false,
|
||||
},
|
||||
redirectURL: "https://example.com/callback",
|
||||
state: "random-state",
|
||||
nonce: "random-nonce",
|
||||
codeChallenge: "code-challenge",
|
||||
expectedSubstrs: []string{
|
||||
"client_id=google-client-id",
|
||||
"response_type=code",
|
||||
"redirect_uri=https%3A%2F%2Fexample.com%2Fcallback",
|
||||
"state=random-state",
|
||||
"nonce=random-nonce",
|
||||
"code_challenge=code-challenge",
|
||||
"code_challenge_method=S256",
|
||||
"access_type=offline",
|
||||
"prompt=consent",
|
||||
"scope=openid+email+profile",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Azure provider with override scopes",
|
||||
provider: NewAzureProvider(),
|
||||
settings: &mockLegacySettings{
|
||||
clientID: "azure-client-id",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant",
|
||||
authURL: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize",
|
||||
scopes: []string{"openid", "offline_access"},
|
||||
pkceEnabled: false,
|
||||
overrideScopes: true,
|
||||
},
|
||||
redirectURL: "https://example.com/azure-callback",
|
||||
state: "azure-state",
|
||||
nonce: "azure-nonce",
|
||||
codeChallenge: "",
|
||||
expectedSubstrs: []string{
|
||||
"client_id=azure-client-id",
|
||||
"response_type=code",
|
||||
"redirect_uri=https%3A%2F%2Fexample.com%2Fazure-callback",
|
||||
"state=azure-state",
|
||||
"nonce=azure-nonce",
|
||||
"response_mode=query",
|
||||
"scope=openid+offline_access",
|
||||
},
|
||||
unexpectedSubstrs: []string{
|
||||
"code_challenge",
|
||||
"access_type",
|
||||
"prompt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Generic provider with relative auth URL",
|
||||
provider: NewGenericProvider(),
|
||||
settings: &mockLegacySettings{
|
||||
clientID: "generic-client-id",
|
||||
issuerURL: "https://keycloak.example.com/auth/realms/master",
|
||||
authURL: "/auth/realms/master/protocol/openid-connect/auth",
|
||||
scopes: []string{"openid", "email"},
|
||||
pkceEnabled: false,
|
||||
overrideScopes: false,
|
||||
},
|
||||
redirectURL: "https://example.com/generic-callback",
|
||||
state: "generic-state",
|
||||
nonce: "generic-nonce",
|
||||
codeChallenge: "",
|
||||
expectedSubstrs: []string{
|
||||
"keycloak.example.com",
|
||||
"client_id=generic-client-id",
|
||||
"response_type=code",
|
||||
"scope=openid+email+offline_access", // Generic provider adds offline_access
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PKCE disabled",
|
||||
provider: NewGoogleProvider(),
|
||||
settings: &mockLegacySettings{
|
||||
clientID: "google-client-id-no-pkce",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
authURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
scopes: []string{"openid", "email"},
|
||||
pkceEnabled: false,
|
||||
overrideScopes: false,
|
||||
},
|
||||
redirectURL: "https://example.com/callback",
|
||||
state: "state-no-pkce",
|
||||
nonce: "nonce-no-pkce",
|
||||
codeChallenge: "should-be-ignored",
|
||||
expectedSubstrs: []string{
|
||||
"client_id=google-client-id-no-pkce",
|
||||
"state=state-no-pkce",
|
||||
"nonce=nonce-no-pkce",
|
||||
},
|
||||
unexpectedSubstrs: []string{
|
||||
"code_challenge",
|
||||
"code_challenge_method",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
adapter := NewAdapter(tt.provider, tt.settings, verifier, cache)
|
||||
|
||||
authURL := adapter.BuildAuthURL(tt.redirectURL, tt.state, tt.nonce, tt.codeChallenge)
|
||||
|
||||
if authURL == "" {
|
||||
t.Fatal("expected non-empty auth URL")
|
||||
}
|
||||
|
||||
for _, expectedSubstr := range tt.expectedSubstrs {
|
||||
if !strings.Contains(authURL, expectedSubstr) {
|
||||
t.Errorf("expected auth URL to contain %q, got %q", expectedSubstr, authURL)
|
||||
}
|
||||
}
|
||||
|
||||
for _, unexpectedSubstr := range tt.unexpectedSubstrs {
|
||||
if strings.Contains(authURL, unexpectedSubstr) {
|
||||
t.Errorf("expected auth URL to NOT contain %q, got %q", unexpectedSubstr, authURL)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_BuildAuthURL_ErrorCases(t *testing.T) {
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
t.Run("invalid issuer URL", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "://invalid-url",
|
||||
authURL: "/relative/path",
|
||||
scopes: []string{"openid"},
|
||||
overrideScopes: false,
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL("https://example.com/callback", "state", "nonce", "")
|
||||
|
||||
if authURL != "" {
|
||||
t.Errorf("expected empty auth URL for invalid issuer URL, got %q", authURL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid auth URL", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://example.com",
|
||||
authURL: "://invalid-auth-url",
|
||||
scopes: []string{"openid"},
|
||||
overrideScopes: false,
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL("https://example.com/callback", "state", "nonce", "")
|
||||
|
||||
if authURL != "" {
|
||||
t.Errorf("expected empty auth URL for invalid auth URL, got %q", authURL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid absolute auth URL", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://example.com",
|
||||
authURL: "://invalid-absolute-url",
|
||||
scopes: []string{"openid"},
|
||||
overrideScopes: false,
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL("https://example.com/callback", "state", "nonce", "")
|
||||
|
||||
if authURL != "" {
|
||||
t.Errorf("expected empty auth URL for invalid absolute auth URL, got %q", authURL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("provider BuildAuthParams error", func(t *testing.T) {
|
||||
// Create a mock provider that returns an error
|
||||
mockProvider := &mockProviderWithError{}
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://example.com",
|
||||
authURL: "https://example.com/auth",
|
||||
scopes: []string{"openid"},
|
||||
overrideScopes: false,
|
||||
}
|
||||
|
||||
adapter := NewAdapter(mockProvider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL("https://example.com/callback", "state", "nonce", "")
|
||||
|
||||
if authURL != "" {
|
||||
t.Errorf("expected empty auth URL when provider returns error, got %q", authURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mockProviderWithError is a test helper that returns errors from BuildAuthParams
|
||||
type mockProviderWithError struct {
|
||||
*BaseProvider
|
||||
}
|
||||
|
||||
func (m *mockProviderWithError) GetType() ProviderType {
|
||||
return ProviderTypeGeneric
|
||||
}
|
||||
|
||||
func (m *mockProviderWithError) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
|
||||
return nil, fmt.Errorf("mock error from BuildAuthParams")
|
||||
}
|
||||
|
||||
func TestAdapter_ConcurrentAccess(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
authURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
scopes: []string{"openid", "email"},
|
||||
pkceEnabled: true,
|
||||
refreshGracePeriod: time.Minute,
|
||||
overrideScopes: false,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"valid-token": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 50
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Test concurrent access to adapter methods
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
// Test GetType
|
||||
providerType := adapter.GetType()
|
||||
if providerType != ProviderTypeGoogle {
|
||||
t.Errorf("worker %d: expected Google provider type", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test BuildAuthURL
|
||||
authURL := adapter.BuildAuthURL(
|
||||
fmt.Sprintf("https://example.com/callback-%d", workerID),
|
||||
fmt.Sprintf("state-%d-%d", workerID, j),
|
||||
fmt.Sprintf("nonce-%d-%d", workerID, j),
|
||||
fmt.Sprintf("challenge-%d-%d", workerID, j),
|
||||
)
|
||||
if authURL == "" {
|
||||
t.Errorf("worker %d: expected non-empty auth URL", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateTokens
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "valid-token",
|
||||
accessToken: fmt.Sprintf("access-token-%d", workerID),
|
||||
refreshToken: fmt.Sprintf("refresh-token-%d", workerID),
|
||||
}
|
||||
|
||||
result, err := adapter.ValidateTokens(session)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in ValidateTokens: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if !result.Authenticated {
|
||||
t.Errorf("worker %d: expected authenticated result", workerID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_MemorySafety(t *testing.T) {
|
||||
const numIterations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numIterations; i++ {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: fmt.Sprintf("client-%d", i),
|
||||
issuerURL: fmt.Sprintf("https://example%d.com", i),
|
||||
authURL: fmt.Sprintf("https://example%d.com/auth", i),
|
||||
scopes: []string{"openid", "email"},
|
||||
pkceEnabled: i%2 == 0, // Alternate PKCE setting
|
||||
refreshGracePeriod: time.Minute,
|
||||
overrideScopes: i%3 == 0, // Alternate override setting
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
fmt.Sprintf("token-%d", i): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
// Exercise all adapter methods
|
||||
_ = adapter.GetType()
|
||||
_ = adapter.BuildAuthURL("https://example.com/callback", "state", "nonce", "challenge")
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: fmt.Sprintf("token-%d", i),
|
||||
accessToken: fmt.Sprintf("access-%d", i),
|
||||
refreshToken: fmt.Sprintf("refresh-%d", i),
|
||||
}
|
||||
_, _ = adapter.ValidateTokens(session)
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_EdgeCases(t *testing.T) {
|
||||
t.Run("empty parameters", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "",
|
||||
issuerURL: "",
|
||||
authURL: "",
|
||||
scopes: []string{},
|
||||
pkceEnabled: false,
|
||||
overrideScopes: false,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL("", "", "", "")
|
||||
|
||||
// Should not crash, but may return empty or invalid URL
|
||||
if authURL != "" {
|
||||
t.Logf("Got auth URL with empty parameters: %s", authURL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long parameters", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
longString := strings.Repeat("a", 5000)
|
||||
settings := &mockLegacySettings{
|
||||
clientID: longString,
|
||||
issuerURL: "https://example.com/" + longString,
|
||||
authURL: "https://example.com/" + longString + "/auth",
|
||||
scopes: []string{"openid", longString},
|
||||
pkceEnabled: true,
|
||||
overrideScopes: false,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL(
|
||||
"https://example.com/callback",
|
||||
longString,
|
||||
longString,
|
||||
longString,
|
||||
)
|
||||
|
||||
// Should not crash
|
||||
if authURL == "" {
|
||||
t.Log("Long parameters resulted in empty auth URL")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("special characters in parameters", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "[email protected]",
|
||||
issuerURL: "https://example.com/auth?param=value&other=test",
|
||||
authURL: "https://example.com/auth/endpoint?default=param",
|
||||
scopes: []string{"openid", "email+special", "profile/test"},
|
||||
pkceEnabled: true,
|
||||
overrideScopes: false,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
authURL := adapter.BuildAuthURL(
|
||||
"https://example.com/callback?return=url",
|
||||
"state+with/special=chars&more",
|
||||
"nonce_with_underscores",
|
||||
"challenge-with-dashes",
|
||||
)
|
||||
|
||||
if authURL == "" {
|
||||
t.Error("expected non-empty auth URL with special characters")
|
||||
}
|
||||
|
||||
// Verify URL is properly encoded
|
||||
if !strings.Contains(authURL, "%") {
|
||||
t.Error("expected auth URL to contain URL encoding")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkAdapter_GetType(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{clientID: "test-client"}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
adapter.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdapter_BuildAuthURL(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{
|
||||
clientID: "test-client",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
authURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
scopes: []string{"openid", "email", "profile"},
|
||||
pkceEnabled: true,
|
||||
overrideScopes: false,
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
adapter.BuildAuthURL(
|
||||
"https://example.com/callback",
|
||||
"test-state",
|
||||
"test-nonce",
|
||||
"test-challenge",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdapter_ValidateTokens(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
settings := &mockLegacySettings{refreshGracePeriod: time.Minute}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"test-token": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
adapter := NewAdapter(provider, settings, verifier, cache)
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "test-token",
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := adapter.ValidateTokens(session)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mockSession implements the Session interface for testing
|
||||
type mockSession struct {
|
||||
idToken string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
authenticated bool
|
||||
}
|
||||
|
||||
func (m *mockSession) GetIDToken() string {
|
||||
return m.idToken
|
||||
}
|
||||
|
||||
func (m *mockSession) GetAccessToken() string {
|
||||
return m.accessToken
|
||||
}
|
||||
|
||||
func (m *mockSession) GetRefreshToken() string {
|
||||
return m.refreshToken
|
||||
}
|
||||
|
||||
func (m *mockSession) GetAuthenticated() bool {
|
||||
return m.authenticated
|
||||
}
|
||||
|
||||
// mockTokenVerifier implements TokenVerifier for testing
|
||||
type mockTokenVerifier struct {
|
||||
shouldFail bool
|
||||
expiredTokens map[string]bool
|
||||
}
|
||||
|
||||
func (m *mockTokenVerifier) VerifyToken(token string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("token verification failed")
|
||||
}
|
||||
if m.expiredTokens != nil && m.expiredTokens[token] {
|
||||
return errors.New("token has expired")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockTokenCache implements TokenCache for testing
|
||||
type mockTokenCache struct {
|
||||
data map[string]map[string]interface{}
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) Get(key string) (map[string]interface{}, bool) {
|
||||
if m.data == nil {
|
||||
return nil, false
|
||||
}
|
||||
claims, exists := m.data[key]
|
||||
return claims, exists
|
||||
}
|
||||
|
||||
func TestNewAzureProvider(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("expected non-nil Azure provider")
|
||||
}
|
||||
|
||||
if provider.BaseProvider == nil {
|
||||
t.Fatal("expected non-nil BaseProvider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_GetType(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
providerType := provider.GetType()
|
||||
|
||||
if providerType != ProviderTypeAzure {
|
||||
t.Errorf("expected provider type %d, got %d", ProviderTypeAzure, providerType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
expectedCapabilities := ProviderCapabilities{
|
||||
SupportsRefreshTokens: true,
|
||||
RequiresOfflineAccessScope: true,
|
||||
PreferredTokenValidation: "access",
|
||||
}
|
||||
|
||||
if capabilities.SupportsRefreshTokens != expectedCapabilities.SupportsRefreshTokens {
|
||||
t.Errorf("expected SupportsRefreshTokens %t, got %t", expectedCapabilities.SupportsRefreshTokens, capabilities.SupportsRefreshTokens)
|
||||
}
|
||||
|
||||
if capabilities.RequiresOfflineAccessScope != expectedCapabilities.RequiresOfflineAccessScope {
|
||||
t.Errorf("expected RequiresOfflineAccessScope %t, got %t", expectedCapabilities.RequiresOfflineAccessScope, capabilities.RequiresOfflineAccessScope)
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != expectedCapabilities.PreferredTokenValidation {
|
||||
t.Errorf("expected PreferredTokenValidation %q, got %q", expectedCapabilities.PreferredTokenValidation, capabilities.PreferredTokenValidation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseParams url.Values
|
||||
scopes []string
|
||||
expectOfflineAccess bool
|
||||
expectResponseMode bool
|
||||
}{
|
||||
{
|
||||
name: "basic params with offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "offline_access", "email"},
|
||||
expectOfflineAccess: true,
|
||||
expectResponseMode: true,
|
||||
},
|
||||
{
|
||||
name: "basic params without offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "email"},
|
||||
expectOfflineAccess: true, // Should be added automatically
|
||||
expectResponseMode: true,
|
||||
},
|
||||
{
|
||||
name: "empty scopes",
|
||||
baseParams: url.Values{},
|
||||
scopes: []string{},
|
||||
expectOfflineAccess: true, // Should be added automatically
|
||||
expectResponseMode: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authParams, err := provider.BuildAuthParams(tt.baseParams, tt.scopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Fatal("expected non-nil auth params")
|
||||
}
|
||||
|
||||
// Check response_mode is set
|
||||
if tt.expectResponseMode {
|
||||
responseMode := authParams.URLValues.Get("response_mode")
|
||||
if responseMode != "query" {
|
||||
t.Errorf("expected response_mode 'query', got %q", responseMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Check offline_access scope
|
||||
if tt.expectOfflineAccess {
|
||||
hasOfflineAccess := false
|
||||
for _, scope := range authParams.Scopes {
|
||||
if scope == "offline_access" {
|
||||
hasOfflineAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOfflineAccess {
|
||||
t.Error("expected offline_access scope to be present")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify other parameters are preserved
|
||||
for key, values := range tt.baseParams {
|
||||
if key == "response_mode" {
|
||||
continue // This gets overridden
|
||||
}
|
||||
paramValues := authParams.URLValues[key]
|
||||
if len(paramValues) != len(values) {
|
||||
t.Errorf("expected %d values for param %s, got %d", len(values), key, len(paramValues))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_ValidateTokens(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *mockSession
|
||||
verifier *mockTokenVerifier
|
||||
cache *mockTokenCache
|
||||
refreshGracePeriod time.Duration
|
||||
expectedResult *ValidationResult
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "unauthenticated with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthenticated without refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with valid JWT access token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "header.payload.signature",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"header.payload.signature": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with invalid access token but valid ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "header.payload.signature",
|
||||
idToken: "id.token.here",
|
||||
},
|
||||
verifier: &mockTokenVerifier{shouldFail: true},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"id.token.here": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with opaque access token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "opaque-token-no-dots",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with ID token only",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "id.token.here",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"id.token.here": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expired ID token with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "expired.token.here",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{
|
||||
expiredTokens: map[string]bool{
|
||||
"expired.token.here": true,
|
||||
},
|
||||
},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated but no tokens",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := provider.ValidateTokens(tt.session, tt.verifier, tt.cache, tt.refreshGracePeriod)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("expected Authenticated %t, got %t", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("expected NeedsRefresh %t, got %t", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("expected IsExpired %t, got %t", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
// Azure provider uses BaseProvider's ValidateConfig which always returns nil
|
||||
err := provider.ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from ValidateConfig: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_HandleTokenRefresh(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
// Test that HandleTokenRefresh doesn't fail
|
||||
tokenData := &TokenResult{
|
||||
IDToken: "id-token",
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
}
|
||||
|
||||
err := provider.HandleTokenRefresh(tokenData)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from HandleTokenRefresh: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_ConcurrentAccess(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 50
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Test concurrent access to provider methods
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: fmt.Sprintf("access-token-%d", workerID),
|
||||
idToken: fmt.Sprintf("id-token-%d", workerID),
|
||||
refreshToken: fmt.Sprintf("refresh-token-%d", workerID),
|
||||
}
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
fmt.Sprintf("access-token-%d", workerID): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
fmt.Sprintf("id-token-%d", workerID): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
// Test GetType
|
||||
if provider.GetType() != ProviderTypeAzure {
|
||||
t.Errorf("worker %d: expected Azure provider type", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test GetCapabilities
|
||||
capabilities := provider.GetCapabilities()
|
||||
if !capabilities.SupportsRefreshTokens {
|
||||
t.Errorf("worker %d: expected refresh token support", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateTokens
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in ValidateTokens: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if !result.Authenticated {
|
||||
t.Errorf("worker %d: expected authenticated result", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test BuildAuthParams
|
||||
baseParams := url.Values{"client_id": []string{fmt.Sprintf("client-%d", workerID)}}
|
||||
scopes := []string{"openid", "email"}
|
||||
authParams, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in BuildAuthParams: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if authParams == nil {
|
||||
t.Errorf("worker %d: expected non-nil auth params", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateConfig
|
||||
err = provider.ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in ValidateConfig: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_MemorySafety(t *testing.T) {
|
||||
const numIterations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numIterations; i++ {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: fmt.Sprintf("access-token-%d.payload.signature", i),
|
||||
idToken: fmt.Sprintf("id-token-%d.payload.signature", i),
|
||||
refreshToken: fmt.Sprintf("refresh-token-%d", i),
|
||||
}
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
fmt.Sprintf("access-token-%d.payload.signature", i): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Exercise all provider methods
|
||||
_ = provider.GetType()
|
||||
_ = provider.GetCapabilities()
|
||||
_, _ = provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
_, _ = provider.BuildAuthParams(url.Values{}, []string{"openid"})
|
||||
_ = provider.ValidateConfig()
|
||||
_ = provider.HandleTokenRefresh(&TokenResult{})
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider_EdgeCases(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
t.Run("nil session", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Expected behavior for nil session
|
||||
t.Logf("Recovered from expected panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
_, err := provider.ValidateTokens(nil, verifier, cache, time.Minute)
|
||||
if err == nil {
|
||||
t.Error("expected error with nil session")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil verifier", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
session := &mockSession{authenticated: true, idToken: "test.token.here"}
|
||||
cache := &mockTokenCache{}
|
||||
_, err := provider.ValidateTokens(session, nil, cache, time.Minute)
|
||||
if err == nil {
|
||||
t.Error("expected error with nil verifier")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil cache", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic with nil cache: %v", r)
|
||||
}
|
||||
}()
|
||||
session := &mockSession{authenticated: true, accessToken: "test.token.here"}
|
||||
verifier := &mockTokenVerifier{}
|
||||
_, err := provider.ValidateTokens(session, verifier, nil, time.Minute)
|
||||
if err == nil {
|
||||
t.Error("expected error with nil cache")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty tokens", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "",
|
||||
idToken: "",
|
||||
refreshToken: "",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with empty tokens: %v", err)
|
||||
}
|
||||
if !result.IsExpired {
|
||||
t.Error("expected IsExpired=true for empty tokens without refresh token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed JWT tokens", func(t *testing.T) {
|
||||
malformedTokens := []string{
|
||||
"not.enough.parts",
|
||||
"too.many.parts.in.this.token",
|
||||
"",
|
||||
"single-part-token",
|
||||
}
|
||||
|
||||
for _, token := range malformedTokens {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: token,
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with malformed token %q: %v", token, err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Errorf("expected non-nil result for malformed token %q", token)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long tokens", func(t *testing.T) {
|
||||
longToken := strings.Repeat("a", 10000) + "." + strings.Repeat("b", 10000) + "." + strings.Repeat("c", 10000)
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: longToken,
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
longToken: {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with very long token: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("expected non-nil result with very long token")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkAzureProvider_GetType(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAzureProvider_GetCapabilities(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAzureProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
baseParams := url.Values{"client_id": []string{"test-client"}}
|
||||
scopes := []string{"openid", "email", "profile"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAzureProvider_ValidateTokens(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "header.payload.signature",
|
||||
idToken: "id.token.here",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"header.payload.signature": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBaseProvider_GetType(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
providerType := provider.GetType()
|
||||
|
||||
if providerType != ProviderTypeGeneric {
|
||||
t.Errorf("expected provider type %d, got %d", ProviderTypeGeneric, providerType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
expectedCapabilities := ProviderCapabilities{
|
||||
SupportsRefreshTokens: true,
|
||||
RequiresOfflineAccessScope: true,
|
||||
PreferredTokenValidation: "id",
|
||||
}
|
||||
|
||||
if capabilities.SupportsRefreshTokens != expectedCapabilities.SupportsRefreshTokens {
|
||||
t.Errorf("expected SupportsRefreshTokens %t, got %t", expectedCapabilities.SupportsRefreshTokens, capabilities.SupportsRefreshTokens)
|
||||
}
|
||||
|
||||
if capabilities.RequiresOfflineAccessScope != expectedCapabilities.RequiresOfflineAccessScope {
|
||||
t.Errorf("expected RequiresOfflineAccessScope %t, got %t", expectedCapabilities.RequiresOfflineAccessScope, capabilities.RequiresOfflineAccessScope)
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != expectedCapabilities.PreferredTokenValidation {
|
||||
t.Errorf("expected PreferredTokenValidation %q, got %q", expectedCapabilities.PreferredTokenValidation, capabilities.PreferredTokenValidation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseParams url.Values
|
||||
scopes []string
|
||||
expectOfflineAccess bool
|
||||
}{
|
||||
{
|
||||
name: "params with offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "offline_access", "email"},
|
||||
expectOfflineAccess: true,
|
||||
},
|
||||
{
|
||||
name: "params without offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "email"},
|
||||
expectOfflineAccess: true, // Should be added automatically
|
||||
},
|
||||
{
|
||||
name: "empty scopes",
|
||||
baseParams: url.Values{},
|
||||
scopes: []string{},
|
||||
expectOfflineAccess: true, // Should be added automatically
|
||||
},
|
||||
{
|
||||
name: "multiple offline_access scopes",
|
||||
baseParams: url.Values{},
|
||||
scopes: []string{"openid", "offline_access", "email", "offline_access"},
|
||||
expectOfflineAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authParams, err := provider.BuildAuthParams(tt.baseParams, tt.scopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Fatal("expected non-nil auth params")
|
||||
}
|
||||
|
||||
// Check offline_access scope
|
||||
if tt.expectOfflineAccess {
|
||||
hasOfflineAccess := false
|
||||
for _, scope := range authParams.Scopes {
|
||||
if scope == "offline_access" {
|
||||
hasOfflineAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOfflineAccess {
|
||||
t.Error("expected offline_access scope to be present")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify other parameters are preserved
|
||||
for key, values := range tt.baseParams {
|
||||
paramValues := authParams.URLValues[key]
|
||||
if len(paramValues) != len(values) {
|
||||
t.Errorf("expected %d values for param %s, got %d", len(values), key, len(paramValues))
|
||||
}
|
||||
for i, expectedValue := range values {
|
||||
if i < len(paramValues) && paramValues[i] != expectedValue {
|
||||
t.Errorf("expected param %s[%d] to be %q, got %q", key, i, expectedValue, paramValues[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_ValidateTokenExpiry(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
session *mockSession
|
||||
cache *mockTokenCache
|
||||
refreshGracePeriod time.Duration
|
||||
expectedResult *ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "token not in cache with refresh token",
|
||||
token: "missing-token",
|
||||
session: &mockSession{
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token not in cache without refresh token",
|
||||
token: "missing-token",
|
||||
session: &mockSession{
|
||||
refreshToken: "",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token with missing exp claim with refresh token",
|
||||
token: "token-without-exp",
|
||||
session: &mockSession{
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"token-without-exp": {
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token with invalid exp claim type with refresh token",
|
||||
token: "token-with-invalid-exp",
|
||||
session: &mockSession{
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"token-with-invalid-exp": {
|
||||
"exp": "not-a-number",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token with invalid exp claim type without refresh token",
|
||||
token: "token-with-invalid-exp",
|
||||
session: &mockSession{
|
||||
refreshToken: "",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"token-with-invalid-exp": {
|
||||
"exp": "not-a-number",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token expired within grace period with refresh token",
|
||||
token: "expiring-token",
|
||||
refreshGracePeriod: time.Minute * 10,
|
||||
session: &mockSession{
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"expiring-token": {
|
||||
"exp": float64(time.Now().Add(time.Minute * 5).Unix()), // Expires in 5 minutes, within grace period
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token expired within grace period without refresh token",
|
||||
token: "expiring-token-no-refresh",
|
||||
refreshGracePeriod: time.Minute * 10,
|
||||
session: &mockSession{
|
||||
refreshToken: "",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"expiring-token-no-refresh": {
|
||||
"exp": float64(time.Now().Add(time.Minute * 5).Unix()), // Expires in 5 minutes, within grace period
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token valid outside grace period",
|
||||
token: "valid-token",
|
||||
refreshGracePeriod: time.Minute * 5,
|
||||
session: &mockSession{
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"valid-token": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()), // Expires in 1 hour, outside grace period
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := provider.ValidateTokenExpiry(tt.session, tt.token, tt.cache, tt.refreshGracePeriod)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("expected Authenticated %t, got %t", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("expected NeedsRefresh %t, got %t", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("expected IsExpired %t, got %t", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_ValidateTokens_AdditionalCases(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
t.Run("authenticated with access token but no ID token and refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Authenticated {
|
||||
t.Error("expected Authenticated to be true")
|
||||
}
|
||||
if !result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be true when no ID token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("authenticated with access token but no ID token and no refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "",
|
||||
refreshToken: "",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Authenticated {
|
||||
t.Error("expected Authenticated to be true")
|
||||
}
|
||||
if result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be false when no refresh token available")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("authenticated with no access token but has refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "",
|
||||
idToken: "",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Authenticated {
|
||||
t.Error("expected Authenticated to be false when no access token")
|
||||
}
|
||||
if !result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be true when refresh token available")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token verification error containing 'token has expired'", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "expired-id-token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{
|
||||
expiredTokens: map[string]bool{
|
||||
"expired-id-token": true,
|
||||
},
|
||||
}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Authenticated {
|
||||
t.Error("expected Authenticated to be false for expired token")
|
||||
}
|
||||
if !result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be true when token expired and refresh token available")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token verification error containing 'token has expired' without refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "expired-id-token",
|
||||
refreshToken: "",
|
||||
}
|
||||
verifier := &mockTokenVerifier{
|
||||
expiredTokens: map[string]bool{
|
||||
"expired-id-token": true,
|
||||
},
|
||||
}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Authenticated {
|
||||
t.Error("expected Authenticated to be false for expired token")
|
||||
}
|
||||
if result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be false when no refresh token")
|
||||
}
|
||||
if !result.IsExpired {
|
||||
t.Error("expected IsExpired to be true for expired token without refresh")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token verification other error with refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "invalid-token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{shouldFail: true}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Authenticated {
|
||||
t.Error("expected Authenticated to be false for invalid token")
|
||||
}
|
||||
if !result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be true when verification fails and refresh token available")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token verification other error without refresh token", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "invalid-token",
|
||||
refreshToken: "",
|
||||
}
|
||||
verifier := &mockTokenVerifier{shouldFail: true}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Authenticated {
|
||||
t.Error("expected Authenticated to be false for invalid token")
|
||||
}
|
||||
if result.NeedsRefresh {
|
||||
t.Error("expected NeedsRefresh to be false when no refresh token")
|
||||
}
|
||||
if !result.IsExpired {
|
||||
t.Error("expected IsExpired to be true for invalid token without refresh")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseProvider_HandleTokenRefresh(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
// Test that HandleTokenRefresh doesn't fail
|
||||
tokenData := &TokenResult{
|
||||
IDToken: "id-token",
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
}
|
||||
|
||||
err := provider.HandleTokenRefresh(tokenData)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from HandleTokenRefresh: %v", err)
|
||||
}
|
||||
|
||||
// Test with nil token data
|
||||
err = provider.HandleTokenRefresh(nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from HandleTokenRefresh with nil data: %v", err)
|
||||
}
|
||||
|
||||
// Test with empty token data
|
||||
emptyTokenData := &TokenResult{}
|
||||
err = provider.HandleTokenRefresh(emptyTokenData)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from HandleTokenRefresh with empty data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
// Base provider ValidateConfig should always return nil
|
||||
err := provider.ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from ValidateConfig: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseProvider_EdgeCases(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
t.Run("BuildAuthParams with nil scopes", func(t *testing.T) {
|
||||
baseParams := url.Values{"client_id": []string{"test-client"}}
|
||||
authParams, err := provider.BuildAuthParams(baseParams, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Fatal("expected non-nil auth params")
|
||||
}
|
||||
|
||||
// Should still add offline_access
|
||||
hasOfflineAccess := false
|
||||
for _, scope := range authParams.Scopes {
|
||||
if scope == "offline_access" {
|
||||
hasOfflineAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOfflineAccess {
|
||||
t.Error("expected offline_access scope to be added even with nil input scopes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BuildAuthParams with nil baseParams", func(t *testing.T) {
|
||||
scopes := []string{"openid", "email"}
|
||||
authParams, err := provider.BuildAuthParams(nil, scopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Fatal("expected non-nil auth params")
|
||||
}
|
||||
|
||||
// Note: nil baseParams results in nil URLValues, which is handled by the calling code
|
||||
if authParams.URLValues != nil {
|
||||
t.Logf("Got non-nil URLValues: %v", authParams.URLValues)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateTokenExpiry with nil cache", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic with nil cache: %v", r)
|
||||
}
|
||||
}()
|
||||
session := &mockSession{refreshToken: "refresh-token"}
|
||||
_, err := provider.ValidateTokenExpiry(session, "test-token", nil, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Logf("Got expected error with nil cache: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateTokenExpiry with nil session", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic with nil session: %v", r)
|
||||
}
|
||||
}()
|
||||
cache := &mockTokenCache{}
|
||||
_, err := provider.ValidateTokenExpiry(nil, "test-token", cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Logf("Got expected error with nil session: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkBaseProvider_GetType(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBaseProvider_GetCapabilities(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBaseProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
baseParams := url.Values{"client_id": []string{"test-client"}}
|
||||
scopes := []string{"openid", "email", "profile"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBaseProvider_ValidateTokenExpiry(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{refreshToken: "refresh-token"}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"test-token": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.ValidateTokenExpiry(session, "test-token", cache, time.Minute)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewProviderFactory(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
if factory == nil {
|
||||
t.Fatal("expected non-nil factory")
|
||||
}
|
||||
|
||||
if factory.registry == nil {
|
||||
t.Fatal("expected non-nil registry in factory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_CreateProvider(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
wantType ProviderType
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "Google provider detection",
|
||||
issuerURL: "https://accounts.google.com/.well-known/openid_configuration",
|
||||
wantType: ProviderTypeGoogle,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider detection - login.microsoftonline.com",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||
wantType: ProviderTypeAzure,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider detection - sts.windows.net",
|
||||
issuerURL: "https://sts.windows.net/tenant-id/",
|
||||
wantType: ProviderTypeAzure,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Generic provider detection",
|
||||
issuerURL: "https://auth.example.com/realms/test",
|
||||
wantType: ProviderTypeGeneric,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Empty issuer URL",
|
||||
issuerURL: "",
|
||||
wantError: true,
|
||||
errorSubstr: "issuer URL cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
issuerURL: "not-a-valid-url",
|
||||
wantType: ProviderTypeGeneric,
|
||||
wantError: false, // url.Parse accepts this as a valid URL
|
||||
},
|
||||
{
|
||||
name: "URL with invalid scheme",
|
||||
issuerURL: "ftp://example.com/auth",
|
||||
wantType: ProviderTypeGeneric,
|
||||
wantError: false, // Should create generic provider for non-standard schemes
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := factory.CreateProvider(tt.issuerURL)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && err.Error() != tt.errorSubstr {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
t.Error("expected non-nil provider")
|
||||
return
|
||||
}
|
||||
|
||||
if provider.GetType() != tt.wantType {
|
||||
t.Errorf("expected provider type %d, got %d", tt.wantType, provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_CreateProviderByType(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerType ProviderType
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "Generic provider",
|
||||
providerType: ProviderTypeGeneric,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Google provider",
|
||||
providerType: ProviderTypeGoogle,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
providerType: ProviderTypeAzure,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid provider type",
|
||||
providerType: ProviderType(999),
|
||||
wantError: true,
|
||||
errorSubstr: "unsupported provider type: 999",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := factory.CreateProviderByType(tt.providerType)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && err.Error() != tt.errorSubstr {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
t.Error("expected non-nil provider")
|
||||
return
|
||||
}
|
||||
|
||||
if provider.GetType() != tt.providerType {
|
||||
t.Errorf("expected provider type %d, got %d", tt.providerType, provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_GetSupportedProviders(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
supported := factory.GetSupportedProviders()
|
||||
|
||||
expectedProviders := map[ProviderType][]string{
|
||||
ProviderTypeGeneric: {"*"},
|
||||
ProviderTypeGoogle: {"accounts.google.com"},
|
||||
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
|
||||
}
|
||||
|
||||
if len(supported) != len(expectedProviders) {
|
||||
t.Errorf("expected %d supported providers, got %d", len(expectedProviders), len(supported))
|
||||
}
|
||||
|
||||
for expectedType, expectedPatterns := range expectedProviders {
|
||||
patterns, exists := supported[expectedType]
|
||||
if !exists {
|
||||
t.Errorf("expected provider type %d to be supported", expectedType)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(patterns) != len(expectedPatterns) {
|
||||
t.Errorf("expected %d patterns for provider type %d, got %d", len(expectedPatterns), expectedType, len(patterns))
|
||||
continue
|
||||
}
|
||||
|
||||
for i, expectedPattern := range expectedPatterns {
|
||||
if patterns[i] != expectedPattern {
|
||||
t.Errorf("expected pattern %q for provider type %d, got %q", expectedPattern, expectedType, patterns[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_DetectProviderType(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
wantType ProviderType
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "Google detection",
|
||||
issuerURL: "https://accounts.google.com/.well-known/openid_configuration",
|
||||
wantType: ProviderTypeGoogle,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Azure detection",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
|
||||
wantType: ProviderTypeAzure,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Generic detection",
|
||||
issuerURL: "https://keycloak.example.com/auth/realms/master",
|
||||
wantType: ProviderTypeGeneric,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
issuerURL: "",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
providerType, err := factory.DetectProviderType(tt.issuerURL)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if providerType != tt.wantType {
|
||||
t.Errorf("expected provider type %d, got %d", tt.wantType, providerType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_IsProviderSupported(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
wantSupport bool
|
||||
}{
|
||||
{
|
||||
name: "Google URL",
|
||||
issuerURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
wantSupport: true,
|
||||
},
|
||||
{
|
||||
name: "Azure URL - login.microsoftonline.com",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
|
||||
wantSupport: true,
|
||||
},
|
||||
{
|
||||
name: "Azure URL - sts.windows.net",
|
||||
issuerURL: "https://sts.windows.net/tenant/",
|
||||
wantSupport: true,
|
||||
},
|
||||
{
|
||||
name: "Generic URL",
|
||||
issuerURL: "https://auth.example.com/realms/master",
|
||||
wantSupport: true,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
issuerURL: "",
|
||||
wantSupport: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
issuerURL: "not-a-valid-url",
|
||||
wantSupport: true, // Generic provider supports all URLs
|
||||
},
|
||||
{
|
||||
name: "Valid but generic URL",
|
||||
issuerURL: "https://keycloak.example.com/auth",
|
||||
wantSupport: true, // Should be supported as generic
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
supported := factory.IsProviderSupported(tt.issuerURL)
|
||||
if supported != tt.wantSupport {
|
||||
t.Errorf("expected support %v for URL %q, got %v", tt.wantSupport, tt.issuerURL, supported)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_ConcurrentAccess(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 100
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://auth.example.com/realms/master",
|
||||
}
|
||||
|
||||
// Test concurrent provider creation
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
url := urls[j%len(urls)]
|
||||
|
||||
// Test CreateProvider
|
||||
provider, err := factory.CreateProvider(url)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error creating provider: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
t.Errorf("worker %d: expected non-nil provider", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test IsProviderSupported
|
||||
supported := factory.IsProviderSupported(url)
|
||||
if !supported {
|
||||
t.Errorf("worker %d: expected URL %s to be supported", workerID, url)
|
||||
return
|
||||
}
|
||||
|
||||
// Test DetectProviderType
|
||||
_, err = factory.DetectProviderType(url)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error detecting provider type: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_MemorySafety(t *testing.T) {
|
||||
// Test that creating many providers doesn't cause memory leaks
|
||||
const numCreations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numCreations; i++ {
|
||||
factory := NewProviderFactory()
|
||||
_, err := factory.CreateProvider("https://accounts.google.com/.well-known/openid_configuration")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Force garbage collection to cleanup any lingering resources
|
||||
runtime.GC()
|
||||
runtime.GC() // Call twice to ensure cleanup
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderFactory_EdgeCases(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
t.Run("nil factory registry handling", func(t *testing.T) {
|
||||
// This test ensures we handle edge cases properly
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Expected behavior - nil registry should cause panic or be handled
|
||||
t.Logf("Recovered from panic as expected: %v", r)
|
||||
}
|
||||
}()
|
||||
brokenFactory := &ProviderFactory{registry: nil}
|
||||
_, err := brokenFactory.CreateProvider("https://accounts.google.com")
|
||||
if err == nil {
|
||||
t.Error("expected error with nil registry")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed URLs", func(t *testing.T) {
|
||||
malformedURLs := []string{
|
||||
"://missing-scheme.com",
|
||||
"https://",
|
||||
"https:///missing-host",
|
||||
"https://example.com:port-not-number",
|
||||
}
|
||||
|
||||
for _, url := range malformedURLs {
|
||||
_, err := factory.CreateProvider(url)
|
||||
// Some malformed URLs might still be accepted by url.Parse,
|
||||
// but we ensure the system doesn't crash
|
||||
if err == nil {
|
||||
// Still check that we get a valid provider
|
||||
provider, err := factory.CreateProvider(url)
|
||||
if err == nil && provider == nil {
|
||||
t.Errorf("got nil provider without error for URL: %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long URLs", func(t *testing.T) {
|
||||
longURL := "https://accounts.google.com/" + string(make([]byte, 10000))
|
||||
for i := range longURL[len("https://accounts.google.com/"):] {
|
||||
longURL = longURL[:len("https://accounts.google.com/")+i] + "a" + longURL[len("https://accounts.google.com/")+i+1:]
|
||||
}
|
||||
|
||||
provider, err := factory.CreateProvider(longURL)
|
||||
if err != nil {
|
||||
t.Logf("long URL rejected as expected: %v", err)
|
||||
} else if provider == nil {
|
||||
t.Error("got nil provider without error for very long URL")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkProviderFactory_CreateProvider(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://auth.example.com/realms/master",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
url := urls[i%len(urls)]
|
||||
_, err := factory.CreateProvider(url)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderFactory_DetectProviderType(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://auth.example.com/realms/master",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
url := urls[i%len(urls)]
|
||||
_, err := factory.DetectProviderType(url)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderFactory_IsProviderSupported(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://auth.example.com/realms/master",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
url := urls[i%len(urls)]
|
||||
factory.IsProviderSupported(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewGoogleProvider(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("expected non-nil Google provider")
|
||||
}
|
||||
|
||||
if provider.BaseProvider == nil {
|
||||
t.Fatal("expected non-nil BaseProvider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_GetType(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
providerType := provider.GetType()
|
||||
|
||||
if providerType != ProviderTypeGoogle {
|
||||
t.Errorf("expected provider type %d, got %d", ProviderTypeGoogle, providerType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
expectedCapabilities := ProviderCapabilities{
|
||||
SupportsRefreshTokens: true,
|
||||
RequiresOfflineAccessScope: false,
|
||||
RequiresPromptConsent: true,
|
||||
PreferredTokenValidation: "id",
|
||||
}
|
||||
|
||||
if capabilities.SupportsRefreshTokens != expectedCapabilities.SupportsRefreshTokens {
|
||||
t.Errorf("expected SupportsRefreshTokens %t, got %t", expectedCapabilities.SupportsRefreshTokens, capabilities.SupportsRefreshTokens)
|
||||
}
|
||||
|
||||
if capabilities.RequiresOfflineAccessScope != expectedCapabilities.RequiresOfflineAccessScope {
|
||||
t.Errorf("expected RequiresOfflineAccessScope %t, got %t", expectedCapabilities.RequiresOfflineAccessScope, capabilities.RequiresOfflineAccessScope)
|
||||
}
|
||||
|
||||
if capabilities.RequiresPromptConsent != expectedCapabilities.RequiresPromptConsent {
|
||||
t.Errorf("expected RequiresPromptConsent %t, got %t", expectedCapabilities.RequiresPromptConsent, capabilities.RequiresPromptConsent)
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != expectedCapabilities.PreferredTokenValidation {
|
||||
t.Errorf("expected PreferredTokenValidation %q, got %q", expectedCapabilities.PreferredTokenValidation, capabilities.PreferredTokenValidation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseParams url.Values
|
||||
scopes []string
|
||||
expectAccessTypeOffline bool
|
||||
expectPromptConsent bool
|
||||
expectOfflineAccessRemoved bool
|
||||
}{
|
||||
{
|
||||
name: "basic params with offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "offline_access", "email"},
|
||||
expectAccessTypeOffline: true,
|
||||
expectPromptConsent: true,
|
||||
expectOfflineAccessRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "basic params without offline_access scope",
|
||||
baseParams: url.Values{"client_id": []string{"test-client"}},
|
||||
scopes: []string{"openid", "email"},
|
||||
expectAccessTypeOffline: true,
|
||||
expectPromptConsent: true,
|
||||
expectOfflineAccessRemoved: false,
|
||||
},
|
||||
{
|
||||
name: "empty scopes",
|
||||
baseParams: url.Values{},
|
||||
scopes: []string{},
|
||||
expectAccessTypeOffline: true,
|
||||
expectPromptConsent: true,
|
||||
expectOfflineAccessRemoved: false,
|
||||
},
|
||||
{
|
||||
name: "multiple offline_access scopes",
|
||||
baseParams: url.Values{},
|
||||
scopes: []string{"openid", "offline_access", "email", "offline_access"},
|
||||
expectAccessTypeOffline: true,
|
||||
expectPromptConsent: true,
|
||||
expectOfflineAccessRemoved: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authParams, err := provider.BuildAuthParams(tt.baseParams, tt.scopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Fatal("expected non-nil auth params")
|
||||
}
|
||||
|
||||
// Check access_type is set to offline
|
||||
if tt.expectAccessTypeOffline {
|
||||
accessType := authParams.URLValues.Get("access_type")
|
||||
if accessType != "offline" {
|
||||
t.Errorf("expected access_type 'offline', got %q", accessType)
|
||||
}
|
||||
}
|
||||
|
||||
// Check prompt is set to consent
|
||||
if tt.expectPromptConsent {
|
||||
prompt := authParams.URLValues.Get("prompt")
|
||||
if prompt != "consent" {
|
||||
t.Errorf("expected prompt 'consent', got %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Check offline_access scope is filtered out
|
||||
if tt.expectOfflineAccessRemoved {
|
||||
hasOfflineAccess := false
|
||||
for _, scope := range authParams.Scopes {
|
||||
if scope == "offline_access" {
|
||||
hasOfflineAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasOfflineAccess {
|
||||
t.Error("expected offline_access scope to be removed for Google provider")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify other scopes are preserved (except offline_access)
|
||||
expectedScopes := make([]string, 0, len(tt.scopes))
|
||||
for _, scope := range tt.scopes {
|
||||
if scope != "offline_access" {
|
||||
expectedScopes = append(expectedScopes, scope)
|
||||
}
|
||||
}
|
||||
|
||||
if len(authParams.Scopes) != len(expectedScopes) {
|
||||
t.Errorf("expected %d scopes after filtering, got %d", len(expectedScopes), len(authParams.Scopes))
|
||||
}
|
||||
|
||||
// Verify other parameters are preserved
|
||||
for key, values := range tt.baseParams {
|
||||
if key == "access_type" || key == "prompt" {
|
||||
continue // These get overridden
|
||||
}
|
||||
paramValues := authParams.URLValues[key]
|
||||
if len(paramValues) != len(values) {
|
||||
t.Errorf("expected %d values for param %s, got %d", len(values), key, len(paramValues))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_ValidateTokens(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *mockSession
|
||||
verifier *mockTokenVerifier
|
||||
cache *mockTokenCache
|
||||
refreshGracePeriod time.Duration
|
||||
expectedResult *ValidationResult
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "unauthenticated with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthenticated without refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{},
|
||||
},
|
||||
{
|
||||
name: "authenticated with valid ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "id.token.here",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"id.token.here": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with expired ID token and refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "expired.token.here",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{
|
||||
expiredTokens: map[string]bool{
|
||||
"expired.token.here": true,
|
||||
},
|
||||
},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with no ID token but has access token and refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with no tokens but has refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
NeedsRefresh: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authenticated with no tokens and no refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
},
|
||||
verifier: &mockTokenVerifier{},
|
||||
cache: &mockTokenCache{},
|
||||
expectedResult: &ValidationResult{
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := provider.ValidateTokens(tt.session, tt.verifier, tt.cache, tt.refreshGracePeriod)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("expected Authenticated %t, got %t", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("expected NeedsRefresh %t, got %t", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("expected IsExpired %t, got %t", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
// Google provider uses BaseProvider's ValidateConfig which always returns nil
|
||||
err := provider.ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from ValidateConfig: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_HandleTokenRefresh(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
// Test that HandleTokenRefresh doesn't fail
|
||||
tokenData := &TokenResult{
|
||||
IDToken: "id-token",
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
}
|
||||
|
||||
err := provider.HandleTokenRefresh(tokenData)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from HandleTokenRefresh: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_ConcurrentAccess(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 50
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Test concurrent access to provider methods
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: fmt.Sprintf("access-token-%d", workerID),
|
||||
idToken: fmt.Sprintf("id-token-%d", workerID),
|
||||
refreshToken: fmt.Sprintf("refresh-token-%d", workerID),
|
||||
}
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
fmt.Sprintf("id-token-%d", workerID): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
// Test GetType
|
||||
if provider.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("worker %d: expected Google provider type", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test GetCapabilities
|
||||
capabilities := provider.GetCapabilities()
|
||||
if !capabilities.SupportsRefreshTokens {
|
||||
t.Errorf("worker %d: expected refresh token support", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateTokens
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in ValidateTokens: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if !result.Authenticated {
|
||||
t.Errorf("worker %d: expected authenticated result", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test BuildAuthParams
|
||||
baseParams := url.Values{"client_id": []string{fmt.Sprintf("client-%d", workerID)}}
|
||||
scopes := []string{"openid", "email"}
|
||||
authParams, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in BuildAuthParams: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
if authParams == nil {
|
||||
t.Errorf("worker %d: expected non-nil auth params", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateConfig
|
||||
err = provider.ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error in ValidateConfig: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_MemorySafety(t *testing.T) {
|
||||
const numIterations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numIterations; i++ {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: fmt.Sprintf("access-token-%d", i),
|
||||
idToken: fmt.Sprintf("id-token-%d", i),
|
||||
refreshToken: fmt.Sprintf("refresh-token-%d", i),
|
||||
}
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
fmt.Sprintf("id-token-%d", i): {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Exercise all provider methods
|
||||
_ = provider.GetType()
|
||||
_ = provider.GetCapabilities()
|
||||
_, _ = provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
_, _ = provider.BuildAuthParams(url.Values{}, []string{"openid"})
|
||||
_ = provider.ValidateConfig()
|
||||
_ = provider.HandleTokenRefresh(&TokenResult{})
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProvider_EdgeCases(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
t.Run("nil session", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Expected behavior for nil session
|
||||
t.Logf("Recovered from expected panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
_, err := provider.ValidateTokens(nil, verifier, cache, time.Minute)
|
||||
if err == nil {
|
||||
t.Error("expected error with nil session")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil verifier", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
session := &mockSession{authenticated: true, idToken: "test.token.here"}
|
||||
cache := &mockTokenCache{}
|
||||
result, err := provider.ValidateTokens(session, nil, cache, time.Minute)
|
||||
// Google provider uses BaseProvider which handles nil verifier gracefully
|
||||
if err != nil {
|
||||
t.Logf("Got expected error with nil verifier: %v", err)
|
||||
} else if result != nil && result.NeedsRefresh {
|
||||
t.Logf("Provider handled nil verifier gracefully by requesting refresh")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil cache", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic with nil cache: %v", r)
|
||||
}
|
||||
}()
|
||||
session := &mockSession{authenticated: true, accessToken: "test-token"}
|
||||
verifier := &mockTokenVerifier{}
|
||||
result, err := provider.ValidateTokens(session, verifier, nil, time.Minute)
|
||||
// Google provider uses BaseProvider which handles nil cache gracefully
|
||||
if err != nil {
|
||||
t.Logf("Got expected error with nil cache: %v", err)
|
||||
} else if result != nil && result.NeedsRefresh {
|
||||
t.Logf("Provider handled nil cache gracefully by requesting refresh")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty tokens", func(t *testing.T) {
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "",
|
||||
idToken: "",
|
||||
refreshToken: "",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with empty tokens: %v", err)
|
||||
}
|
||||
if !result.IsExpired {
|
||||
t.Error("expected IsExpired=true for empty tokens without refresh token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("offline_access scope filtering", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputScopes []string
|
||||
expectScopes []string
|
||||
}{
|
||||
{
|
||||
name: "single offline_access",
|
||||
inputScopes: []string{"offline_access"},
|
||||
expectScopes: []string{},
|
||||
},
|
||||
{
|
||||
name: "offline_access with others",
|
||||
inputScopes: []string{"openid", "offline_access", "email"},
|
||||
expectScopes: []string{"openid", "email"},
|
||||
},
|
||||
{
|
||||
name: "multiple offline_access",
|
||||
inputScopes: []string{"offline_access", "openid", "offline_access", "profile"},
|
||||
expectScopes: []string{"openid", "profile"},
|
||||
},
|
||||
{
|
||||
name: "no offline_access",
|
||||
inputScopes: []string{"openid", "email", "profile"},
|
||||
expectScopes: []string{"openid", "email", "profile"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authParams, err := provider.BuildAuthParams(url.Values{}, tt.inputScopes)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(authParams.Scopes) != len(tt.expectScopes) {
|
||||
t.Errorf("expected %d scopes, got %d", len(tt.expectScopes), len(authParams.Scopes))
|
||||
}
|
||||
|
||||
for i, expectedScope := range tt.expectScopes {
|
||||
if i >= len(authParams.Scopes) || authParams.Scopes[i] != expectedScope {
|
||||
t.Errorf("expected scope %q at position %d, got %q", expectedScope, i, authParams.Scopes[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("very long tokens", func(t *testing.T) {
|
||||
longToken := strings.Repeat("a", 5000)
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: longToken,
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
longToken: {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with very long token: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("expected non-nil result with very long token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("special characters in parameters", func(t *testing.T) {
|
||||
specialParams := url.Values{
|
||||
"client_id": []string{"[email protected]"},
|
||||
"redirect_uri": []string{"https://example.com/callback?param=value&other=test"},
|
||||
"state": []string{"state+with/special=chars&more"},
|
||||
}
|
||||
scopes := []string{"openid", "email+special", "profile/test"}
|
||||
|
||||
authParams, err := provider.BuildAuthParams(specialParams, scopes)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with special characters: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if authParams == nil {
|
||||
t.Error("expected non-nil auth params with special characters")
|
||||
}
|
||||
|
||||
// Verify all special parameter values are preserved
|
||||
for key, expectedValues := range specialParams {
|
||||
if key == "access_type" || key == "prompt" {
|
||||
continue // These get overridden
|
||||
}
|
||||
actualValues := authParams.URLValues[key]
|
||||
if len(actualValues) != len(expectedValues) {
|
||||
t.Errorf("parameter %s: expected %d values, got %d", key, len(expectedValues), len(actualValues))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkGoogleProvider_GetType(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleProvider_GetCapabilities(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
baseParams := url.Values{"client_id": []string{"test-client"}}
|
||||
scopes := []string{"openid", "email", "profile", "offline_access"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleProvider_ValidateTokens(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "id.token.here",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
data: map[string]map[string]interface{}{
|
||||
"id.token.here": {
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleProvider_OfflineAccessFiltering(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
baseParams := url.Values{"client_id": []string{"test-client"}}
|
||||
scopes := []string{"openid", "offline_access", "email", "offline_access", "profile", "offline_access"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProviderRegistry_GetProviderByType(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
googleProvider := NewGoogleProvider()
|
||||
azureProvider := NewAzureProvider()
|
||||
genericProvider := NewGenericProvider()
|
||||
|
||||
registry.RegisterProvider(googleProvider)
|
||||
registry.RegisterProvider(azureProvider)
|
||||
registry.RegisterProvider(genericProvider)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerType ProviderType
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "Google provider",
|
||||
providerType: ProviderTypeGoogle,
|
||||
expectNil: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
providerType: ProviderTypeAzure,
|
||||
expectNil: false,
|
||||
},
|
||||
{
|
||||
name: "Generic provider",
|
||||
providerType: ProviderTypeGeneric,
|
||||
expectNil: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid provider type",
|
||||
providerType: ProviderType(999),
|
||||
expectNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := registry.GetProviderByType(tt.providerType)
|
||||
|
||||
if tt.expectNil {
|
||||
if provider != nil {
|
||||
t.Errorf("expected nil provider for type %d, got %v", tt.providerType, provider)
|
||||
}
|
||||
} else {
|
||||
if provider == nil {
|
||||
t.Errorf("expected non-nil provider for type %d", tt.providerType)
|
||||
} else if provider.GetType() != tt.providerType {
|
||||
t.Errorf("expected provider type %d, got %d", tt.providerType, provider.GetType())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_GetRegisteredProviders(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Initially should be empty
|
||||
providers := registry.GetRegisteredProviders()
|
||||
if len(providers) != 0 {
|
||||
t.Errorf("expected 0 registered providers initially, got %d", len(providers))
|
||||
}
|
||||
|
||||
// Register providers one by one
|
||||
expectedTypes := []ProviderType{
|
||||
ProviderTypeGoogle,
|
||||
ProviderTypeAzure,
|
||||
ProviderTypeGeneric,
|
||||
}
|
||||
|
||||
for i, providerType := range expectedTypes {
|
||||
switch providerType {
|
||||
case ProviderTypeGoogle:
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
case ProviderTypeAzure:
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
case ProviderTypeGeneric:
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
}
|
||||
|
||||
providers := registry.GetRegisteredProviders()
|
||||
if len(providers) != i+1 {
|
||||
t.Errorf("expected %d registered providers after registering %d, got %d", i+1, i+1, len(providers))
|
||||
}
|
||||
|
||||
// Check that the newly registered type is present
|
||||
found := false
|
||||
for _, registeredType := range providers {
|
||||
if registeredType == providerType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected provider type %d to be in registered providers list", providerType)
|
||||
}
|
||||
}
|
||||
|
||||
// Final check - all providers should be registered
|
||||
finalProviders := registry.GetRegisteredProviders()
|
||||
if len(finalProviders) != len(expectedTypes) {
|
||||
t.Errorf("expected %d final registered providers, got %d", len(expectedTypes), len(finalProviders))
|
||||
}
|
||||
|
||||
// Check all expected types are present
|
||||
for _, expectedType := range expectedTypes {
|
||||
found := false
|
||||
for _, registeredType := range finalProviders {
|
||||
if registeredType == expectedType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected provider type %d to be in final registered providers list", expectedType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_ClearCache(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
// Populate cache by detecting providers
|
||||
googleURL := "https://accounts.google.com/.well-known/openid_configuration"
|
||||
azureURL := "https://login.microsoftonline.com/tenant/v2.0"
|
||||
genericURL := "https://keycloak.example.com/auth/realms/master"
|
||||
|
||||
// These calls should populate the cache
|
||||
provider1 := registry.DetectProvider(googleURL)
|
||||
provider2 := registry.DetectProvider(azureURL)
|
||||
provider3 := registry.DetectProvider(genericURL)
|
||||
|
||||
// Verify providers were detected correctly
|
||||
if provider1.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider, got %d", provider1.GetType())
|
||||
}
|
||||
if provider2.GetType() != ProviderTypeAzure {
|
||||
t.Errorf("expected Azure provider, got %d", provider2.GetType())
|
||||
}
|
||||
if provider3.GetType() != ProviderTypeGeneric {
|
||||
t.Errorf("expected Generic provider, got %d", provider3.GetType())
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
registry.ClearCache()
|
||||
|
||||
// Detect again - should work but might create new instances internally
|
||||
provider1After := registry.DetectProvider(googleURL)
|
||||
provider2After := registry.DetectProvider(azureURL)
|
||||
provider3After := registry.DetectProvider(genericURL)
|
||||
|
||||
// Verify detection still works correctly after cache clear
|
||||
if provider1After.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider after cache clear, got %d", provider1After.GetType())
|
||||
}
|
||||
if provider2After.GetType() != ProviderTypeAzure {
|
||||
t.Errorf("expected Azure provider after cache clear, got %d", provider2After.GetType())
|
||||
}
|
||||
if provider3After.GetType() != ProviderTypeGeneric {
|
||||
t.Errorf("expected Generic provider after cache clear, got %d", provider3After.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_DetectProvider_EdgeCases(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
expectedType ProviderType
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "Google URL with subdomain",
|
||||
issuerURL: "https://accounts.google.com.evil.com/",
|
||||
expectedType: ProviderTypeGoogle, // Contains "accounts.google.com"
|
||||
},
|
||||
{
|
||||
name: "Azure URL with subdomain",
|
||||
issuerURL: "https://login.microsoftonline.com.evil.com/",
|
||||
expectedType: ProviderTypeAzure, // Contains "login.microsoftonline.com"
|
||||
},
|
||||
{
|
||||
name: "Google URL case insensitive",
|
||||
issuerURL: "https://ACCOUNTS.GOOGLE.COM/auth",
|
||||
expectedType: ProviderTypeGeneric, // Case sensitive matching
|
||||
},
|
||||
{
|
||||
name: "Azure login URL case insensitive",
|
||||
issuerURL: "https://LOGIN.MICROSOFTONLINE.COM/tenant",
|
||||
expectedType: ProviderTypeGeneric, // Case sensitive matching
|
||||
},
|
||||
{
|
||||
name: "Azure STS URL case insensitive",
|
||||
issuerURL: "https://STS.WINDOWS.NET/tenant",
|
||||
expectedType: ProviderTypeGeneric, // Case sensitive matching
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
issuerURL: "://invalid-url",
|
||||
expectNil: true,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
issuerURL: "",
|
||||
expectedType: ProviderTypeGeneric, // Falls back to generic
|
||||
},
|
||||
{
|
||||
name: "URL without host",
|
||||
issuerURL: "/path/only",
|
||||
expectedType: ProviderTypeGeneric, // Falls back to generic
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := registry.DetectProvider(tt.issuerURL)
|
||||
|
||||
if tt.expectNil {
|
||||
if provider != nil {
|
||||
t.Errorf("expected nil provider for URL %q, got %v", tt.issuerURL, provider)
|
||||
}
|
||||
} else {
|
||||
if provider == nil {
|
||||
t.Errorf("expected non-nil provider for URL %q", tt.issuerURL)
|
||||
} else if provider.GetType() != tt.expectedType {
|
||||
t.Errorf("expected provider type %d for URL %q, got %d", tt.expectedType, tt.issuerURL, provider.GetType())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_ConcurrentAccess(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 100
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://sts.windows.net/tenant/",
|
||||
"https://keycloak.example.com/auth/realms/master",
|
||||
}
|
||||
|
||||
// Test concurrent access to all registry methods
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
url := urls[j%len(urls)]
|
||||
|
||||
// Test DetectProvider
|
||||
provider := registry.DetectProvider(url)
|
||||
if provider == nil {
|
||||
t.Errorf("worker %d: expected non-nil provider for URL %s", workerID, url)
|
||||
return
|
||||
}
|
||||
|
||||
// Test GetProviderByType
|
||||
providerByType := registry.GetProviderByType(provider.GetType())
|
||||
if providerByType == nil {
|
||||
t.Errorf("worker %d: expected non-nil provider for type %d", workerID, provider.GetType())
|
||||
return
|
||||
}
|
||||
|
||||
// Test GetRegisteredProviders
|
||||
providers := registry.GetRegisteredProviders()
|
||||
if len(providers) == 0 {
|
||||
t.Errorf("worker %d: expected non-empty providers list", workerID)
|
||||
return
|
||||
}
|
||||
|
||||
// Occasionally clear cache to test concurrent cache operations
|
||||
if workerID%10 == 0 && j == 0 {
|
||||
registry.ClearCache()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_MemorySafety(t *testing.T) {
|
||||
const numIterations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numIterations; i++ {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
// Exercise all registry methods
|
||||
urls := []string{
|
||||
"https://accounts.google.com/config",
|
||||
"https://login.microsoftonline.com/tenant/config",
|
||||
"https://keycloak.example.com/auth",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
provider := registry.DetectProvider(url)
|
||||
if provider != nil {
|
||||
_ = registry.GetProviderByType(provider.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
_ = registry.GetRegisteredProviders()
|
||||
registry.ClearCache()
|
||||
|
||||
// Create many entries to test cache memory management
|
||||
for j := 0; j < 10; j++ {
|
||||
testURL := fmt.Sprintf("https://example%d.com/auth", j)
|
||||
registry.DetectProvider(testURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistry_CacheConsistency(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register providers
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
testURL := "https://accounts.google.com/.well-known/openid_configuration"
|
||||
|
||||
// First detection should populate cache
|
||||
provider1 := registry.DetectProvider(testURL)
|
||||
if provider1 == nil {
|
||||
t.Fatal("expected non-nil provider from first detection")
|
||||
}
|
||||
if provider1.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider, got %d", provider1.GetType())
|
||||
}
|
||||
|
||||
// Second detection should use cache (same result)
|
||||
provider2 := registry.DetectProvider(testURL)
|
||||
if provider2 == nil {
|
||||
t.Fatal("expected non-nil provider from second detection")
|
||||
}
|
||||
if provider2.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider from cache, got %d", provider2.GetType())
|
||||
}
|
||||
|
||||
// Clear cache and detect again
|
||||
registry.ClearCache()
|
||||
provider3 := registry.DetectProvider(testURL)
|
||||
if provider3 == nil {
|
||||
t.Fatal("expected non-nil provider after cache clear")
|
||||
}
|
||||
if provider3.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider after cache clear, got %d", provider3.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// Test registry without any providers registered
|
||||
func TestProviderRegistry_EmptyRegistry(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Test with empty registry
|
||||
provider := registry.DetectProvider("https://accounts.google.com/auth")
|
||||
if provider != nil {
|
||||
t.Errorf("expected nil provider from empty registry, got %v", provider)
|
||||
}
|
||||
|
||||
providerByType := registry.GetProviderByType(ProviderTypeGoogle)
|
||||
if providerByType != nil {
|
||||
t.Errorf("expected nil provider by type from empty registry, got %v", providerByType)
|
||||
}
|
||||
|
||||
providers := registry.GetRegisteredProviders()
|
||||
if len(providers) != 0 {
|
||||
t.Errorf("expected empty providers list from empty registry, got %d providers", len(providers))
|
||||
}
|
||||
|
||||
// Clear cache should not panic on empty registry
|
||||
registry.ClearCache()
|
||||
}
|
||||
|
||||
// Test multiple providers of same type (edge case)
|
||||
func TestProviderRegistry_DuplicateProviderTypes(t *testing.T) {
|
||||
registry := NewProviderRegistry()
|
||||
|
||||
// Register same provider type multiple times
|
||||
provider1 := NewGoogleProvider()
|
||||
provider2 := NewGoogleProvider()
|
||||
|
||||
registry.RegisterProvider(provider1)
|
||||
registry.RegisterProvider(provider2)
|
||||
|
||||
// GetProviderByType should return one of them (likely the latest)
|
||||
retrieved := registry.GetProviderByType(ProviderTypeGoogle)
|
||||
if retrieved == nil {
|
||||
t.Error("expected non-nil provider")
|
||||
}
|
||||
if retrieved.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("expected Google provider type, got %d", retrieved.GetType())
|
||||
}
|
||||
|
||||
// Both should be in the providers list
|
||||
allProviders := registry.GetRegisteredProviders()
|
||||
googleCount := 0
|
||||
for _, providerType := range allProviders {
|
||||
if providerType == ProviderTypeGoogle {
|
||||
googleCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This behavior depends on implementation - the registry might deduplicate or keep all
|
||||
if googleCount == 0 {
|
||||
t.Error("expected at least one Google provider in registered list")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkProviderRegistry_DetectProvider(b *testing.B) {
|
||||
registry := NewProviderRegistry()
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://keycloak.example.com/auth/realms/master",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
url := urls[i%len(urls)]
|
||||
registry.DetectProvider(url)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderRegistry_GetProviderByType(b *testing.B) {
|
||||
registry := NewProviderRegistry()
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
types := []ProviderType{
|
||||
ProviderTypeGoogle,
|
||||
ProviderTypeAzure,
|
||||
ProviderTypeGeneric,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
providerType := types[i%len(types)]
|
||||
registry.GetProviderByType(providerType)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderRegistry_GetRegisteredProviders(b *testing.B) {
|
||||
registry := NewProviderRegistry()
|
||||
registry.RegisterProvider(NewGoogleProvider())
|
||||
registry.RegisterProvider(NewAzureProvider())
|
||||
registry.RegisterProvider(NewGenericProvider())
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
registry.GetRegisteredProviders()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,761 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewConfigValidator(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
if validator == nil {
|
||||
t.Fatal("expected non-nil config validator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateIssuerURL(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid HTTPS URL",
|
||||
issuerURL: "https://accounts.google.com/.well-known/openid_configuration",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "valid HTTP URL",
|
||||
issuerURL: "http://localhost:8080/auth/realms/master",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "empty URL",
|
||||
issuerURL: "",
|
||||
wantError: true,
|
||||
errorSubstr: "issuer URL cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "invalid URL format",
|
||||
issuerURL: "://invalid-url",
|
||||
wantError: true,
|
||||
errorSubstr: "invalid issuer URL format",
|
||||
},
|
||||
{
|
||||
name: "URL without scheme",
|
||||
issuerURL: "example.com/auth",
|
||||
wantError: true,
|
||||
errorSubstr: "issuer URL must include scheme",
|
||||
},
|
||||
{
|
||||
name: "URL with invalid scheme",
|
||||
issuerURL: "ftp://example.com/auth",
|
||||
wantError: true,
|
||||
errorSubstr: "issuer URL scheme must be http or https",
|
||||
},
|
||||
{
|
||||
name: "URL without host",
|
||||
issuerURL: "https:///path/only",
|
||||
wantError: true,
|
||||
errorSubstr: "issuer URL must include host",
|
||||
},
|
||||
{
|
||||
name: "URL with port",
|
||||
issuerURL: "https://example.com:8443/auth",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "URL with path and query",
|
||||
issuerURL: "https://example.com/auth/realms/master?param=value",
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateIssuerURL(tt.issuerURL)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateClientID(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clientID string
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid client ID",
|
||||
clientID: "valid-client-id",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "long client ID",
|
||||
clientID: "very-long-client-id-with-many-characters-12345678901234567890",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "empty client ID",
|
||||
clientID: "",
|
||||
wantError: true,
|
||||
errorSubstr: "client ID cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "very short client ID",
|
||||
clientID: "ab",
|
||||
wantError: true,
|
||||
errorSubstr: "client ID appears to be too short",
|
||||
},
|
||||
{
|
||||
name: "minimum valid length",
|
||||
clientID: "abc",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "client ID with special characters",
|
||||
clientID: "[email protected]",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "client ID with numbers",
|
||||
clientID: "client-123-456",
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateClientID(tt.clientID)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateScopes(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scopes []string
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid scopes with openid",
|
||||
scopes: []string{"openid", "email", "profile"},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "openid scope only",
|
||||
scopes: []string{"openid"},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "empty scopes",
|
||||
scopes: []string{},
|
||||
wantError: true,
|
||||
errorSubstr: "at least one scope must be provided",
|
||||
},
|
||||
{
|
||||
name: "nil scopes",
|
||||
scopes: nil,
|
||||
wantError: true,
|
||||
errorSubstr: "at least one scope must be provided",
|
||||
},
|
||||
{
|
||||
name: "scopes without openid",
|
||||
scopes: []string{"email", "profile"},
|
||||
wantError: true,
|
||||
errorSubstr: "'openid' scope is required for OIDC authentication",
|
||||
},
|
||||
{
|
||||
name: "scopes with openid and whitespace",
|
||||
scopes: []string{" openid ", "email", "profile"},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "scopes with mixed case openid",
|
||||
scopes: []string{"OpenID", "email"}, // This should fail as it's case sensitive
|
||||
wantError: true,
|
||||
errorSubstr: "'openid' scope is required for OIDC authentication",
|
||||
},
|
||||
{
|
||||
name: "scopes with offline_access",
|
||||
scopes: []string{"openid", "offline_access", "email"},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "many scopes",
|
||||
scopes: []string{"openid", "email", "profile", "address", "phone", "offline_access", "custom_scope"},
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateScopes(tt.scopes)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateRedirectURL(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
redirectURL string
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid HTTPS redirect URL",
|
||||
redirectURL: "https://example.com/callback",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "valid HTTP redirect URL",
|
||||
redirectURL: "http://localhost:8080/callback",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "empty redirect URL",
|
||||
redirectURL: "",
|
||||
wantError: true,
|
||||
errorSubstr: "redirect URL cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "invalid redirect URL format",
|
||||
redirectURL: "://invalid-url",
|
||||
wantError: true,
|
||||
errorSubstr: "invalid redirect URL format",
|
||||
},
|
||||
{
|
||||
name: "redirect URL without scheme",
|
||||
redirectURL: "example.com/callback",
|
||||
wantError: true,
|
||||
errorSubstr: "redirect URL must include scheme",
|
||||
},
|
||||
{
|
||||
name: "redirect URL with custom scheme",
|
||||
redirectURL: "myapp://callback",
|
||||
wantError: false, // Custom schemes are allowed for mobile apps
|
||||
},
|
||||
{
|
||||
name: "redirect URL with query parameters",
|
||||
redirectURL: "https://example.com/callback?state=123&code=456",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "redirect URL with fragment",
|
||||
redirectURL: "https://example.com/callback#section",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "localhost redirect URL",
|
||||
redirectURL: "http://localhost/callback",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "IP address redirect URL",
|
||||
redirectURL: "http://192.168.1.1:8080/callback",
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateRedirectURL(tt.redirectURL)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateProviderSpecificConfig(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
t.Run("Google provider config", func(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]interface{}
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid Google issuer URL",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://accounts.google.com/.well-known/openid_configuration",
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid Google issuer URL",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://example.com/auth",
|
||||
},
|
||||
wantError: true,
|
||||
errorSubstr: "google provider requires issuer URL to contain accounts.google.com",
|
||||
},
|
||||
{
|
||||
name: "empty config",
|
||||
config: map[string]interface{}{},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "non-string issuer URL",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": 12345,
|
||||
},
|
||||
wantError: false, // Type assertion fails, but doesn't error
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateProviderSpecificConfig(provider, tt.config)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Azure provider config", func(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]interface{}
|
||||
wantError bool
|
||||
errorSubstr string
|
||||
}{
|
||||
{
|
||||
name: "valid Azure issuer URL - login.microsoftonline.com",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0",
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "valid Azure issuer URL - sts.windows.net",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://sts.windows.net/12345678-1234-1234-1234-123456789012/",
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "valid Azure issuer URL with proper tenant ID",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0",
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid Azure issuer URL",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://example.com/auth",
|
||||
},
|
||||
wantError: true,
|
||||
errorSubstr: "azure provider requires issuer URL to contain login.microsoftonline.com or sts.windows.net",
|
||||
},
|
||||
{
|
||||
name: "Azure issuer URL without tenant ID",
|
||||
config: map[string]interface{}{
|
||||
"issuer_url": "https://login.microsoftonline.com/v2.0",
|
||||
},
|
||||
wantError: true,
|
||||
errorSubstr: "azure issuer URL should include tenant ID",
|
||||
},
|
||||
{
|
||||
name: "empty config",
|
||||
config: map[string]interface{}{},
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateProviderSpecificConfig(provider, tt.config)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errorSubstr != "" && !strings.Contains(err.Error(), tt.errorSubstr) {
|
||||
t.Errorf("expected error to contain %q, got %q", tt.errorSubstr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Generic provider config", func(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
config := map[string]interface{}{
|
||||
"issuer_url": "https://example.com/auth",
|
||||
"any_key": "any_value",
|
||||
}
|
||||
|
||||
err := validator.ValidateProviderSpecificConfig(provider, config)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for generic provider: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown provider type", func(t *testing.T) {
|
||||
// Create a mock provider with invalid type
|
||||
mockProvider := &struct {
|
||||
OIDCProvider
|
||||
}{}
|
||||
mockProvider.OIDCProvider = NewGenericProvider()
|
||||
|
||||
// Override GetType to return invalid type
|
||||
provider := &mockProviderWithInvalidType{mockProvider.OIDCProvider}
|
||||
|
||||
err := validator.ValidateProviderSpecificConfig(provider, map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown provider type")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown provider type") {
|
||||
t.Errorf("expected error about unknown provider type, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mockProviderWithInvalidType is a test helper that returns an invalid provider type
|
||||
type mockProviderWithInvalidType struct {
|
||||
OIDCProvider
|
||||
}
|
||||
|
||||
func (m *mockProviderWithInvalidType) GetType() ProviderType {
|
||||
return ProviderType(999) // Invalid provider type
|
||||
}
|
||||
|
||||
func TestConfigValidator_ConcurrentAccess(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
// Track initial goroutine count for memory safety
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
const numGoroutines = 50
|
||||
const numOperationsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
testData := []struct {
|
||||
issuerURL string
|
||||
clientID string
|
||||
scopes []string
|
||||
redirectURL string
|
||||
}{
|
||||
{
|
||||
issuerURL: "https://accounts.google.com/.well-known/openid_configuration",
|
||||
clientID: "google-client-id",
|
||||
scopes: []string{"openid", "email", "profile"},
|
||||
redirectURL: "https://example.com/callback",
|
||||
},
|
||||
{
|
||||
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
|
||||
clientID: "azure-client-id",
|
||||
scopes: []string{"openid", "offline_access"},
|
||||
redirectURL: "https://example.com/azure-callback",
|
||||
},
|
||||
{
|
||||
issuerURL: "https://keycloak.example.com/auth/realms/master",
|
||||
clientID: "generic-client-id",
|
||||
scopes: []string{"openid", "email"},
|
||||
redirectURL: "http://localhost:8080/callback",
|
||||
},
|
||||
}
|
||||
|
||||
// Test concurrent validation operations
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
data := testData[workerID%len(testData)]
|
||||
|
||||
for j := 0; j < numOperationsPerGoroutine; j++ {
|
||||
// Test ValidateIssuerURL
|
||||
err := validator.ValidateIssuerURL(data.issuerURL)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error validating issuer URL: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateClientID
|
||||
err = validator.ValidateClientID(data.clientID)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error validating client ID: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateScopes
|
||||
err = validator.ValidateScopes(data.scopes)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error validating scopes: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test ValidateRedirectURL
|
||||
err = validator.ValidateRedirectURL(data.redirectURL)
|
||||
if err != nil {
|
||||
t.Errorf("worker %d: unexpected error validating redirect URL: %v", workerID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for potential goroutine leaks - allow some tolerance for test framework overhead
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_MemorySafety(t *testing.T) {
|
||||
const numIterations = 1000
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < numIterations; i++ {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
// Exercise all validation methods
|
||||
_ = validator.ValidateIssuerURL(fmt.Sprintf("https://example%d.com/auth", i))
|
||||
_ = validator.ValidateClientID(fmt.Sprintf("client-id-%d", i))
|
||||
_ = validator.ValidateScopes([]string{"openid", fmt.Sprintf("scope-%d", i)})
|
||||
_ = validator.ValidateRedirectURL(fmt.Sprintf("https://example%d.com/callback", i))
|
||||
|
||||
// Test provider-specific validation
|
||||
provider := NewGoogleProvider()
|
||||
config := map[string]interface{}{
|
||||
"issuer_url": fmt.Sprintf("https://accounts.google.com/config-%d", i),
|
||||
}
|
||||
_ = validator.ValidateProviderSpecificConfig(provider, config)
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+5 {
|
||||
t.Errorf("potential goroutine leak: started with %d goroutines, ended with %d", initialGoroutines, finalGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidator_EdgeCases(t *testing.T) {
|
||||
validator := NewConfigValidator()
|
||||
|
||||
t.Run("very long URLs and strings", func(t *testing.T) {
|
||||
longString := strings.Repeat("a", 10000)
|
||||
longURL := "https://" + longString + ".com/auth"
|
||||
|
||||
// These should not crash
|
||||
err := validator.ValidateIssuerURL(longURL)
|
||||
if err != nil {
|
||||
t.Logf("Long URL validation failed as expected: %v", err)
|
||||
}
|
||||
|
||||
err = validator.ValidateClientID(longString)
|
||||
if err != nil {
|
||||
t.Logf("Long client ID validation failed as expected: %v", err)
|
||||
}
|
||||
|
||||
err = validator.ValidateRedirectURL(longURL)
|
||||
if err != nil {
|
||||
t.Logf("Long redirect URL validation failed as expected: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("special characters and encoding", func(t *testing.T) {
|
||||
specialURLs := []string{
|
||||
"https://example.com/auth?param=value%20with%20spaces",
|
||||
"https://example.com/auth#fragment",
|
||||
"https://example.com/auth/path with spaces",
|
||||
"https://example.com/auth?param=特殊字符",
|
||||
"https://xn--e1afmkfd.xn--p1ai/auth", // Punycode domain
|
||||
}
|
||||
|
||||
for _, url := range specialURLs {
|
||||
err := validator.ValidateIssuerURL(url)
|
||||
// Some may fail, but should not crash
|
||||
if err != nil {
|
||||
t.Logf("Special URL %q validation failed: %v", url, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil and empty inputs", func(t *testing.T) {
|
||||
// Test nil scopes
|
||||
err := validator.ValidateScopes(nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for nil scopes")
|
||||
}
|
||||
|
||||
// Test empty scopes
|
||||
err = validator.ValidateScopes([]string{})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty scopes")
|
||||
}
|
||||
|
||||
// Test nil provider
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Recovered from expected panic with nil provider: %v", r)
|
||||
}
|
||||
}()
|
||||
err = validator.ValidateProviderSpecificConfig(nil, map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Error("expected error with nil provider")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed tenant IDs", func(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
malformedTenantConfigs := []map[string]interface{}{
|
||||
{
|
||||
"issuer_url": "https://login.microsoftonline.com/not-a-guid/v2.0",
|
||||
},
|
||||
{
|
||||
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-123456789012/v2.0", // Wrong length
|
||||
},
|
||||
{
|
||||
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-1234-12345678901/v2.0", // Wrong format
|
||||
},
|
||||
}
|
||||
|
||||
for i, config := range malformedTenantConfigs {
|
||||
err := validator.ValidateProviderSpecificConfig(provider, config)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for malformed tenant ID in config %d", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for performance validation
|
||||
func BenchmarkConfigValidator_ValidateIssuerURL(b *testing.B) {
|
||||
validator := NewConfigValidator()
|
||||
urls := []string{
|
||||
"https://accounts.google.com/.well-known/openid_configuration",
|
||||
"https://login.microsoftonline.com/tenant/v2.0",
|
||||
"https://keycloak.example.com/auth/realms/master",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
url := urls[i%len(urls)]
|
||||
validator.ValidateIssuerURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConfigValidator_ValidateScopes(b *testing.B) {
|
||||
validator := NewConfigValidator()
|
||||
scopes := []string{"openid", "email", "profile", "offline_access"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
validator.ValidateScopes(scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConfigValidator_ValidateProviderSpecificConfig(b *testing.B) {
|
||||
validator := NewConfigValidator()
|
||||
provider := NewGoogleProvider()
|
||||
config := map[string]interface{}{
|
||||
"issuer_url": "https://accounts.google.com/.well-known/openid_configuration",
|
||||
"client_id": "test-client-id",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
validator.ValidateProviderSpecificConfig(provider, config)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user