Files
traefikoidc/internal/providers/registry_test.go
T
lukaszraczyloandGitHub bde1db1c3b traefik plugin 0.7.7 (#73)
* Automatic discovery of the scopes.

Issue #61 raised very valid concerns about users configuring scopes that are not supported by the provider.
This change introduces automatic discovery of supported scopes by fetching the provider's discovery document and filtering out unsupported scopes.

Before:
User configures: scopes: ["openid", "profile", "email", "offline_access"]
Self-hosted GitLab: "The requested scope is invalid, unknown, or malformed"
Authentication: ❌ FAILS

After:
User configures: scopes: ["openid", "profile", "email", "offline_access"]
Middleware checks discovery doc → offline_access not supported
Automatically filters to: ["openid", "profile", "email"]
Authentication: ✅ SUCCEEDS

* Resolves issue #74 by enabling user to specify expected audience in the configuration.

* Fix flaky tests.
2025-10-08 11:44:00 +01:00

743 lines
19 KiB
Go

package providers
import (
"fmt"
"sync"
"testing"
)
// TestProviderRegistry_NewProviderRegistry tests registry constructor
func TestProviderRegistry_NewProviderRegistry(t *testing.T) {
registry := NewProviderRegistry()
if registry == nil {
t.Fatal("Expected registry to be created, got nil")
}
if registry.providers == nil {
t.Error("Providers slice should be initialized")
}
if registry.cache == nil {
t.Error("Cache map should be initialized")
}
if registry.typeMap == nil {
t.Error("TypeMap should be initialized")
}
if registry.maxCacheSize != 1000 {
t.Errorf("Expected maxCacheSize 1000, got %d", registry.maxCacheSize)
}
if registry.cacheCount != 0 {
t.Errorf("Expected initial cacheCount 0, got %d", registry.cacheCount)
}
}
// TestProviderRegistry_RegisterProvider tests provider registration
func TestProviderRegistry_RegisterProvider(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
googleProvider := NewGoogleProvider()
azureProvider := NewAzureProvider()
// Register providers
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(googleProvider)
registry.RegisterProvider(azureProvider)
// Verify providers are registered
if len(registry.providers) != 3 {
t.Errorf("Expected 3 providers, got %d", len(registry.providers))
}
if len(registry.typeMap) != 3 {
t.Errorf("Expected 3 type mappings, got %d", len(registry.typeMap))
}
// Verify type mappings
if registry.typeMap[ProviderTypeGeneric] != genericProvider {
t.Error("Generic provider not mapped correctly")
}
if registry.typeMap[ProviderTypeGoogle] != googleProvider {
t.Error("Google provider not mapped correctly")
}
if registry.typeMap[ProviderTypeAzure] != azureProvider {
t.Error("Azure provider not mapped correctly")
}
}
// TestProviderRegistry_GetProviderByType tests provider retrieval by type
func TestProviderRegistry_GetProviderByType(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
googleProvider := NewGoogleProvider()
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(googleProvider)
tests := []struct {
name string
providerType ProviderType
expected OIDCProvider
}{
{
name: "Get Generic provider",
providerType: ProviderTypeGeneric,
expected: genericProvider,
},
{
name: "Get Google provider",
providerType: ProviderTypeGoogle,
expected: googleProvider,
},
{
name: "Get unregistered provider",
providerType: ProviderTypeAzure,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := registry.GetProviderByType(tt.providerType)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
// TestProviderRegistry_GetRegisteredProviders tests listing registered provider types
func TestProviderRegistry_GetRegisteredProviders(t *testing.T) {
registry := NewProviderRegistry()
// Initially empty
types := registry.GetRegisteredProviders()
if len(types) != 0 {
t.Errorf("Expected 0 registered providers, got %d", len(types))
}
// Register some providers
registry.RegisterProvider(NewGenericProvider())
registry.RegisterProvider(NewGoogleProvider())
types = registry.GetRegisteredProviders()
if len(types) != 2 {
t.Errorf("Expected 2 registered providers, got %d", len(types))
}
// Verify types are correct
expectedTypes := map[ProviderType]bool{
ProviderTypeGeneric: false,
ProviderTypeGoogle: false,
}
for _, providerType := range types {
if _, exists := expectedTypes[providerType]; exists {
expectedTypes[providerType] = true
} else {
t.Errorf("Unexpected provider type: %v", providerType)
}
}
for providerType, found := range expectedTypes {
if !found {
t.Errorf("Provider type %v not found in results", providerType)
}
}
}
// TestProviderRegistry_DetectProvider tests provider detection
func TestProviderRegistry_DetectProvider(t *testing.T) {
registry := NewProviderRegistry()
// Register providers
genericProvider := NewGenericProvider()
googleProvider := NewGoogleProvider()
azureProvider := NewAzureProvider()
githubProvider := NewGitHubProvider()
auth0Provider := NewAuth0Provider()
oktaProvider := NewOktaProvider()
keycloakProvider := NewKeycloakProvider()
cognitoProvider := NewAWSCognitoProvider()
gitlabProvider := NewGitLabProvider()
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(googleProvider)
registry.RegisterProvider(azureProvider)
registry.RegisterProvider(githubProvider)
registry.RegisterProvider(auth0Provider)
registry.RegisterProvider(oktaProvider)
registry.RegisterProvider(keycloakProvider)
registry.RegisterProvider(cognitoProvider)
registry.RegisterProvider(gitlabProvider)
tests := []struct {
name string
issuerURL string
expected OIDCProvider
}{
{
name: "Google provider detection",
issuerURL: "https://accounts.google.com",
expected: googleProvider,
},
{
name: "Google provider with path",
issuerURL: "https://accounts.google.com/oauth2",
expected: googleProvider,
},
{
name: "Azure provider detection - login.microsoftonline.com",
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
expected: azureProvider,
},
{
name: "Azure provider detection - sts.windows.net",
issuerURL: "https://sts.windows.net/tenant",
expected: azureProvider,
},
{
name: "GitHub provider detection",
issuerURL: "https://github.com/login/oauth",
expected: githubProvider,
},
{
name: "Auth0 provider detection",
issuerURL: "https://tenant.auth0.com",
expected: auth0Provider,
},
{
name: "Okta provider detection",
issuerURL: "https://tenant.okta.com",
expected: oktaProvider,
},
{
name: "Okta preview provider detection",
issuerURL: "https://tenant.oktapreview.com",
expected: oktaProvider,
},
{
name: "Keycloak provider detection",
issuerURL: "https://auth.example.com/auth/realms/master",
expected: keycloakProvider,
},
{
name: "AWS Cognito provider detection",
issuerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example",
expected: cognitoProvider,
},
{
name: "GitLab provider detection",
issuerURL: "https://gitlab.com/oauth",
expected: gitlabProvider,
},
{
name: "GitLab self-hosted detection - gitlab subdomain",
issuerURL: "https://gitlab.example.com",
expected: gitlabProvider,
},
{
name: "GitLab self-hosted detection - gitlab in domain",
issuerURL: "https://my-gitlab.company.io",
expected: gitlabProvider,
},
{
name: "GitLab self-hosted detection - gitlab prefix",
issuerURL: "https://gitlab-prod.internal.net",
expected: gitlabProvider,
},
{
name: "GitLab self-hosted detection - gitlab suffix",
issuerURL: "https://company-gitlab.net",
expected: gitlabProvider,
},
{
name: "Generic provider fallback",
issuerURL: "https://auth.example.com",
expected: genericProvider,
},
{
name: "Invalid URL",
issuerURL: "not-a-url",
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := registry.DetectProvider(tt.issuerURL)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
// TestProviderRegistry_DetectProvider_Caching tests cache behavior
func TestProviderRegistry_DetectProvider_Caching(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
issuerURL := "https://auth.example.com"
// First call should detect and cache
result1 := registry.DetectProvider(issuerURL)
if result1 != genericProvider {
t.Errorf("Expected generic provider, got %v", result1)
}
// Verify it's cached
registry.mu.RLock()
cachedResult, found := registry.cache[issuerURL]
registry.mu.RUnlock()
if !found {
t.Error("Expected result to be cached")
}
if cachedResult != genericProvider {
t.Errorf("Expected cached generic provider, got %v", cachedResult)
}
// Second call should return cached result
result2 := registry.DetectProvider(issuerURL)
if result2 != genericProvider {
t.Errorf("Expected cached generic provider, got %v", result2)
}
// Should be same instance (from cache)
if result1 != result2 {
t.Error("Expected same instance from cache")
}
}
// TestProviderRegistry_ClearCache tests cache clearing
func TestProviderRegistry_ClearCache(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
// Populate cache
registry.DetectProvider("https://auth1.example.com")
registry.DetectProvider("https://auth2.example.com")
// Verify cache has entries
registry.mu.RLock()
cacheSize := len(registry.cache)
registry.mu.RUnlock()
if cacheSize != 2 {
t.Errorf("Expected 2 cache entries, got %d", cacheSize)
}
// Clear cache
registry.ClearCache()
// Verify cache is empty
registry.mu.RLock()
cacheSize = len(registry.cache)
cacheCount := registry.cacheCount
registry.mu.RUnlock()
if cacheSize != 0 {
t.Errorf("Expected 0 cache entries after clear, got %d", cacheSize)
}
if cacheCount != 0 {
t.Errorf("Expected 0 cache count after clear, got %d", cacheCount)
}
}
// TestProviderRegistry_CacheEviction tests cache size limits and eviction
func TestProviderRegistry_CacheEviction(t *testing.T) {
registry := NewProviderRegistry()
registry.maxCacheSize = 2 // Set small cache size for testing
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
// Fill cache to capacity
registry.DetectProvider("https://auth1.example.com")
registry.DetectProvider("https://auth2.example.com")
// Verify cache is at capacity
registry.mu.RLock()
cacheSize := len(registry.cache)
registry.mu.RUnlock()
if cacheSize != 2 {
t.Errorf("Expected 2 cache entries, got %d", cacheSize)
}
// Add one more entry (should trigger eviction)
registry.DetectProvider("https://auth3.example.com")
// Cache size should still be at max
registry.mu.RLock()
cacheSize = len(registry.cache)
registry.mu.RUnlock()
if cacheSize != 2 {
t.Errorf("Expected 2 cache entries after eviction, got %d", cacheSize)
}
// Verify the new entry is cached
registry.mu.RLock()
_, found := registry.cache["https://auth3.example.com"]
registry.mu.RUnlock()
if !found {
t.Error("Expected new entry to be cached")
}
}
// TestProviderRegistry_ConcurrentAccess tests thread safety
func TestProviderRegistry_ConcurrentAccess(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
googleProvider := NewGoogleProvider()
azureProvider := NewAzureProvider()
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(googleProvider)
registry.RegisterProvider(azureProvider)
var wg sync.WaitGroup
goroutines := 10
iterations := 100
// Test concurrent detection
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
issuerURL := "https://accounts.google.com"
if id%2 == 0 {
issuerURL = "https://login.microsoftonline.com/tenant"
} else if id%3 == 0 {
issuerURL = "https://auth.example.com"
}
result := registry.DetectProvider(issuerURL)
if result == nil {
t.Errorf("Expected provider for URL %s", issuerURL)
}
}
}(i)
}
// Test concurrent registration
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
newProvider := NewGenericProvider()
registry.RegisterProvider(newProvider)
}
}()
// Test concurrent cache clearing
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
registry.ClearCache()
}
}()
wg.Wait()
// Verify final state is consistent
types := registry.GetRegisteredProviders()
if len(types) < 3 { // Should have at least the original 3
t.Errorf("Expected at least 3 provider types, got %d", len(types))
}
}
// TestProviderRegistry_DoubleCheckedLocking tests the double-checked locking pattern
func TestProviderRegistry_DoubleCheckedLocking(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
var wg sync.WaitGroup
goroutines := 100
issuerURL := "https://auth.example.com"
// Multiple goroutines trying to detect the same provider simultaneously
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result := registry.DetectProvider(issuerURL)
if result != genericProvider {
t.Errorf("Expected generic provider, got %v", result)
}
}()
}
wg.Wait()
// Verify only one cache entry was created
registry.mu.RLock()
cacheSize := len(registry.cache)
registry.mu.RUnlock()
if cacheSize != 1 {
t.Errorf("Expected 1 cache entry, got %d", cacheSize)
}
}
// TestProviderRegistry_DetectGitLabSelfHosted tests improved GitLab detection for issue #61
func TestProviderRegistry_DetectGitLabSelfHosted(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
gitlabProvider := NewGitLabProvider()
githubProvider := NewGitHubProvider()
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(gitlabProvider)
registry.RegisterProvider(githubProvider)
tests := []struct {
name string
issuerURL string
expected OIDCProvider
description string
}{
{
name: "GitLab.com official",
issuerURL: "https://gitlab.com",
expected: gitlabProvider,
description: "Should detect official GitLab.com",
},
{
name: "GitLab.com with path",
issuerURL: "https://gitlab.com/oauth/authorize",
expected: gitlabProvider,
description: "Should detect GitLab.com with path",
},
{
name: "Self-hosted gitlab.example.com",
issuerURL: "https://gitlab.example.com",
expected: gitlabProvider,
description: "Should detect gitlab as subdomain",
},
{
name: "Self-hosted my.gitlab.io",
issuerURL: "https://my.gitlab.io",
expected: gitlabProvider,
description: "Should detect gitlab in domain",
},
{
name: "Self-hosted example-gitlab.com",
issuerURL: "https://example-gitlab.com",
expected: gitlabProvider,
description: "Should detect gitlab as suffix",
},
{
name: "Self-hosted gitlab-prod.company.net",
issuerURL: "https://gitlab-prod.company.net",
expected: gitlabProvider,
description: "Should detect gitlab as prefix",
},
{
name: "Self-hosted my-gitlab.internal",
issuerURL: "https://my-gitlab.internal",
expected: gitlabProvider,
description: "Should detect gitlab in middle of host",
},
{
name: "Self-hosted company.gitlab.services",
issuerURL: "https://company.gitlab.services",
expected: gitlabProvider,
description: "Should detect gitlab in middle of domain",
},
{
name: "Self-hosted with port",
issuerURL: "https://gitlab.example.com:8443",
expected: gitlabProvider,
description: "Should detect GitLab with custom port",
},
{
name: "Self-hosted with path and query",
issuerURL: "https://gitlab.example.com/oauth?param=value",
expected: gitlabProvider,
description: "Should detect GitLab with complex URL",
},
{
name: "Case insensitive - GITLAB",
issuerURL: "https://GITLAB.example.com",
expected: gitlabProvider,
description: "Should detect GitLab case-insensitively",
},
{
name: "Case insensitive - GitLab",
issuerURL: "https://GitLab.example.com",
expected: gitlabProvider,
description: "Should detect GitLab with mixed case",
},
{
name: "Not GitLab - git prefix only",
issuerURL: "https://github.com",
expected: githubProvider, // Should match GitHub provider, not GitLab
description: "Should not match github.com as GitLab",
},
{
name: "Not GitLab - lab suffix only",
issuerURL: "https://mylab.example.com",
expected: genericProvider,
description: "Should not match partial gitlab string",
},
{
name: "Not GitLab - git and lab separate",
issuerURL: "https://git.mylab.example.com",
expected: genericProvider,
description: "Should not match git and lab when not together",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear cache to ensure fresh detection
registry.ClearCache()
result := registry.DetectProvider(tt.issuerURL)
if result != tt.expected {
t.Errorf("%s: Expected %v, got %v", tt.description, tt.expected, result)
}
})
}
}
// TestProviderRegistry_GitLabDetection_RealWorldURLs tests real-world GitLab URLs
func TestProviderRegistry_GitLabDetection_RealWorldURLs(t *testing.T) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
gitlabProvider := NewGitLabProvider()
githubProvider := NewGitHubProvider()
registry.RegisterProvider(genericProvider)
registry.RegisterProvider(gitlabProvider)
registry.RegisterProvider(githubProvider)
realWorldTests := []struct {
name string
issuerURL string
expected OIDCProvider
}{
// Actual self-hosted GitLab examples from issue #61
{
name: "Company self-hosted GitLab",
issuerURL: "https://gitlab.company.com",
expected: gitlabProvider,
},
{
name: "Organization GitLab instance with gitlab in subdomain",
issuerURL: "https://gitlab.organization.org",
expected: gitlabProvider,
},
{
name: "Internal GitLab server",
issuerURL: "https://gitlab.internal.corp",
expected: gitlabProvider,
},
{
name: "GitLab with custom subdomain",
issuerURL: "https://code.gitlab.mycompany.com",
expected: gitlabProvider,
},
// Negative cases to ensure we don't over-match
{
name: "GitHub should not match GitLab",
issuerURL: "https://github.com",
expected: githubProvider,
},
{
name: "Generic git server",
issuerURL: "https://git.example.com",
expected: genericProvider,
},
}
for _, tt := range realWorldTests {
t.Run(tt.name, func(t *testing.T) {
registry.ClearCache()
result := registry.DetectProvider(tt.issuerURL)
if result != tt.expected {
var expectedType, resultType string
if tt.expected != nil {
expectedType = fmt.Sprintf("%v", tt.expected.GetType())
} else {
expectedType = "nil"
}
if result != nil {
resultType = fmt.Sprintf("%v", result.GetType())
} else {
resultType = "nil"
}
t.Errorf("Expected provider type %s, got %s for URL %s",
expectedType, resultType, tt.issuerURL)
}
})
}
}
// Benchmark tests
func BenchmarkProviderRegistry_DetectProvider_Cached(b *testing.B) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
issuerURL := "https://auth.example.com"
// Warm up cache
registry.DetectProvider(issuerURL)
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.DetectProvider(issuerURL)
}
}
func BenchmarkProviderRegistry_DetectProvider_Uncached(b *testing.B) {
registry := NewProviderRegistry()
genericProvider := NewGenericProvider()
registry.RegisterProvider(genericProvider)
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.ClearCache() // Clear cache for each iteration
registry.DetectProvider("https://auth.example.com")
}
}
func BenchmarkProviderRegistry_RegisterProvider(b *testing.B) {
registry := NewProviderRegistry()
b.ResetTimer()
for i := 0; i < b.N; i++ {
provider := NewGenericProvider()
registry.RegisterProvider(provider)
}
}