fixup! fixup! fixup! Further pursue of perfection.

This commit is contained in:
2025-09-05 13:57:54 +01:00
parent 2c902eaafb
commit e267cb3a6f
17 changed files with 7751 additions and 13 deletions
+736
View File
@@ -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)
}
}
}
+695
View File
@@ -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)
}
}
}
+650
View File
@@ -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)
}
}
}
+508
View File
@@ -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)
}
}
+724
View File
@@ -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)
}
}
}
+521
View File
@@ -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()
}
}
+761
View File
@@ -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)
}
}