mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
1138 lines
30 KiB
Go
1138 lines
30 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Mock implementations for testing
|
|
// ============================================================================
|
|
|
|
type MockLogger struct {
|
|
debugMessages []string
|
|
infoMessages []string
|
|
errorMessages []string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewMockLogger() *MockLogger {
|
|
return &MockLogger{
|
|
debugMessages: []string{},
|
|
infoMessages: []string{},
|
|
errorMessages: []string{},
|
|
}
|
|
}
|
|
|
|
func (m *MockLogger) Debug(msg string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.debugMessages = append(m.debugMessages, msg)
|
|
}
|
|
|
|
func (m *MockLogger) Debugf(format string, args ...interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.debugMessages = append(m.debugMessages, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (m *MockLogger) Info(msg string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.infoMessages = append(m.infoMessages, msg)
|
|
}
|
|
|
|
func (m *MockLogger) Infof(format string, args ...interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.infoMessages = append(m.infoMessages, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (m *MockLogger) Error(msg string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.errorMessages = append(m.errorMessages, msg)
|
|
}
|
|
|
|
func (m *MockLogger) Errorf(format string, args ...interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.errorMessages = append(m.errorMessages, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (m *MockLogger) GetDebugMessages() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return append([]string{}, m.debugMessages...)
|
|
}
|
|
|
|
func (m *MockLogger) GetInfoMessages() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return append([]string{}, m.infoMessages...)
|
|
}
|
|
|
|
func (m *MockLogger) GetErrorMessages() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return append([]string{}, m.errorMessages...)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Config Creation Tests
|
|
// ============================================================================
|
|
|
|
func TestCreateConfig(t *testing.T) {
|
|
t.Run("CreateConfig_DefaultValues", func(t *testing.T) {
|
|
config := CreateConfig()
|
|
|
|
if config == nil {
|
|
t.Fatal("Expected config to be created, got nil")
|
|
}
|
|
|
|
// Check default scopes
|
|
expectedScopes := []string{"openid", "profile", "email"}
|
|
if len(config.Scopes) != len(expectedScopes) {
|
|
t.Errorf("Expected %d default scopes, got %d", len(expectedScopes), len(config.Scopes))
|
|
}
|
|
for i, scope := range expectedScopes {
|
|
if config.Scopes[i] != scope {
|
|
t.Errorf("Expected scope %s at position %d, got %s", scope, i, config.Scopes[i])
|
|
}
|
|
}
|
|
|
|
// Check default log level
|
|
if config.LogLevel != "INFO" {
|
|
t.Errorf("Expected default log level '%s', got '%s'", "INFO", config.LogLevel)
|
|
}
|
|
|
|
// Check default rate limit
|
|
if config.RateLimit != 10 {
|
|
t.Errorf("Expected default rate limit %d, got %d", 10, config.RateLimit)
|
|
}
|
|
|
|
// Check ForceHTTPS default
|
|
if !config.ForceHTTPS {
|
|
t.Error("Expected ForceHTTPS to be true by default")
|
|
}
|
|
|
|
// Check EnablePKCE default
|
|
if !config.EnablePKCE {
|
|
t.Error("Expected EnablePKCE to be true by default")
|
|
}
|
|
|
|
// Check OverrideScopes default
|
|
if config.OverrideScopes {
|
|
t.Error("Expected OverrideScopes to be false by default")
|
|
}
|
|
|
|
// Check RefreshGracePeriodSeconds default
|
|
if config.RefreshGracePeriodSeconds != 60 {
|
|
t.Errorf("Expected default RefreshGracePeriodSeconds %d, got %d", 60, config.RefreshGracePeriodSeconds)
|
|
}
|
|
})
|
|
|
|
t.Run("CreateConfig_EmptyHeaders", func(t *testing.T) {
|
|
config := CreateConfig()
|
|
if config.Headers == nil {
|
|
t.Error("Expected Headers to be initialized, got nil")
|
|
}
|
|
if len(config.Headers) != 0 {
|
|
t.Errorf("Expected empty Headers slice, got %d headers", len(config.Headers))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Settings Tests
|
|
// ============================================================================
|
|
|
|
func TestNewSettings(t *testing.T) {
|
|
logger := NewMockLogger()
|
|
settings := NewSettings(logger)
|
|
|
|
if settings == nil {
|
|
t.Fatal("Expected settings to be created, got nil")
|
|
}
|
|
|
|
if settings.logger != logger {
|
|
t.Error("Logger not set correctly in settings")
|
|
}
|
|
}
|
|
|
|
func TestInitializeTraefikOidc_Deprecated(t *testing.T) {
|
|
logger := NewMockLogger()
|
|
settings := NewSettings(logger)
|
|
config := CreateConfig()
|
|
|
|
_, err := settings.InitializeTraefikOidc(context.Background(), nil, config, "test")
|
|
|
|
if err == nil {
|
|
t.Error("Expected error for deprecated function, got nil")
|
|
}
|
|
|
|
expectedError := "InitializeTraefikOidc is deprecated - use New function from main package instead"
|
|
if err.Error() != expectedError {
|
|
t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSetupHeaderTemplates_Deprecated(t *testing.T) {
|
|
logger := NewMockLogger()
|
|
settings := NewSettings(logger)
|
|
config := CreateConfig()
|
|
|
|
err := settings.setupHeaderTemplates(nil, config, logger)
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error for deprecated function stub, got %v", err)
|
|
}
|
|
|
|
// Check that debug message was logged
|
|
debugMessages := logger.GetDebugMessages()
|
|
found := false
|
|
for _, msg := range debugMessages {
|
|
if msg == "setupHeaderTemplates is deprecated" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Expected deprecation debug message")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Uncovered Functions Tests (Smoke Tests)
|
|
// ============================================================================
|
|
|
|
func TestUncoveredConfigFunctions(t *testing.T) {
|
|
t.Run("NewLogger", func(t *testing.T) {
|
|
logger := NewLogger("INFO")
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = logger
|
|
})
|
|
|
|
t.Run("CreateDefaultHTTPClient", func(t *testing.T) {
|
|
client := CreateDefaultHTTPClient()
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = client
|
|
})
|
|
|
|
t.Run("CreateTokenHTTPClient", func(t *testing.T) {
|
|
client := CreateTokenHTTPClient()
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = client
|
|
})
|
|
|
|
t.Run("GetGlobalCacheManager", func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
manager := GetGlobalCacheManager(&wg)
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = manager
|
|
})
|
|
|
|
t.Run("NewSessionManager", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test", false, "secret", nil)
|
|
// This function may return an error, which is acceptable
|
|
_ = sessionManager
|
|
_ = err
|
|
})
|
|
|
|
t.Run("NewErrorRecoveryManager", func(t *testing.T) {
|
|
recoveryManager := NewErrorRecoveryManager(nil)
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = recoveryManager
|
|
})
|
|
|
|
t.Run("extractClaims", func(t *testing.T) {
|
|
// Test extractClaims with a mock token
|
|
testToken := "test.token.here"
|
|
claims, err := extractClaims(testToken)
|
|
// This function may return an error for invalid tokens
|
|
_ = claims
|
|
_ = err
|
|
})
|
|
|
|
t.Run("startReplayCacheCleanup", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
startReplayCacheCleanup(ctx, nil)
|
|
// This is mainly a smoke test to ensure it doesn't panic
|
|
})
|
|
|
|
t.Run("GetGlobalMemoryMonitor", func(t *testing.T) {
|
|
monitor := GetGlobalMemoryMonitor()
|
|
// This function returns nil in the current implementation
|
|
// Testing for the function call itself
|
|
_ = monitor
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Templated Header Config Tests
|
|
// ============================================================================
|
|
|
|
func TestTemplateParsingInConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
headers []HeaderConfig
|
|
expectedTemplates int
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "Single Valid Template",
|
|
headers: []HeaderConfig{
|
|
{Name: "X-Email", Value: "{{.Claims.email}}"},
|
|
},
|
|
expectedTemplates: 1,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Multiple Valid Templates",
|
|
headers: []HeaderConfig{
|
|
{Name: "X-Email", Value: "{{.Claims.email}}"},
|
|
{Name: "X-Subject", Value: "{{.Claims.sub}}"},
|
|
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
|
},
|
|
expectedTemplates: 3,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Template with Conditional",
|
|
headers: []HeaderConfig{
|
|
{Name: "X-User", Value: "{{if .Claims.preferred_username}}{{.Claims.preferred_username}}{{else}}{{.Claims.sub}}{{end}}"},
|
|
},
|
|
expectedTemplates: 1,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Template with Range",
|
|
headers: []HeaderConfig{
|
|
{Name: "X-Groups", Value: "{{range .Claims.groups}}{{.}},{{end}}"},
|
|
},
|
|
expectedTemplates: 1,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
parsedTemplates := make(map[string]*template.Template)
|
|
|
|
for _, header := range tc.headers {
|
|
tmpl, err := template.New(header.Name).Parse(header.Value)
|
|
if err != nil {
|
|
if !tc.expectError {
|
|
t.Errorf("Failed to parse template for header %s: %v", header.Name, err)
|
|
}
|
|
continue
|
|
}
|
|
parsedTemplates[header.Name] = tmpl
|
|
}
|
|
|
|
if !tc.expectError && len(parsedTemplates) != tc.expectedTemplates {
|
|
t.Errorf("Expected %d parsed templates, got %d", tc.expectedTemplates, len(parsedTemplates))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHeaderConfig(t *testing.T) {
|
|
headers := []HeaderConfig{
|
|
{Name: "X-User-Email", Value: "{{.Email}}"},
|
|
{Name: "X-User-Groups", Value: "{{.Groups}}"},
|
|
{Name: "X-Static-Header", Value: "static-value"},
|
|
}
|
|
|
|
if len(headers) != 3 {
|
|
t.Errorf("Expected 3 headers, got %d", len(headers))
|
|
}
|
|
|
|
// Test individual header properties
|
|
tests := []struct {
|
|
index int
|
|
expectedName string
|
|
expectedValue string
|
|
}{
|
|
{0, "X-User-Email", "{{.Email}}"},
|
|
{1, "X-User-Groups", "{{.Groups}}"},
|
|
{2, "X-Static-Header", "static-value"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expectedName, func(t *testing.T) {
|
|
if headers[tt.index].Name != tt.expectedName {
|
|
t.Errorf("Header[%d].Name = %s, expected %s",
|
|
tt.index, headers[tt.index].Name, tt.expectedName)
|
|
}
|
|
if headers[tt.index].Value != tt.expectedValue {
|
|
t.Errorf("Header[%d].Value = %s, expected %s",
|
|
tt.index, headers[tt.index].Value, tt.expectedValue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Auth Config Tests
|
|
// ============================================================================
|
|
|
|
func TestAuthConfig(t *testing.T) {
|
|
t.Run("Scopes Configuration", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
expectedScopes []string
|
|
}{
|
|
{
|
|
name: "Default scopes",
|
|
config: &Config{
|
|
Scopes: []string{"openid", "profile", "email"},
|
|
},
|
|
expectedScopes: []string{"openid", "profile", "email"},
|
|
},
|
|
{
|
|
name: "Custom scopes",
|
|
config: &Config{
|
|
Scopes: []string{"openid", "custom_scope"},
|
|
},
|
|
expectedScopes: []string{"openid", "custom_scope"},
|
|
},
|
|
{
|
|
name: "Empty scopes",
|
|
config: &Config{
|
|
Scopes: []string{},
|
|
},
|
|
expectedScopes: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if !equalSlices(tc.config.Scopes, tc.expectedScopes) {
|
|
t.Errorf("Expected scopes %v, got %v", tc.expectedScopes, tc.config.Scopes)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Excluded URLs Configuration", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
expectedExclude []string
|
|
}{
|
|
{
|
|
name: "No excluded URLs",
|
|
config: &Config{},
|
|
expectedExclude: nil,
|
|
},
|
|
{
|
|
name: "With excluded URLs",
|
|
config: &Config{
|
|
ExcludedURLs: []string{"/health", "/metrics", "/api/public"},
|
|
},
|
|
expectedExclude: []string{"/health", "/metrics", "/api/public"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.expectedExclude == nil {
|
|
if tc.config.ExcludedURLs != nil {
|
|
t.Errorf("Expected nil ExcludedURLs, got %v", tc.config.ExcludedURLs)
|
|
}
|
|
} else if !equalSlices(tc.config.ExcludedURLs, tc.expectedExclude) {
|
|
t.Errorf("Expected ExcludedURLs %v, got %v", tc.expectedExclude, tc.config.ExcludedURLs)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Config Parser Tests
|
|
// ============================================================================
|
|
|
|
func TestConfigParser(t *testing.T) {
|
|
t.Run("ParseProviderURL", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "Valid HTTPS URL",
|
|
input: "https://provider.com/.well-known/openid-configuration",
|
|
expected: "https://provider.com/.well-known/openid-configuration",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid HTTP URL",
|
|
input: "http://localhost:8080/.well-known/openid-configuration",
|
|
expected: "http://localhost:8080/.well-known/openid-configuration",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "URL with trailing slash",
|
|
input: "https://provider.com/",
|
|
expected: "https://provider.com/",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Invalid URL",
|
|
input: "not-a-url",
|
|
expected: "",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "Empty URL",
|
|
input: "",
|
|
expected: "",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
config := &Config{ProviderURL: tc.input}
|
|
// Since we're testing parsing, we'd validate the URL format
|
|
if tc.input == "" {
|
|
if !tc.expectError {
|
|
t.Error("Expected error for empty URL")
|
|
}
|
|
} else if tc.input == "not-a-url" {
|
|
// In real parsing, this would be caught
|
|
if !tc.expectError {
|
|
t.Error("Expected error for invalid URL")
|
|
}
|
|
} else {
|
|
if config.ProviderURL != tc.expected {
|
|
t.Errorf("Expected URL %s, got %s", tc.expected, config.ProviderURL)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("ParseTimeouts", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
refreshInterval string
|
|
gracePeriod string
|
|
expectedRefresh time.Duration
|
|
expectedGrace time.Duration
|
|
}{
|
|
{
|
|
name: "Default values",
|
|
refreshInterval: "",
|
|
gracePeriod: "",
|
|
expectedRefresh: 0,
|
|
expectedGrace: 0,
|
|
},
|
|
{
|
|
name: "Custom refresh interval",
|
|
refreshInterval: "5m",
|
|
gracePeriod: "",
|
|
expectedRefresh: 5 * time.Minute,
|
|
expectedGrace: 0,
|
|
},
|
|
{
|
|
name: "Custom grace period",
|
|
refreshInterval: "",
|
|
gracePeriod: "30s",
|
|
expectedRefresh: 0,
|
|
expectedGrace: 30 * time.Second,
|
|
},
|
|
{
|
|
name: "Both custom",
|
|
refreshInterval: "10m",
|
|
gracePeriod: "1m",
|
|
expectedRefresh: 10 * time.Minute,
|
|
expectedGrace: 1 * time.Minute,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// This would be part of config parsing
|
|
// Here we're just testing the concept
|
|
var refreshDuration, graceDuration time.Duration
|
|
|
|
if tc.refreshInterval != "" {
|
|
d, _ := time.ParseDuration(tc.refreshInterval)
|
|
refreshDuration = d
|
|
}
|
|
if tc.gracePeriod != "" {
|
|
d, _ := time.ParseDuration(tc.gracePeriod)
|
|
graceDuration = d
|
|
}
|
|
|
|
if refreshDuration != tc.expectedRefresh {
|
|
t.Errorf("Expected refresh %v, got %v", tc.expectedRefresh, refreshDuration)
|
|
}
|
|
if graceDuration != tc.expectedGrace {
|
|
t.Errorf("Expected grace %v, got %v", tc.expectedGrace, graceDuration)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Scope and String Map Functions Tests
|
|
// ============================================================================
|
|
|
|
func TestDeduplicateScopes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "No duplicates",
|
|
input: []string{"openid", "profile", "email"},
|
|
expected: []string{"openid", "profile", "email"},
|
|
},
|
|
{
|
|
name: "With duplicates",
|
|
input: []string{"openid", "profile", "email", "openid", "profile"},
|
|
expected: []string{"openid", "profile", "email"},
|
|
},
|
|
{
|
|
name: "All duplicates",
|
|
input: []string{"openid", "openid", "openid"},
|
|
expected: []string{"openid"},
|
|
},
|
|
{
|
|
name: "Empty input",
|
|
input: []string{},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "Single element",
|
|
input: []string{"openid"},
|
|
expected: []string{"openid"},
|
|
},
|
|
{
|
|
name: "Mixed case duplicates",
|
|
input: []string{"openid", "OpenID", "profile", "Profile"},
|
|
expected: []string{"openid", "OpenID", "profile", "Profile"}, // Case sensitive
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := deduplicateScopes(tt.input)
|
|
if !reflect.DeepEqual(result, tt.expected) {
|
|
t.Errorf("deduplicateScopes(%v) = %v, expected %v", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMergeScopes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
defaultScopes []string
|
|
userScopes []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "Merge empty user scopes",
|
|
defaultScopes: []string{"openid", "profile"},
|
|
userScopes: []string{},
|
|
expected: []string{"openid", "profile"},
|
|
},
|
|
{
|
|
name: "Merge empty default scopes",
|
|
defaultScopes: []string{},
|
|
userScopes: []string{"email", "groups"},
|
|
expected: []string{"email", "groups"},
|
|
},
|
|
{
|
|
name: "Merge both non-empty",
|
|
defaultScopes: []string{"openid", "profile"},
|
|
userScopes: []string{"email", "groups"},
|
|
expected: []string{"openid", "profile", "email", "groups"},
|
|
},
|
|
{
|
|
name: "Merge with overlapping scopes",
|
|
defaultScopes: []string{"openid", "profile"},
|
|
userScopes: []string{"profile", "email"},
|
|
expected: []string{"openid", "profile", "profile", "email"}, // Doesn't deduplicate
|
|
},
|
|
{
|
|
name: "Both empty",
|
|
defaultScopes: []string{},
|
|
userScopes: []string{},
|
|
expected: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mergeScopes(tt.defaultScopes, tt.userScopes)
|
|
if !reflect.DeepEqual(result, tt.expected) {
|
|
t.Errorf("mergeScopes(%v, %v) = %v, expected %v",
|
|
tt.defaultScopes, tt.userScopes, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateStringMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
expected map[string]struct{}
|
|
}{
|
|
{
|
|
name: "Normal input",
|
|
input: []string{"item1", "item2", "item3"},
|
|
expected: map[string]struct{}{
|
|
"item1": {},
|
|
"item2": {},
|
|
"item3": {},
|
|
},
|
|
},
|
|
{
|
|
name: "With duplicates",
|
|
input: []string{"item1", "item2", "item1"},
|
|
expected: map[string]struct{}{
|
|
"item1": {},
|
|
"item2": {},
|
|
},
|
|
},
|
|
{
|
|
name: "Empty input",
|
|
input: []string{},
|
|
expected: map[string]struct{}{},
|
|
},
|
|
{
|
|
name: "Single item",
|
|
input: []string{"item"},
|
|
expected: map[string]struct{}{
|
|
"item": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := createStringMap(tt.input)
|
|
if !reflect.DeepEqual(result, tt.expected) {
|
|
t.Errorf("createStringMap(%v) = %v, expected %v", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateCaseInsensitiveStringMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
expected map[string]struct{}
|
|
}{
|
|
{
|
|
name: "Mixed case input",
|
|
input: []string{"Item1", "ITEM2", "item3"},
|
|
expected: map[string]struct{}{
|
|
"item1": {},
|
|
"item2": {},
|
|
"item3": {},
|
|
},
|
|
},
|
|
{
|
|
name: "All uppercase",
|
|
input: []string{"ITEM1", "ITEM2", "ITEM3"},
|
|
expected: map[string]struct{}{
|
|
"item1": {},
|
|
"item2": {},
|
|
"item3": {},
|
|
},
|
|
},
|
|
{
|
|
name: "All lowercase",
|
|
input: []string{"item1", "item2", "item3"},
|
|
expected: map[string]struct{}{
|
|
"item1": {},
|
|
"item2": {},
|
|
"item3": {},
|
|
},
|
|
},
|
|
{
|
|
name: "Case variations of same item",
|
|
input: []string{"Item", "ITEM", "item", "iTem"},
|
|
expected: map[string]struct{}{
|
|
"item": {},
|
|
},
|
|
},
|
|
{
|
|
name: "Empty input",
|
|
input: []string{},
|
|
expected: map[string]struct{}{},
|
|
},
|
|
{
|
|
name: "With special characters",
|
|
input: []string{"[email protected]", "[email protected]"},
|
|
expected: map[string]struct{}{
|
|
"[email protected]": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := createCaseInsensitiveStringMap(tt.input)
|
|
if !reflect.DeepEqual(result, tt.expected) {
|
|
t.Errorf("createCaseInsensitiveStringMap(%v) = %v, expected %v",
|
|
tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsTestMode(t *testing.T) {
|
|
// This function is a stub that always returns false
|
|
result := isTestMode()
|
|
if result != false {
|
|
t.Errorf("isTestMode() = %v, expected false", result)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Constants Tests
|
|
// ============================================================================
|
|
|
|
func TestConstants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
got interface{}
|
|
expected interface{}
|
|
}{
|
|
{"minEncryptionKeyLength", minEncryptionKeyLength, 16},
|
|
{"ConstSessionTimeout", ConstSessionTimeout, 86400},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.got != tt.expected {
|
|
t.Errorf("%s = %v, expected %v", tt.name, tt.got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultExcludedURLs(t *testing.T) {
|
|
// Check that default excluded URLs are defined correctly
|
|
expectedURLs := []string{
|
|
"/favicon.ico",
|
|
"/robots.txt",
|
|
"/health",
|
|
"/.well-known/",
|
|
"/metrics",
|
|
"/ping",
|
|
"/api/",
|
|
"/static/",
|
|
"/assets/",
|
|
"/js/",
|
|
"/css/",
|
|
"/images/",
|
|
"/fonts/",
|
|
}
|
|
|
|
if len(defaultExcludedURLs) != len(expectedURLs) {
|
|
t.Errorf("Expected %d default excluded URLs, got %d",
|
|
len(expectedURLs), len(defaultExcludedURLs))
|
|
}
|
|
|
|
for _, url := range expectedURLs {
|
|
if _, exists := defaultExcludedURLs[url]; !exists {
|
|
t.Errorf("Expected URL %s to be in defaultExcludedURLs", url)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Complex Config Tests
|
|
// ============================================================================
|
|
|
|
func TestConfig_AllFieldsPopulated(t *testing.T) {
|
|
config := &Config{
|
|
ProviderURL: "https://auth.example.com",
|
|
ClientID: "complex-client-id",
|
|
ClientSecret: "complex-client-secret",
|
|
CallbackURL: "/auth/callback",
|
|
LogoutURL: "/auth/logout",
|
|
PostLogoutRedirectURI: "https://example.com/goodbye",
|
|
SessionEncryptionKey: strings.Repeat("a", 32),
|
|
ForceHTTPS: true,
|
|
LogLevel: "DEBUG",
|
|
Scopes: []string{"openid", "profile", "email", "groups", "custom"},
|
|
OverrideScopes: true,
|
|
AllowedUsers: []string{"[email protected]", "[email protected]"},
|
|
AllowedUserDomains: []string{"example.com", "trusted.org"},
|
|
AllowedRolesAndGroups: []string{"admin", "power-users", "developers"},
|
|
ExcludedURLs: append([]string{"/custom"}, "/public"),
|
|
EnablePKCE: true,
|
|
RateLimit: 100,
|
|
RefreshGracePeriodSeconds: 300,
|
|
CookieDomain: ".example.com",
|
|
Headers: []HeaderConfig{
|
|
{Name: "X-Auth-User", Value: "{{.Email}}"},
|
|
{Name: "X-Auth-Groups", Value: "{{.Groups}}"},
|
|
{Name: "X-Auth-Roles", Value: "{{.Roles}}"},
|
|
},
|
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
|
|
// Verify all fields are set
|
|
tests := []struct {
|
|
name string
|
|
got interface{}
|
|
expected interface{}
|
|
}{
|
|
{"ProviderURL", config.ProviderURL, "https://auth.example.com"},
|
|
{"ClientID", config.ClientID, "complex-client-id"},
|
|
{"ClientSecret", config.ClientSecret, "complex-client-secret"},
|
|
{"CallbackURL", config.CallbackURL, "/auth/callback"},
|
|
{"LogoutURL", config.LogoutURL, "/auth/logout"},
|
|
{"PostLogoutRedirectURI", config.PostLogoutRedirectURI, "https://example.com/goodbye"},
|
|
{"SessionEncryptionKey", config.SessionEncryptionKey, strings.Repeat("a", 32)},
|
|
{"ForceHTTPS", config.ForceHTTPS, true},
|
|
{"LogLevel", config.LogLevel, "DEBUG"},
|
|
{"OverrideScopes", config.OverrideScopes, true},
|
|
{"EnablePKCE", config.EnablePKCE, true},
|
|
{"RateLimit", config.RateLimit, 100},
|
|
{"RefreshGracePeriodSeconds", config.RefreshGracePeriodSeconds, 300},
|
|
{"CookieDomain", config.CookieDomain, ".example.com"},
|
|
{"Scopes length", len(config.Scopes), 5},
|
|
{"AllowedUsers length", len(config.AllowedUsers), 2},
|
|
{"AllowedUserDomains length", len(config.AllowedUserDomains), 2},
|
|
{"AllowedRolesAndGroups length", len(config.AllowedRolesAndGroups), 3},
|
|
{"ExcludedURLs length", len(config.ExcludedURLs), 2},
|
|
{"Headers length", len(config.Headers), 3},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if !reflect.DeepEqual(tt.got, tt.expected) {
|
|
t.Errorf("%s: got %v, expected %v", tt.name, tt.got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Verify HTTPClient
|
|
if config.HTTPClient == nil {
|
|
t.Error("HTTPClient should not be nil")
|
|
}
|
|
if config.HTTPClient.Timeout != 30*time.Second {
|
|
t.Error("HTTPClient timeout not set correctly")
|
|
}
|
|
}
|
|
|
|
func TestConfig_ValidationScenarios(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
expectValid bool
|
|
checkFunc func(*Config) error
|
|
}{
|
|
{
|
|
name: "Valid minimal config",
|
|
config: &Config{
|
|
ProviderURL: "https://provider.example.com",
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
SessionEncryptionKey: "encryption-key-32-bytes-for-aes",
|
|
},
|
|
expectValid: true,
|
|
checkFunc: func(c *Config) error {
|
|
if len(c.SessionEncryptionKey) < minEncryptionKeyLength {
|
|
return fmt.Errorf("encryption key too short")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "Config with empty provider URL",
|
|
config: &Config{
|
|
ProviderURL: "",
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
SessionEncryptionKey: "encryption-key-32",
|
|
},
|
|
expectValid: false,
|
|
checkFunc: func(c *Config) error {
|
|
if c.ProviderURL == "" {
|
|
return fmt.Errorf("provider URL is required")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "Config with short encryption key",
|
|
config: &Config{
|
|
ProviderURL: "https://provider.example.com",
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
SessionEncryptionKey: "short",
|
|
},
|
|
expectValid: false,
|
|
checkFunc: func(c *Config) error {
|
|
if len(c.SessionEncryptionKey) < minEncryptionKeyLength {
|
|
return fmt.Errorf("encryption key too short")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "Config with custom headers",
|
|
config: &Config{
|
|
ProviderURL: "https://provider.example.com",
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
SessionEncryptionKey: "encryption-key-32-bytes-for-aes",
|
|
Headers: []HeaderConfig{
|
|
{Name: "X-Custom", Value: "value"},
|
|
},
|
|
},
|
|
expectValid: true,
|
|
checkFunc: func(c *Config) error {
|
|
if len(c.Headers) == 0 {
|
|
return fmt.Errorf("expected headers to be set")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.checkFunc(tt.config)
|
|
if tt.expectValid && err != nil {
|
|
t.Errorf("Expected config to be valid, got error: %v", err)
|
|
}
|
|
if !tt.expectValid && err == nil {
|
|
t.Error("Expected config to be invalid, got no error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Concurrent Access Tests
|
|
// ============================================================================
|
|
|
|
func TestConfig_ConcurrentAccess(t *testing.T) {
|
|
config := CreateConfig()
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
|
|
// Test concurrent reads (safe)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
_ = config.LogLevel
|
|
_ = config.ForceHTTPS
|
|
_ = config.EnablePKCE
|
|
_ = config.Scopes
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Test concurrent writes with proper synchronization
|
|
var mu sync.Mutex
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
mu.Lock()
|
|
config.Headers = append(config.Headers, HeaderConfig{
|
|
Name: fmt.Sprintf("X-Header-%d", idx),
|
|
Value: fmt.Sprintf("value-%d", idx),
|
|
})
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Verify headers were added
|
|
if len(config.Headers) != numGoroutines {
|
|
t.Errorf("Expected %d headers, got %d", numGoroutines, len(config.Headers))
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Benchmark Tests
|
|
// ============================================================================
|
|
|
|
func BenchmarkCreateConfig(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_ = CreateConfig()
|
|
}
|
|
}
|
|
|
|
func BenchmarkNewSettings(b *testing.B) {
|
|
logger := NewMockLogger()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = NewSettings(logger)
|
|
}
|
|
}
|
|
|
|
func BenchmarkDeduplicateScopes(b *testing.B) {
|
|
scopes := []string{"openid", "profile", "email", "groups", "openid", "profile", "custom"}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = deduplicateScopes(scopes)
|
|
}
|
|
}
|
|
|
|
func BenchmarkCreateStringMap(b *testing.B) {
|
|
items := []string{"item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8"}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = createStringMap(items)
|
|
}
|
|
}
|
|
|
|
func BenchmarkCreateCaseInsensitiveStringMap(b *testing.B) {
|
|
items := []string{"Item1", "ITEM2", "item3", "Item4", "ITEM5", "item6", "Item7", "ITEM8"}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = createCaseInsensitiveStringMap(items)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func equalSlices(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i, v := range a {
|
|
if v != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|