mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Increase test coverage
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCreateProxy tests the CreateProxy method
|
||||
func TestCreateProxy(t *testing.T) {
|
||||
factory := NewFactory(nil)
|
||||
client, err := factory.CreateProxy()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create proxy client: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("Expected non-nil proxy client")
|
||||
}
|
||||
|
||||
// Verify proxy configuration specifics
|
||||
if client.Timeout != 60*time.Second {
|
||||
t.Errorf("Expected proxy timeout to be 60s, got %v", client.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateConfigEdgeCases tests additional validation scenarios
|
||||
func TestValidateConfigEdgeCases(t *testing.T) {
|
||||
factory := NewFactory(nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
shouldFail bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "Negative MaxIdleConnsPerHost",
|
||||
config: Config{
|
||||
MaxIdleConnsPerHost: -1,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "MaxIdleConnsPerHost cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "Excessive MaxIdleConnsPerHost",
|
||||
config: Config{
|
||||
MaxIdleConnsPerHost: 200,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "MaxIdleConnsPerHost too high",
|
||||
},
|
||||
{
|
||||
name: "Negative MaxConnsPerHost",
|
||||
config: Config{
|
||||
MaxConnsPerHost: -1,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "MaxConnsPerHost cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "Excessive MaxConnsPerHost",
|
||||
config: Config{
|
||||
MaxConnsPerHost: 300,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "MaxConnsPerHost too high",
|
||||
},
|
||||
{
|
||||
name: "Negative WriteBufferSize",
|
||||
config: Config{
|
||||
WriteBufferSize: -1,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "buffer sizes cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "Negative ReadBufferSize",
|
||||
config: Config{
|
||||
ReadBufferSize: -1,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "buffer sizes cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "Excessive WriteBufferSize",
|
||||
config: Config{
|
||||
WriteBufferSize: 2 * 1024 * 1024,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "buffer sizes too large",
|
||||
},
|
||||
{
|
||||
name: "Excessive ReadBufferSize",
|
||||
config: Config{
|
||||
ReadBufferSize: 2 * 1024 * 1024,
|
||||
},
|
||||
shouldFail: true,
|
||||
errorMsg: "buffer sizes too large",
|
||||
},
|
||||
{
|
||||
name: "Valid edge values",
|
||||
config: Config{
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
MaxConnsPerHost: 200,
|
||||
Timeout: 5 * time.Minute,
|
||||
WriteBufferSize: 1024 * 1024,
|
||||
ReadBufferSize: 1024 * 1024,
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := factory.ValidateConfig(&tc.config)
|
||||
if tc.shouldFail {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected validation to fail with message containing: %s", tc.errorMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected validation error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportPoolClose tests the Close method of TransportPool
|
||||
func TestTransportPoolClose(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
pool := &TransportPool{
|
||||
transports: make(map[string]*sharedTransport),
|
||||
maxConns: 20,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
clientCount: 0,
|
||||
maxClients: 5,
|
||||
}
|
||||
|
||||
// Create some transports
|
||||
config := PresetConfigs[ClientTypeDefault]
|
||||
transport1 := pool.GetOrCreateTransport(config)
|
||||
if transport1 == nil {
|
||||
t.Fatal("Failed to create transport")
|
||||
}
|
||||
|
||||
// Modify config slightly to create a different transport
|
||||
config.Timeout = 20 * time.Second
|
||||
transport2 := pool.GetOrCreateTransport(config)
|
||||
if transport2 == nil {
|
||||
t.Fatal("Failed to create second transport")
|
||||
}
|
||||
|
||||
// Verify transports were created
|
||||
pool.mu.RLock()
|
||||
initialCount := len(pool.transports)
|
||||
pool.mu.RUnlock()
|
||||
if initialCount == 0 {
|
||||
t.Fatal("Expected transports to be created")
|
||||
}
|
||||
|
||||
// Close the pool
|
||||
err := pool.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to close pool: %v", err)
|
||||
}
|
||||
|
||||
// Verify all transports were removed
|
||||
pool.mu.RLock()
|
||||
finalCount := len(pool.transports)
|
||||
pool.mu.RUnlock()
|
||||
if finalCount != 0 {
|
||||
t.Fatalf("Expected 0 transports after close, got %d", finalCount)
|
||||
}
|
||||
|
||||
// Verify client count was reset
|
||||
if pool.clientCount != 0 {
|
||||
t.Fatalf("Expected client count to be 0 after close, got %d", pool.clientCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoOpLogger tests the no-op logger implementation
|
||||
func TestNoOpLogger(t *testing.T) {
|
||||
logger := &noOpLogger{}
|
||||
|
||||
// These should not panic or cause any issues
|
||||
logger.Debug("test debug")
|
||||
logger.Debugf("test debug %s", "formatted")
|
||||
logger.Info("test info")
|
||||
logger.Infof("test info %s", "formatted")
|
||||
logger.Error("test error")
|
||||
logger.Errorf("test error %s", "formatted")
|
||||
|
||||
// Test using logger with factory
|
||||
factory := NewFactory(logger)
|
||||
client, err := factory.CreateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client with no-op logger: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("Expected non-nil client")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateClientWithCustomTLS tests creating client with custom TLS config
|
||||
func TestCreateClientWithCustomTLS(t *testing.T) {
|
||||
factory := NewFactory(nil)
|
||||
|
||||
customTLS := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
config := Config{
|
||||
Timeout: 10 * time.Second,
|
||||
MaxIdleConns: 10,
|
||||
MaxIdleConnsPerHost: 2,
|
||||
MaxConnsPerHost: 5,
|
||||
TLSConfig: customTLS,
|
||||
}
|
||||
|
||||
client, err := factory.CreateClient(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client with custom TLS: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("Expected non-nil client")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateClientWithMaxRedirects tests redirect limiting
|
||||
func TestCreateClientWithMaxRedirects(t *testing.T) {
|
||||
redirectCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectCount++
|
||||
if redirectCount <= 3 {
|
||||
http.Redirect(w, r, "/redirect", http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("final"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
factory := NewFactory(nil)
|
||||
|
||||
// Test with max redirects = 2 (should fail)
|
||||
config := Config{
|
||||
Timeout: 10 * time.Second,
|
||||
MaxRedirects: 2,
|
||||
MaxIdleConns: 10,
|
||||
MaxIdleConnsPerHost: 2,
|
||||
MaxConnsPerHost: 5,
|
||||
}
|
||||
|
||||
client, err := factory.CreateClient(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
redirectCount = 0
|
||||
_, err = client.Get(server.URL)
|
||||
if err == nil {
|
||||
t.Fatal("Expected redirect limit error")
|
||||
}
|
||||
|
||||
// Test with max redirects = 5 (should succeed)
|
||||
config.MaxRedirects = 5
|
||||
client, err = factory.CreateClient(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
redirectCount = 0
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportPoolMaxClientsLimit tests the max clients limitation
|
||||
func TestTransportPoolMaxClientsLimit(t *testing.T) {
|
||||
pool := &TransportPool{
|
||||
transports: make(map[string]*sharedTransport),
|
||||
maxConns: 20,
|
||||
clientCount: 0,
|
||||
maxClients: 2, // Set low limit for testing
|
||||
}
|
||||
|
||||
// Create transports up to the limit
|
||||
configs := []Config{
|
||||
{Timeout: 10 * time.Second},
|
||||
{Timeout: 20 * time.Second},
|
||||
{Timeout: 30 * time.Second}, // This should not create a new transport
|
||||
}
|
||||
|
||||
var transports []*http.Transport
|
||||
for i, config := range configs {
|
||||
transport := pool.GetOrCreateTransport(config)
|
||||
if i < 2 {
|
||||
if transport == nil {
|
||||
t.Fatalf("Expected transport %d to be created", i)
|
||||
}
|
||||
transports = append(transports, transport)
|
||||
} else {
|
||||
// When limit is reached, should return existing transport or nil
|
||||
if transport == nil {
|
||||
// This is acceptable - nil when limit reached
|
||||
t.Log("Transport creation blocked due to client limit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify client count doesn't exceed limit
|
||||
if pool.clientCount > pool.maxClients {
|
||||
t.Fatalf("Client count %d exceeds max %d", pool.clientCount, pool.maxClients)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCleanupIdleTransportsContext tests cleanup goroutine with context
|
||||
func TestCleanupIdleTransportsContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
pool := &TransportPool{
|
||||
transports: make(map[string]*sharedTransport),
|
||||
maxConns: 20,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
clientCount: 0,
|
||||
maxClients: 5,
|
||||
}
|
||||
|
||||
// Start cleanup goroutine
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
pool.cleanupIdleTransports(ctx)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Give it a moment to start
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Cancel context to stop cleanup
|
||||
cancel()
|
||||
|
||||
// Wait for goroutine to exit
|
||||
select {
|
||||
case <-done:
|
||||
// Success - goroutine exited
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("Cleanup goroutine did not exit after context cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFactoryWithLogger tests factory creation with custom logger
|
||||
func TestFactoryWithLogger(t *testing.T) {
|
||||
// Create a mock logger that implements the Logger interface
|
||||
logger := &MockLogger{}
|
||||
|
||||
factory := NewFactory(logger)
|
||||
if factory.logger == nil {
|
||||
t.Fatal("Expected logger to be set")
|
||||
}
|
||||
}
|
||||
|
||||
// MockLogger for testing
|
||||
type MockLogger struct {
|
||||
debugCalled bool
|
||||
debugfCalled bool
|
||||
infoCalled bool
|
||||
infofCalled bool
|
||||
errorCalled bool
|
||||
errorfCalled bool
|
||||
}
|
||||
|
||||
func (m *MockLogger) Debug(msg string) { m.debugCalled = true }
|
||||
func (m *MockLogger) Debugf(format string, args ...interface{}) { m.debugfCalled = true }
|
||||
func (m *MockLogger) Info(msg string) { m.infoCalled = true }
|
||||
func (m *MockLogger) Infof(format string, args ...interface{}) { m.infofCalled = true }
|
||||
func (m *MockLogger) Error(msg string) { m.errorCalled = true }
|
||||
func (m *MockLogger) Errorf(format string, args ...interface{}) { m.errorfCalled = true }
|
||||
|
||||
// TestCreateClientLogging tests that logger is called during client creation
|
||||
func TestCreateClientLogging(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
factory := NewFactory(logger)
|
||||
|
||||
client, err := factory.CreateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("Expected non-nil client")
|
||||
}
|
||||
|
||||
// Verify logger was called
|
||||
if !logger.debugfCalled {
|
||||
t.Error("Expected Debugf to be called during client creation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestAzureProvider_NewAzureProvider tests the constructor
|
||||
func TestAzureProvider_NewAzureProvider(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
if provider.BaseProvider == nil {
|
||||
t.Error("BaseProvider should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_GetType tests provider type
|
||||
func TestAzureProvider_GetType(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
if provider.GetType() != ProviderTypeAzure {
|
||||
t.Errorf("Expected ProviderTypeAzure, got %v", provider.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_GetCapabilities tests Azure-specific capabilities
|
||||
func TestAzureProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
if !capabilities.SupportsRefreshTokens {
|
||||
t.Error("Expected SupportsRefreshTokens to be true")
|
||||
}
|
||||
|
||||
if !capabilities.RequiresOfflineAccessScope {
|
||||
t.Error("Expected RequiresOfflineAccessScope to be true for Azure")
|
||||
}
|
||||
|
||||
if capabilities.RequiresPromptConsent {
|
||||
t.Error("Expected RequiresPromptConsent to be false for Azure")
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != "access" {
|
||||
t.Errorf("Expected PreferredTokenValidation 'access', got '%s'", capabilities.PreferredTokenValidation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_BuildAuthParams tests Azure-specific auth parameters
|
||||
func TestAzureProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputScopes []string
|
||||
expectedScopes []string
|
||||
shouldHaveResponseMode bool
|
||||
shouldAddOfflineAccess bool
|
||||
}{
|
||||
{
|
||||
name: "Basic scopes without offline_access",
|
||||
inputScopes: []string{"openid", "profile", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "email", "offline_access"},
|
||||
shouldHaveResponseMode: true,
|
||||
shouldAddOfflineAccess: true,
|
||||
},
|
||||
{
|
||||
name: "Scopes with offline_access already present",
|
||||
inputScopes: []string{"openid", "profile", "offline_access", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "offline_access", "email"},
|
||||
shouldHaveResponseMode: true,
|
||||
shouldAddOfflineAccess: false,
|
||||
},
|
||||
{
|
||||
name: "Only offline_access scope",
|
||||
inputScopes: []string{"offline_access"},
|
||||
expectedScopes: []string{"offline_access"},
|
||||
shouldHaveResponseMode: true,
|
||||
shouldAddOfflineAccess: false,
|
||||
},
|
||||
{
|
||||
name: "Empty scopes (should add offline_access)",
|
||||
inputScopes: []string{},
|
||||
expectedScopes: []string{"offline_access"},
|
||||
shouldHaveResponseMode: true,
|
||||
shouldAddOfflineAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
|
||||
result, err := provider.BuildAuthParams(baseParams, tt.inputScopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check Azure-specific parameters
|
||||
if tt.shouldHaveResponseMode {
|
||||
if result.URLValues.Get("response_mode") != "query" {
|
||||
t.Errorf("Expected response_mode 'query', got '%s'", result.URLValues.Get("response_mode"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check scopes
|
||||
if len(result.Scopes) != len(tt.expectedScopes) {
|
||||
t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(result.Scopes))
|
||||
}
|
||||
|
||||
for _, expectedScope := range tt.expectedScopes {
|
||||
found := false
|
||||
for _, actualScope := range result.Scopes {
|
||||
if actualScope == expectedScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' not found in result", expectedScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify offline_access is present
|
||||
hasOfflineAccess := false
|
||||
for _, scope := range result.Scopes {
|
||||
if scope == "offline_access" {
|
||||
hasOfflineAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOfflineAccess {
|
||||
t.Error("Azure provider should always include offline_access scope")
|
||||
}
|
||||
|
||||
// Verify original base parameters are preserved
|
||||
if result.URLValues.Get("client_id") != "test-client" {
|
||||
t.Errorf("Expected client_id 'test-client', got '%s'", result.URLValues.Get("client_id"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_ValidateTokens tests Azure-specific token validation logic
|
||||
func TestAzureProvider_ValidateTokens(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *mockSession
|
||||
verifierError error
|
||||
cacheData map[string]interface{}
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "Unauthenticated with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unauthenticated without refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JWT access token valid",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "valid.jwt.token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: nil,
|
||||
cacheData: map[string]interface{}{
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JWT access token invalid, valid ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "invalid.jwt.token",
|
||||
idToken: "valid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: errors.New("invalid token"),
|
||||
cacheData: map[string]interface{}{
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Opaque access token with valid ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "opaque-token-no-dots",
|
||||
idToken: "valid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
cacheData: map[string]interface{}{
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Opaque access token without ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "opaque-token-no-dots",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No access token, valid ID token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "valid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: nil,
|
||||
cacheData: map[string]interface{}{
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No access token, invalid ID token, with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "invalid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: errors.New("invalid token"),
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No tokens, with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No tokens, no refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier := &mockTokenVerifier{error: tt.verifierError}
|
||||
cache := &mockTokenCache{claims: make(map[string]map[string]interface{})}
|
||||
|
||||
// Set up cache data
|
||||
if tt.cacheData != nil {
|
||||
if tt.session.accessToken != "" && strings.Count(tt.session.accessToken, ".") == 2 {
|
||||
cache.claims[tt.session.accessToken] = tt.cacheData
|
||||
}
|
||||
if tt.session.idToken != "" {
|
||||
cache.claims[tt.session.idToken] = tt.cacheData
|
||||
}
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokens(tt.session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_ValidateConfig tests configuration validation
|
||||
func TestAzureProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
err := provider.ValidateConfig()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_InterfaceCompliance tests that Azure provider implements OIDCProvider
|
||||
func TestAzureProvider_InterfaceCompliance(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
// Verify it implements the OIDCProvider interface
|
||||
var _ OIDCProvider = provider
|
||||
}
|
||||
|
||||
// TestAzureProvider_OfflineAccessHandling tests comprehensive offline_access handling
|
||||
func TestAzureProvider_OfflineAccessHandling(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputScopes []string
|
||||
expectedCount int // Expected number of offline_access scopes (should be 1)
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "No offline_access - should add one",
|
||||
inputScopes: []string{"openid", "profile", "email"},
|
||||
expectedCount: 1,
|
||||
description: "Should add offline_access when not present",
|
||||
},
|
||||
{
|
||||
name: "One offline_access - should preserve",
|
||||
inputScopes: []string{"openid", "offline_access", "profile"},
|
||||
expectedCount: 1,
|
||||
description: "Should preserve existing offline_access",
|
||||
},
|
||||
{
|
||||
name: "Multiple offline_access - should preserve all",
|
||||
inputScopes: []string{"openid", "offline_access", "profile", "offline_access"},
|
||||
expectedCount: 2,
|
||||
description: "Should preserve all offline_access scopes if multiple exist",
|
||||
},
|
||||
{
|
||||
name: "Only offline_access",
|
||||
inputScopes: []string{"offline_access"},
|
||||
expectedCount: 1,
|
||||
description: "Should preserve when only offline_access is present",
|
||||
},
|
||||
{
|
||||
name: "Empty scopes - should add offline_access",
|
||||
inputScopes: []string{},
|
||||
expectedCount: 1,
|
||||
description: "Should add offline_access when no scopes provided",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseParams := make(url.Values)
|
||||
result, err := provider.BuildAuthParams(baseParams, tt.inputScopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Count offline_access occurrences in result
|
||||
offlineAccessCount := 0
|
||||
for _, scope := range result.Scopes {
|
||||
if scope == "offline_access" {
|
||||
offlineAccessCount++
|
||||
}
|
||||
}
|
||||
|
||||
if offlineAccessCount != tt.expectedCount {
|
||||
t.Errorf("Expected %d offline_access scopes in result, got %d", tt.expectedCount, offlineAccessCount)
|
||||
}
|
||||
|
||||
// Ensure at least one offline_access is always present
|
||||
if offlineAccessCount == 0 {
|
||||
t.Error("Azure provider should always have at least one offline_access scope")
|
||||
}
|
||||
|
||||
// Verify other scopes are preserved (except for the empty case)
|
||||
if len(tt.inputScopes) > 0 {
|
||||
for _, originalScope := range tt.inputScopes {
|
||||
found := false
|
||||
for _, resultScope := range result.Scopes {
|
||||
if resultScope == originalScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' to be preserved", originalScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_TokenValidationPriority tests access token vs ID token priority
|
||||
func TestAzureProvider_TokenValidationPriority(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
// Test that Azure prefers access tokens over ID tokens when both are JWT
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "valid.access.token",
|
||||
idToken: "valid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
|
||||
verifier := &mockTokenVerifier{} // Valid tokens
|
||||
cache := &mockTokenCache{
|
||||
claims: map[string]map[string]interface{}{
|
||||
"valid.access.token": {
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
"valid.id.token": {
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !result.Authenticated {
|
||||
t.Error("Should be authenticated with valid access token")
|
||||
}
|
||||
|
||||
if result.NeedsRefresh {
|
||||
t.Error("Should not need refresh with valid access token")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAzureProvider_AuthParamsPreservation tests that base parameters are not overwritten
|
||||
func TestAzureProvider_AuthParamsPreservation(t *testing.T) {
|
||||
provider := NewAzureProvider()
|
||||
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
baseParams.Set("redirect_uri", "https://example.com/callback")
|
||||
baseParams.Set("response_type", "code")
|
||||
baseParams.Set("state", "test-state")
|
||||
baseParams.Set("nonce", "test-nonce")
|
||||
|
||||
scopes := []string{"openid", "profile"}
|
||||
|
||||
result, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify all original parameters are preserved
|
||||
expectedParams := map[string]string{
|
||||
"client_id": "test-client",
|
||||
"redirect_uri": "https://example.com/callback",
|
||||
"response_type": "code",
|
||||
"state": "test-state",
|
||||
"nonce": "test-nonce",
|
||||
"response_mode": "query", // Added by Azure provider
|
||||
}
|
||||
|
||||
for key, expectedValue := range expectedParams {
|
||||
actualValue := result.URLValues.Get(key)
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("Expected %s '%s', got '%s'", key, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify scopes (should include offline_access)
|
||||
if len(result.Scopes) != 3 {
|
||||
t.Errorf("Expected 3 scopes (including offline_access), got %d", len(result.Scopes))
|
||||
}
|
||||
|
||||
expectedScopes := []string{"openid", "profile", "offline_access"}
|
||||
for _, expectedScope := range expectedScopes {
|
||||
found := false
|
||||
for _, actualScope := range result.Scopes {
|
||||
if actualScope == expectedScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' not found", expectedScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkAzureProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
scopes := []string{"openid", "profile", "email"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.BuildAuthParams(baseParams, scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAzureProvider_ValidateTokens(b *testing.B) {
|
||||
provider := NewAzureProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "valid.access.token",
|
||||
idToken: "valid.id.token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
claims: map[string]map[string]interface{}{
|
||||
"valid.access.token": {
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock implementations for testing
|
||||
type mockSession struct {
|
||||
authenticated bool
|
||||
idToken string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func (s *mockSession) GetIDToken() string { return s.idToken }
|
||||
func (s *mockSession) GetAccessToken() string { return s.accessToken }
|
||||
func (s *mockSession) GetRefreshToken() string { return s.refreshToken }
|
||||
func (s *mockSession) GetAuthenticated() bool { return s.authenticated }
|
||||
|
||||
type mockTokenVerifier struct {
|
||||
error error
|
||||
}
|
||||
|
||||
func (v *mockTokenVerifier) VerifyToken(token string) error {
|
||||
return v.error
|
||||
}
|
||||
|
||||
type mockTokenCache struct {
|
||||
claims map[string]map[string]interface{}
|
||||
}
|
||||
|
||||
func (c *mockTokenCache) Get(key string) (map[string]interface{}, bool) {
|
||||
claims, exists := c.claims[key]
|
||||
return claims, exists
|
||||
}
|
||||
|
||||
// TestBaseProvider_GetType tests the default provider type
|
||||
func TestBaseProvider_GetType(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
if provider.GetType() != ProviderTypeGeneric {
|
||||
t.Errorf("Expected ProviderTypeGeneric, got %v", provider.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_GetCapabilities tests the default capabilities
|
||||
func TestBaseProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
if !capabilities.SupportsRefreshTokens {
|
||||
t.Error("Expected SupportsRefreshTokens to be true")
|
||||
}
|
||||
|
||||
if !capabilities.RequiresOfflineAccessScope {
|
||||
t.Error("Expected RequiresOfflineAccessScope to be true")
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != "id" {
|
||||
t.Errorf("Expected PreferredTokenValidation 'id', got '%s'", capabilities.PreferredTokenValidation)
|
||||
}
|
||||
|
||||
if capabilities.RequiresPromptConsent {
|
||||
t.Error("Expected RequiresPromptConsent to be false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokens_Unauthenticated tests validation when not authenticated
|
||||
func TestBaseProvider_ValidateTokens_Unauthenticated(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{authenticated: false}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
refreshToken string
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "No refresh token",
|
||||
refreshToken: "",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has refresh token",
|
||||
refreshToken: "refresh-token",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
session.refreshToken = tt.refreshToken
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokens_AuthenticatedNoAccessToken tests authenticated session without access token
|
||||
func TestBaseProvider_ValidateTokens_AuthenticatedNoAccessToken(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "", // No access token
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
refreshToken string
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "No access token, no refresh token",
|
||||
refreshToken: "",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No access token, has refresh token",
|
||||
refreshToken: "refresh-token",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
session.refreshToken = tt.refreshToken
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokens_AuthenticatedNoIDToken tests authenticated session without ID token
|
||||
func TestBaseProvider_ValidateTokens_AuthenticatedNoIDToken(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "", // No ID token
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
refreshToken string
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "No ID token, no refresh token",
|
||||
refreshToken: "",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No ID token, has refresh token",
|
||||
refreshToken: "refresh-token",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
session.refreshToken = tt.refreshToken
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokens_TokenVerificationFailure tests token verification failures
|
||||
func TestBaseProvider_ValidateTokens_TokenVerificationFailure(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
accessToken: "access-token",
|
||||
idToken: "id-token",
|
||||
}
|
||||
cache := &mockTokenCache{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
verifierError error
|
||||
refreshToken string
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "Token expired, has refresh token",
|
||||
verifierError: errors.New("token has expired"),
|
||||
refreshToken: "refresh-token",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Token expired, no refresh token",
|
||||
verifierError: errors.New("token has expired"),
|
||||
refreshToken: "",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Other verification error, has refresh token",
|
||||
verifierError: errors.New("invalid signature"),
|
||||
refreshToken: "refresh-token",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Other verification error, no refresh token",
|
||||
verifierError: errors.New("invalid signature"),
|
||||
refreshToken: "",
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier := &mockTokenVerifier{error: tt.verifierError}
|
||||
session.refreshToken = tt.refreshToken
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokenExpiry tests token expiry validation logic
|
||||
func TestBaseProvider_ValidateTokenExpiry(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{refreshToken: "refresh-token"}
|
||||
|
||||
now := time.Now()
|
||||
gracePeriod := 5 * time.Minute
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
claims map[string]interface{}
|
||||
cacheFound bool
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "Token not found in cache, has refresh token",
|
||||
claims: nil,
|
||||
cacheFound: false,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Claims without exp, has refresh token",
|
||||
claims: map[string]interface{}{"sub": "user123"},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Token expired (beyond grace period), has refresh token",
|
||||
claims: map[string]interface{}{
|
||||
"exp": float64(now.Add(-10 * time.Minute).Unix()),
|
||||
},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Token expires within grace period, has refresh token",
|
||||
claims: map[string]interface{}{
|
||||
"exp": float64(now.Add(2 * time.Minute).Unix()),
|
||||
},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Token valid (beyond grace period)",
|
||||
claims: map[string]interface{}{
|
||||
"exp": float64(now.Add(10 * time.Minute).Unix()),
|
||||
},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := &mockTokenCache{claims: make(map[string]map[string]interface{})}
|
||||
if tt.cacheFound {
|
||||
cache.claims["test-token"] = tt.claims
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokenExpiry(session, "test-token", cache, gracePeriod)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateTokenExpiry_NoRefreshToken tests expiry validation without refresh token
|
||||
func TestBaseProvider_ValidateTokenExpiry_NoRefreshToken(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{refreshToken: ""} // No refresh token
|
||||
|
||||
now := time.Now()
|
||||
gracePeriod := 5 * time.Minute
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
claims map[string]interface{}
|
||||
cacheFound bool
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "Token not found in cache, no refresh token",
|
||||
claims: nil,
|
||||
cacheFound: false,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Claims without exp, no refresh token",
|
||||
claims: map[string]interface{}{"sub": "user123"},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Token expires within grace period, no refresh token",
|
||||
claims: map[string]interface{}{
|
||||
"exp": float64(now.Add(2 * time.Minute).Unix()),
|
||||
},
|
||||
cacheFound: true,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := &mockTokenCache{claims: make(map[string]map[string]interface{})}
|
||||
if tt.cacheFound {
|
||||
cache.claims["test-token"] = tt.claims
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokenExpiry(session, "test-token", cache, gracePeriod)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_BuildAuthParams tests authorization parameter building
|
||||
func TestBaseProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scopes []string
|
||||
expectedScopes []string
|
||||
}{
|
||||
{
|
||||
name: "No existing offline_access scope",
|
||||
scopes: []string{"openid", "profile", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "email", "offline_access"},
|
||||
},
|
||||
{
|
||||
name: "Existing offline_access scope",
|
||||
scopes: []string{"openid", "profile", "offline_access", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "offline_access", "email"},
|
||||
},
|
||||
{
|
||||
name: "Empty scopes",
|
||||
scopes: []string{},
|
||||
expectedScopes: []string{"offline_access"},
|
||||
},
|
||||
{
|
||||
name: "Only offline_access",
|
||||
scopes: []string{"offline_access"},
|
||||
expectedScopes: []string{"offline_access"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseParams := make(map[string][]string)
|
||||
baseParams["client_id"] = []string{"test-client"}
|
||||
|
||||
result, err := provider.BuildAuthParams(baseParams, tt.scopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Scopes) != len(tt.expectedScopes) {
|
||||
t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(result.Scopes))
|
||||
}
|
||||
|
||||
// Check that all expected scopes are present
|
||||
for _, expectedScope := range tt.expectedScopes {
|
||||
found := false
|
||||
for _, actualScope := range result.Scopes {
|
||||
if actualScope == expectedScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' not found in result", expectedScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify base parameters are preserved
|
||||
if result.URLValues.Get("client_id") != "test-client" {
|
||||
t.Errorf("Expected client_id 'test-client', got '%s'", result.URLValues.Get("client_id"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_HandleTokenRefresh tests token refresh handling
|
||||
func TestBaseProvider_HandleTokenRefresh(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
tokenData := &TokenResult{
|
||||
IDToken: "new-id-token",
|
||||
AccessToken: "new-access-token",
|
||||
RefreshToken: "new-refresh-token",
|
||||
}
|
||||
|
||||
// Base provider should do nothing and return no error
|
||||
err := provider.HandleTokenRefresh(tokenData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseProvider_ValidateConfig tests configuration validation
|
||||
func TestBaseProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
// Base provider should always return valid configuration
|
||||
err := provider.ValidateConfig()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewBaseProvider tests the constructor
|
||||
func TestNewBaseProvider(t *testing.T) {
|
||||
provider := NewBaseProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
// Verify it implements the OIDCProvider interface
|
||||
var _ OIDCProvider = provider
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkBaseProvider_ValidateTokens(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "test-token",
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
claims: map[string]map[string]interface{}{
|
||||
"test-token": {
|
||||
"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.ValidateTokens(session, verifier, cache, time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBaseProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewBaseProvider()
|
||||
baseParams := make(map[string][]string)
|
||||
baseParams["client_id"] = []string{"test-client"}
|
||||
scopes := []string{"openid", "profile", "email"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.BuildAuthParams(baseParams, scopes)
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,16 @@ func (f *ProviderFactory) CreateProvider(issuerURL string) (OIDCProvider, error)
|
||||
return nil, fmt.Errorf("issuer URL cannot be empty")
|
||||
}
|
||||
|
||||
if _, err := url.Parse(issuerURL); err != nil {
|
||||
parsedURL, err := url.Parse(issuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid issuer URL format: %w", err)
|
||||
}
|
||||
|
||||
// Check if the URL has a valid scheme and host
|
||||
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
return nil, fmt.Errorf("invalid issuer URL format: URL must have a valid scheme and host")
|
||||
}
|
||||
|
||||
provider := f.registry.DetectProvider(issuerURL)
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("unable to detect provider for issuer URL: %s", issuerURL)
|
||||
@@ -100,6 +106,11 @@ func (f *ProviderFactory) IsProviderSupported(issuerURL string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the URL has a valid scheme and host
|
||||
if normalizedURL.Scheme == "" || normalizedURL.Host == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
host := strings.ToLower(normalizedURL.Host)
|
||||
supportedProviders := f.GetSupportedProviders()
|
||||
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestProviderFactory_NewProviderFactory tests the factory constructor
|
||||
func TestProviderFactory_NewProviderFactory(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
if factory == nil {
|
||||
t.Fatal("Expected factory to be created, got nil")
|
||||
}
|
||||
|
||||
if factory.registry == nil {
|
||||
t.Error("Expected registry to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_CreateProvider tests provider creation by issuer URL
|
||||
func TestProviderFactory_CreateProvider(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
expectedType ProviderType
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "Google provider",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
expectedType: ProviderTypeGoogle,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Google provider with path",
|
||||
issuerURL: "https://accounts.google.com/oauth2",
|
||||
expectedType: ProviderTypeGoogle,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider - login.microsoftonline.com",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||
expectedType: ProviderTypeAzure,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider - sts.windows.net",
|
||||
issuerURL: "https://sts.windows.net/tenant-id",
|
||||
expectedType: ProviderTypeAzure,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Generic provider",
|
||||
issuerURL: "https://auth.example.com",
|
||||
expectedType: ProviderTypeGeneric,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty issuer URL",
|
||||
issuerURL: "",
|
||||
wantErr: true,
|
||||
errMsg: "issuer URL cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
issuerURL: "not-a-url",
|
||||
wantErr: true,
|
||||
errMsg: "invalid issuer URL format",
|
||||
},
|
||||
{
|
||||
name: "URL without scheme",
|
||||
issuerURL: "example.com",
|
||||
wantErr: true,
|
||||
errMsg: "invalid issuer URL format",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := factory.CreateProvider(tt.issuerURL)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Expected error containing '%s', got '%s'", tt.errMsg, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
if provider.GetType() != tt.expectedType {
|
||||
t.Errorf("Expected provider type %v, got %v", tt.expectedType, provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_CreateProviderByType tests provider creation by type
|
||||
func TestProviderFactory_CreateProviderByType(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerType ProviderType
|
||||
expectedType ProviderType
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "Generic provider",
|
||||
providerType: ProviderTypeGeneric,
|
||||
expectedType: ProviderTypeGeneric,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Google provider",
|
||||
providerType: ProviderTypeGoogle,
|
||||
expectedType: ProviderTypeGoogle,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
providerType: ProviderTypeAzure,
|
||||
expectedType: ProviderTypeAzure,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid provider type",
|
||||
providerType: ProviderType(999),
|
||||
wantErr: true,
|
||||
errMsg: "unsupported provider type",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := factory.CreateProviderByType(tt.providerType)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Expected error containing '%s', got '%s'", tt.errMsg, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
if provider.GetType() != tt.expectedType {
|
||||
t.Errorf("Expected provider type %v, got %v", tt.expectedType, provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_GetSupportedProviders tests listing supported providers
|
||||
func TestProviderFactory_GetSupportedProviders(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
supported := factory.GetSupportedProviders()
|
||||
|
||||
// Verify expected provider types are present
|
||||
expectedTypes := []ProviderType{
|
||||
ProviderTypeGeneric,
|
||||
ProviderTypeGoogle,
|
||||
ProviderTypeAzure,
|
||||
}
|
||||
|
||||
for _, expectedType := range expectedTypes {
|
||||
if _, exists := supported[expectedType]; !exists {
|
||||
t.Errorf("Expected provider type %v to be supported", expectedType)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Google patterns
|
||||
googlePatterns := supported[ProviderTypeGoogle]
|
||||
if len(googlePatterns) != 1 || googlePatterns[0] != "accounts.google.com" {
|
||||
t.Errorf("Expected Google patterns ['accounts.google.com'], got %v", googlePatterns)
|
||||
}
|
||||
|
||||
// Verify Azure patterns
|
||||
azurePatterns := supported[ProviderTypeAzure]
|
||||
expectedAzurePatterns := []string{"login.microsoftonline.com", "sts.windows.net"}
|
||||
if len(azurePatterns) != len(expectedAzurePatterns) {
|
||||
t.Errorf("Expected %d Azure patterns, got %d", len(expectedAzurePatterns), len(azurePatterns))
|
||||
}
|
||||
|
||||
for _, expectedPattern := range expectedAzurePatterns {
|
||||
found := false
|
||||
for _, pattern := range azurePatterns {
|
||||
if pattern == expectedPattern {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected Azure pattern '%s' not found", expectedPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Generic patterns
|
||||
genericPatterns := supported[ProviderTypeGeneric]
|
||||
if len(genericPatterns) != 1 || genericPatterns[0] != "*" {
|
||||
t.Errorf("Expected Generic patterns ['*'], got %v", genericPatterns)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_DetectProviderType tests provider type detection
|
||||
func TestProviderFactory_DetectProviderType(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
expectedType ProviderType
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Google provider detection",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
expectedType: ProviderTypeGoogle,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider detection",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
|
||||
expectedType: ProviderTypeAzure,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Generic provider detection",
|
||||
issuerURL: "https://auth.example.com",
|
||||
expectedType: ProviderTypeGeneric,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
issuerURL: "not-a-url",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
issuerURL: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
providerType, err := factory.DetectProviderType(tt.issuerURL)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if providerType != tt.expectedType {
|
||||
t.Errorf("Expected provider type %v, got %v", tt.expectedType, providerType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_IsProviderSupported tests provider support checking
|
||||
func TestProviderFactory_IsProviderSupported(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Google provider supported",
|
||||
issuerURL: "https://accounts.google.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Google provider with subdomain supported",
|
||||
issuerURL: "https://accounts.google.com/oauth2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Azure login.microsoftonline.com supported",
|
||||
issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Azure sts.windows.net supported",
|
||||
issuerURL: "https://sts.windows.net/tenant",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Generic provider supported (wildcard)",
|
||||
issuerURL: "https://auth.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Any valid URL supported (wildcard)",
|
||||
issuerURL: "https://custom-auth.company.org",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Empty URL not supported",
|
||||
issuerURL: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format not supported",
|
||||
issuerURL: "not-a-url",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL without scheme not supported",
|
||||
issuerURL: "example.com",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := factory.IsProviderSupported(tt.issuerURL)
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderFactory_IntegrationTest tests the full flow
|
||||
func TestProviderFactory_IntegrationTest(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
// Test Google provider flow
|
||||
t.Run("Google Provider Flow", func(t *testing.T) {
|
||||
// Check if supported
|
||||
if !factory.IsProviderSupported("https://accounts.google.com") {
|
||||
t.Error("Google provider should be supported")
|
||||
}
|
||||
|
||||
// Detect type
|
||||
providerType, err := factory.DetectProviderType("https://accounts.google.com")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error detecting Google provider: %v", err)
|
||||
}
|
||||
if providerType != ProviderTypeGoogle {
|
||||
t.Errorf("Expected ProviderTypeGoogle, got %v", providerType)
|
||||
}
|
||||
|
||||
// Create provider by URL
|
||||
provider, err := factory.CreateProvider("https://accounts.google.com")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating Google provider: %v", err)
|
||||
}
|
||||
if provider.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("Expected ProviderTypeGoogle, got %v", provider.GetType())
|
||||
}
|
||||
|
||||
// Create provider by type
|
||||
provider2, err := factory.CreateProviderByType(ProviderTypeGoogle)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating Google provider by type: %v", err)
|
||||
}
|
||||
if provider2.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("Expected ProviderTypeGoogle, got %v", provider2.GetType())
|
||||
}
|
||||
})
|
||||
|
||||
// Test Azure provider flow
|
||||
t.Run("Azure Provider Flow", func(t *testing.T) {
|
||||
azureURL := "https://login.microsoftonline.com/tenant/v2.0"
|
||||
|
||||
// Check if supported
|
||||
if !factory.IsProviderSupported(azureURL) {
|
||||
t.Error("Azure provider should be supported")
|
||||
}
|
||||
|
||||
// Detect type
|
||||
providerType, err := factory.DetectProviderType(azureURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error detecting Azure provider: %v", err)
|
||||
}
|
||||
if providerType != ProviderTypeAzure {
|
||||
t.Errorf("Expected ProviderTypeAzure, got %v", providerType)
|
||||
}
|
||||
|
||||
// Create provider
|
||||
provider, err := factory.CreateProvider(azureURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating Azure provider: %v", err)
|
||||
}
|
||||
if provider.GetType() != ProviderTypeAzure {
|
||||
t.Errorf("Expected ProviderTypeAzure, got %v", provider.GetType())
|
||||
}
|
||||
})
|
||||
|
||||
// Test Generic provider flow
|
||||
t.Run("Generic Provider Flow", func(t *testing.T) {
|
||||
genericURL := "https://auth.custom-provider.com"
|
||||
|
||||
// Check if supported
|
||||
if !factory.IsProviderSupported(genericURL) {
|
||||
t.Error("Generic provider should be supported")
|
||||
}
|
||||
|
||||
// Detect type
|
||||
providerType, err := factory.DetectProviderType(genericURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error detecting generic provider: %v", err)
|
||||
}
|
||||
if providerType != ProviderTypeGeneric {
|
||||
t.Errorf("Expected ProviderTypeGeneric, got %v", providerType)
|
||||
}
|
||||
|
||||
// Create provider
|
||||
provider, err := factory.CreateProvider(genericURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating generic provider: %v", err)
|
||||
}
|
||||
if provider.GetType() != ProviderTypeGeneric {
|
||||
t.Errorf("Expected ProviderTypeGeneric, got %v", provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestProviderFactory_CaseInsensitiveHostMatching tests case insensitive host matching
|
||||
func TestProviderFactory_CaseInsensitiveHostMatching(t *testing.T) {
|
||||
factory := NewProviderFactory()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issuerURL string
|
||||
expectedType ProviderType
|
||||
}{
|
||||
{
|
||||
name: "Google with uppercase",
|
||||
issuerURL: "https://ACCOUNTS.GOOGLE.COM",
|
||||
expectedType: ProviderTypeGoogle,
|
||||
},
|
||||
{
|
||||
name: "Google with mixed case",
|
||||
issuerURL: "https://Accounts.Google.Com",
|
||||
expectedType: ProviderTypeGoogle,
|
||||
},
|
||||
{
|
||||
name: "Azure with uppercase",
|
||||
issuerURL: "https://LOGIN.MICROSOFTONLINE.COM/tenant",
|
||||
expectedType: ProviderTypeAzure,
|
||||
},
|
||||
{
|
||||
name: "Azure STS with mixed case",
|
||||
issuerURL: "https://Sts.Windows.Net/tenant",
|
||||
expectedType: ProviderTypeAzure,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Should be supported
|
||||
if !factory.IsProviderSupported(tt.issuerURL) {
|
||||
t.Errorf("URL %s should be supported", tt.issuerURL)
|
||||
}
|
||||
|
||||
// Should detect correct type
|
||||
providerType, err := factory.DetectProviderType(tt.issuerURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if providerType != tt.expectedType {
|
||||
t.Errorf("Expected %v, got %v", tt.expectedType, providerType)
|
||||
}
|
||||
|
||||
// Should create correct provider
|
||||
provider, err := factory.CreateProvider(tt.issuerURL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if provider.GetType() != tt.expectedType {
|
||||
t.Errorf("Expected %v, got %v", tt.expectedType, provider.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkProviderFactory_CreateProvider(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
issuerURL := "https://accounts.google.com"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
factory.CreateProvider(issuerURL)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderFactory_IsProviderSupported(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
issuerURL := "https://auth.example.com"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
factory.IsProviderSupported(issuerURL)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProviderFactory_DetectProviderType(b *testing.B) {
|
||||
factory := NewProviderFactory()
|
||||
issuerURL := "https://login.microsoftonline.com/tenant"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
factory.DetectProviderType(issuerURL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGenericProvider_NewGenericProvider tests the constructor
|
||||
func TestGenericProvider_NewGenericProvider(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
if provider.BaseProvider == nil {
|
||||
t.Error("BaseProvider should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenericProvider_GetType tests provider type
|
||||
func TestGenericProvider_GetType(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
if provider.GetType() != ProviderTypeGeneric {
|
||||
t.Errorf("Expected ProviderTypeGeneric, got %v", provider.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenericProvider_GetCapabilities tests that it inherits BaseProvider capabilities
|
||||
func TestGenericProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
// Should have the same capabilities as BaseProvider
|
||||
baseProvider := NewBaseProvider()
|
||||
baseCapabilities := baseProvider.GetCapabilities()
|
||||
|
||||
if capabilities.SupportsRefreshTokens != baseCapabilities.SupportsRefreshTokens {
|
||||
t.Errorf("Expected SupportsRefreshTokens %v, got %v",
|
||||
baseCapabilities.SupportsRefreshTokens, capabilities.SupportsRefreshTokens)
|
||||
}
|
||||
|
||||
if capabilities.RequiresOfflineAccessScope != baseCapabilities.RequiresOfflineAccessScope {
|
||||
t.Errorf("Expected RequiresOfflineAccessScope %v, got %v",
|
||||
baseCapabilities.RequiresOfflineAccessScope, capabilities.RequiresOfflineAccessScope)
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != baseCapabilities.PreferredTokenValidation {
|
||||
t.Errorf("Expected PreferredTokenValidation %v, got %v",
|
||||
baseCapabilities.PreferredTokenValidation, capabilities.PreferredTokenValidation)
|
||||
}
|
||||
|
||||
if capabilities.RequiresPromptConsent != baseCapabilities.RequiresPromptConsent {
|
||||
t.Errorf("Expected RequiresPromptConsent %v, got %v",
|
||||
baseCapabilities.RequiresPromptConsent, capabilities.RequiresPromptConsent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenericProvider_InterfaceCompliance tests that Generic provider implements OIDCProvider
|
||||
func TestGenericProvider_InterfaceCompliance(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
// Verify it implements the OIDCProvider interface
|
||||
var _ OIDCProvider = provider
|
||||
}
|
||||
|
||||
// TestGenericProvider_InheritsBaseProviderBehavior tests inherited functionality
|
||||
func TestGenericProvider_InheritsBaseProviderBehavior(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
baseProvider := NewBaseProvider()
|
||||
|
||||
// Test BuildAuthParams behavior is the same
|
||||
scopes := []string{"openid", "profile", "email"}
|
||||
baseParams := make(map[string][]string)
|
||||
baseParams["client_id"] = []string{"test-client"}
|
||||
|
||||
genericResult, genericErr := provider.BuildAuthParams(baseParams, scopes)
|
||||
baseResult, baseErr := baseProvider.BuildAuthParams(baseParams, scopes)
|
||||
|
||||
if (genericErr == nil) != (baseErr == nil) {
|
||||
t.Errorf("BuildAuthParams error mismatch: generic=%v, base=%v", genericErr, baseErr)
|
||||
}
|
||||
|
||||
if genericErr == nil && baseErr == nil {
|
||||
// Compare scopes length (offline_access should be added)
|
||||
if len(genericResult.Scopes) != len(baseResult.Scopes) {
|
||||
t.Errorf("BuildAuthParams scope count mismatch: generic=%d, base=%d",
|
||||
len(genericResult.Scopes), len(baseResult.Scopes))
|
||||
}
|
||||
|
||||
// Verify offline_access is added in both cases
|
||||
genericHasOffline := false
|
||||
baseHasOffline := false
|
||||
|
||||
for _, scope := range genericResult.Scopes {
|
||||
if scope == "offline_access" {
|
||||
genericHasOffline = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, scope := range baseResult.Scopes {
|
||||
if scope == "offline_access" {
|
||||
baseHasOffline = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if genericHasOffline != baseHasOffline {
|
||||
t.Errorf("offline_access scope handling mismatch: generic=%v, base=%v",
|
||||
genericHasOffline, baseHasOffline)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ValidateConfig behavior is the same
|
||||
genericConfigErr := provider.ValidateConfig()
|
||||
baseConfigErr := baseProvider.ValidateConfig()
|
||||
|
||||
if (genericConfigErr == nil) != (baseConfigErr == nil) {
|
||||
t.Errorf("ValidateConfig error mismatch: generic=%v, base=%v", genericConfigErr, baseConfigErr)
|
||||
}
|
||||
|
||||
// Test HandleTokenRefresh behavior is the same
|
||||
tokenData := &TokenResult{IDToken: "test-token"}
|
||||
genericRefreshErr := provider.HandleTokenRefresh(tokenData)
|
||||
baseRefreshErr := baseProvider.HandleTokenRefresh(tokenData)
|
||||
|
||||
if (genericRefreshErr == nil) != (baseRefreshErr == nil) {
|
||||
t.Errorf("HandleTokenRefresh error mismatch: generic=%v, base=%v",
|
||||
genericRefreshErr, baseRefreshErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenericProvider_ValidateTokens tests token validation inheritance
|
||||
func TestGenericProvider_ValidateTokens(t *testing.T) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *mockSession
|
||||
verifierError error
|
||||
expectedResult ValidationResult
|
||||
}{
|
||||
{
|
||||
name: "Unauthenticated with refresh token",
|
||||
session: &mockSession{
|
||||
authenticated: false,
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Authenticated with valid tokens",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "valid-token",
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: nil,
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: true,
|
||||
NeedsRefresh: false,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Authenticated with invalid token, has refresh",
|
||||
session: &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "invalid-token",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
verifierError: &testError{"token expired"},
|
||||
expectedResult: ValidationResult{
|
||||
Authenticated: false,
|
||||
NeedsRefresh: true,
|
||||
IsExpired: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier := &mockTokenVerifier{error: tt.verifierError}
|
||||
cache := &mockTokenCache{
|
||||
claims: map[string]map[string]interface{}{
|
||||
"valid-token": {
|
||||
"exp": float64(9999999999), // Far future
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokens(tt.session, verifier, cache, 0)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Authenticated != tt.expectedResult.Authenticated {
|
||||
t.Errorf("Expected Authenticated %v, got %v", tt.expectedResult.Authenticated, result.Authenticated)
|
||||
}
|
||||
|
||||
if result.NeedsRefresh != tt.expectedResult.NeedsRefresh {
|
||||
t.Errorf("Expected NeedsRefresh %v, got %v", tt.expectedResult.NeedsRefresh, result.NeedsRefresh)
|
||||
}
|
||||
|
||||
if result.IsExpired != tt.expectedResult.IsExpired {
|
||||
t.Errorf("Expected IsExpired %v, got %v", tt.expectedResult.IsExpired, result.IsExpired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkGenericProvider_GetType(b *testing.B) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenericProvider_GetCapabilities(b *testing.B) {
|
||||
provider := NewGenericProvider()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
// Test error type for testing
|
||||
type testError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *testError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGoogleProvider_NewGoogleProvider tests the constructor
|
||||
func TestGoogleProvider_NewGoogleProvider(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
if provider == nil {
|
||||
t.Fatal("Expected provider to be created, got nil")
|
||||
}
|
||||
|
||||
if provider.BaseProvider == nil {
|
||||
t.Error("BaseProvider should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_GetType tests provider type
|
||||
func TestGoogleProvider_GetType(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
if provider.GetType() != ProviderTypeGoogle {
|
||||
t.Errorf("Expected ProviderTypeGoogle, got %v", provider.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_GetCapabilities tests Google-specific capabilities
|
||||
func TestGoogleProvider_GetCapabilities(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
capabilities := provider.GetCapabilities()
|
||||
|
||||
if !capabilities.SupportsRefreshTokens {
|
||||
t.Error("Expected SupportsRefreshTokens to be true")
|
||||
}
|
||||
|
||||
if capabilities.RequiresOfflineAccessScope {
|
||||
t.Error("Expected RequiresOfflineAccessScope to be false for Google")
|
||||
}
|
||||
|
||||
if !capabilities.RequiresPromptConsent {
|
||||
t.Error("Expected RequiresPromptConsent to be true for Google")
|
||||
}
|
||||
|
||||
if capabilities.PreferredTokenValidation != "id" {
|
||||
t.Errorf("Expected PreferredTokenValidation 'id', got '%s'", capabilities.PreferredTokenValidation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_BuildAuthParams tests Google-specific auth parameters
|
||||
func TestGoogleProvider_BuildAuthParams(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputScopes []string
|
||||
expectedScopes []string
|
||||
shouldHaveAccessType bool
|
||||
shouldHavePrompt bool
|
||||
}{
|
||||
{
|
||||
name: "Basic scopes without offline_access",
|
||||
inputScopes: []string{"openid", "profile", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "email"},
|
||||
shouldHaveAccessType: true,
|
||||
shouldHavePrompt: true,
|
||||
},
|
||||
{
|
||||
name: "Scopes with offline_access (should be filtered out)",
|
||||
inputScopes: []string{"openid", "profile", "offline_access", "email"},
|
||||
expectedScopes: []string{"openid", "profile", "email"},
|
||||
shouldHaveAccessType: true,
|
||||
shouldHavePrompt: true,
|
||||
},
|
||||
{
|
||||
name: "Only offline_access scope (should be filtered out)",
|
||||
inputScopes: []string{"offline_access"},
|
||||
expectedScopes: []string{},
|
||||
shouldHaveAccessType: true,
|
||||
shouldHavePrompt: true,
|
||||
},
|
||||
{
|
||||
name: "Empty scopes",
|
||||
inputScopes: []string{},
|
||||
expectedScopes: []string{},
|
||||
shouldHaveAccessType: true,
|
||||
shouldHavePrompt: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
|
||||
result, err := provider.BuildAuthParams(baseParams, tt.inputScopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check Google-specific parameters
|
||||
if tt.shouldHaveAccessType {
|
||||
if result.URLValues.Get("access_type") != "offline" {
|
||||
t.Errorf("Expected access_type 'offline', got '%s'", result.URLValues.Get("access_type"))
|
||||
}
|
||||
}
|
||||
|
||||
if tt.shouldHavePrompt {
|
||||
if result.URLValues.Get("prompt") != "consent" {
|
||||
t.Errorf("Expected prompt 'consent', got '%s'", result.URLValues.Get("prompt"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check filtered scopes
|
||||
if len(result.Scopes) != len(tt.expectedScopes) {
|
||||
t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(result.Scopes))
|
||||
}
|
||||
|
||||
for _, expectedScope := range tt.expectedScopes {
|
||||
found := false
|
||||
for _, actualScope := range result.Scopes {
|
||||
if actualScope == expectedScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' not found in result", expectedScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure offline_access is not in the result scopes
|
||||
for _, scope := range result.Scopes {
|
||||
if scope == "offline_access" {
|
||||
t.Error("offline_access scope should be filtered out for Google")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify original base parameters are preserved
|
||||
if result.URLValues.Get("client_id") != "test-client" {
|
||||
t.Errorf("Expected client_id 'test-client', got '%s'", result.URLValues.Get("client_id"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_ValidateConfig tests configuration validation
|
||||
func TestGoogleProvider_ValidateConfig(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
err := provider.ValidateConfig()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_InterfaceCompliance tests that Google provider implements OIDCProvider
|
||||
func TestGoogleProvider_InterfaceCompliance(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
// Verify it implements the OIDCProvider interface
|
||||
var _ OIDCProvider = provider
|
||||
}
|
||||
|
||||
// TestGoogleProvider_OfflineAccessFiltering tests comprehensive offline_access filtering
|
||||
func TestGoogleProvider_OfflineAccessFiltering(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputScopes []string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Multiple offline_access occurrences",
|
||||
inputScopes: []string{"openid", "offline_access", "profile", "offline_access", "email"},
|
||||
description: "Should remove all instances of offline_access",
|
||||
},
|
||||
{
|
||||
name: "Case sensitive filtering",
|
||||
inputScopes: []string{"openid", "OFFLINE_ACCESS", "profile", "offline_access"},
|
||||
description: "Should only remove exact case matches",
|
||||
},
|
||||
{
|
||||
name: "Similar but different scopes",
|
||||
inputScopes: []string{"openid", "offline_access_extended", "profile", "offline_access"},
|
||||
description: "Should only remove exact offline_access matches",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseParams := make(url.Values)
|
||||
result, err := provider.BuildAuthParams(baseParams, tt.inputScopes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Count offline_access occurrences in result
|
||||
offlineAccessCount := 0
|
||||
for _, scope := range result.Scopes {
|
||||
if scope == "offline_access" {
|
||||
offlineAccessCount++
|
||||
}
|
||||
}
|
||||
|
||||
if offlineAccessCount != 0 {
|
||||
t.Errorf("Expected 0 offline_access scopes in result, got %d", offlineAccessCount)
|
||||
}
|
||||
|
||||
// Verify other scopes are preserved
|
||||
for _, originalScope := range tt.inputScopes {
|
||||
if originalScope == "offline_access" {
|
||||
continue // Skip the filtered scope
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, resultScope := range result.Scopes {
|
||||
if resultScope == originalScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' to be preserved", originalScope)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_BaseProviderInheritance tests inherited functionality from BaseProvider
|
||||
func TestGoogleProvider_BaseProviderInheritance(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
// Test ValidateTokens (inherited from BaseProvider)
|
||||
session := &mockSession{
|
||||
authenticated: true,
|
||||
idToken: "test-token",
|
||||
accessToken: "access-token", // Add access token for proper validation
|
||||
}
|
||||
verifier := &mockTokenVerifier{}
|
||||
cache := &mockTokenCache{
|
||||
claims: map[string]map[string]interface{}{
|
||||
"test-token": {
|
||||
"exp": float64(9999999999), // Far future
|
||||
"sub": "user123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := provider.ValidateTokens(session, verifier, cache, 0)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !result.Authenticated {
|
||||
t.Error("Expected result to be authenticated")
|
||||
}
|
||||
|
||||
// Test HandleTokenRefresh (inherited from BaseProvider)
|
||||
tokenData := &TokenResult{IDToken: "new-token"}
|
||||
err = provider.HandleTokenRefresh(tokenData)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoogleProvider_AuthParamsPreservation tests that base parameters are not overwritten
|
||||
func TestGoogleProvider_AuthParamsPreservation(t *testing.T) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
baseParams.Set("redirect_uri", "https://example.com/callback")
|
||||
baseParams.Set("response_type", "code")
|
||||
baseParams.Set("state", "test-state")
|
||||
baseParams.Set("nonce", "test-nonce")
|
||||
|
||||
scopes := []string{"openid", "profile"}
|
||||
|
||||
result, err := provider.BuildAuthParams(baseParams, scopes)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify all original parameters are preserved
|
||||
expectedParams := map[string]string{
|
||||
"client_id": "test-client",
|
||||
"redirect_uri": "https://example.com/callback",
|
||||
"response_type": "code",
|
||||
"state": "test-state",
|
||||
"nonce": "test-nonce",
|
||||
"access_type": "offline", // Added by Google provider
|
||||
"prompt": "consent", // Added by Google provider
|
||||
}
|
||||
|
||||
for key, expectedValue := range expectedParams {
|
||||
actualValue := result.URLValues.Get(key)
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("Expected %s '%s', got '%s'", key, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify scopes
|
||||
if len(result.Scopes) != 2 {
|
||||
t.Errorf("Expected 2 scopes, got %d", len(result.Scopes))
|
||||
}
|
||||
|
||||
expectedScopes := []string{"openid", "profile"}
|
||||
for _, expectedScope := range expectedScopes {
|
||||
found := false
|
||||
for _, actualScope := range result.Scopes {
|
||||
if actualScope == expectedScope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected scope '%s' not found", expectedScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkGoogleProvider_BuildAuthParams(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
baseParams := make(url.Values)
|
||||
baseParams.Set("client_id", "test-client")
|
||||
scopes := []string{"openid", "profile", "email", "offline_access"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.BuildAuthParams(baseParams, scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleProvider_GetCapabilities(b *testing.B) {
|
||||
provider := NewGoogleProvider()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider.GetCapabilities()
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,14 @@ func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
host := normalizedURL.Host
|
||||
|
||||
// Check if the URL has a valid scheme and host
|
||||
if normalizedURL.Scheme == "" || normalizedURL.Host == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert host to lowercase for case-insensitive matching
|
||||
host := strings.ToLower(normalizedURL.Host)
|
||||
|
||||
for _, p := range r.providers {
|
||||
switch p.GetType() {
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"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()
|
||||
|
||||
registry.RegisterProvider(genericProvider)
|
||||
registry.RegisterProvider(googleProvider)
|
||||
registry.RegisterProvider(azureProvider)
|
||||
|
||||
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: "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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user