Files
traefikoidc/config/config_consolidated_test.go
T

558 lines
15 KiB
Go

package config
import (
"testing"
"text/template"
"time"
)
// ============================================================================
// Config Creation Tests
// ============================================================================
func TestConfigCreation(t *testing.T) {
t.Run("CreateConfig_DefaultValues", func(t *testing.T) {
config := CreateConfig()
// 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 OverrideScopes default
if config.OverrideScopes {
t.Error("Expected OverrideScopes to be false by default")
}
})
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))
}
})
}
// ============================================================================
// Config Validation Tests - SKIPPED (no Validate method in real Config)
// ============================================================================
/*
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config *Config
expectedError string
}{
{
name: "Empty Config",
config: &Config{},
expectedError: "providerURL is required",
},
{
name: "Missing CallbackURL",
config: &Config{
ProviderURL: "https://provider.com",
},
expectedError: "callbackURL is required",
},
{
name: "Missing ClientID",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
},
expectedError: "clientID is required",
},
{
name: "Missing ClientSecret",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
},
expectedError: "clientSecret is required",
},
{
name: "Missing SessionEncryptionKey",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
},
expectedError: "sessionEncryptionKey is required",
},
{
name: "Short SessionEncryptionKey",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "short",
},
expectedError: "sessionEncryptionKey must be at least 32 characters",
},
{
name: "Invalid LogLevel",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "this-is-a-long-enough-encryption-key",
LogLevel: "invalid",
},
expectedError: "invalid log level: invalid (must be one of: debug, info, warn, error)",
},
{
name: "Invalid RateLimit",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "this-is-a-long-enough-encryption-key",
RateLimit: 0,
},
expectedError: "rateLimit must be greater than 0",
},
{
name: "Valid Config",
config: &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "this-is-a-long-enough-encryption-key",
RateLimit: 10,
},
expectedError: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.config.Validate()
if tc.expectedError == "" {
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
} else {
if err == nil {
t.Errorf("Expected error: %s, got nil", tc.expectedError)
} else if err.Error() != tc.expectedError {
t.Errorf("Expected error: %s, got: %s", tc.expectedError, err.Error())
}
}
})
}
}
*/
// ============================================================================
// Templated Header Config Tests
// ============================================================================
/*
func TestHeaderConfigValidation(t *testing.T) {
tests := []struct {
name string
header HeaderConfig
expectedError string
}{
{
name: "Empty Name",
header: HeaderConfig{Name: "", Value: "{{.Claims.email}}"},
expectedError: "header name cannot be empty",
},
{
name: "Empty Value",
header: HeaderConfig{Name: "X-Email", Value: ""},
expectedError: "header value template cannot be empty",
},
{
name: "Not a Template",
header: HeaderConfig{Name: "X-Email", Value: "static-value"},
expectedError: "header value 'static-value' does not appear to be a valid template (missing {{ }})",
},
{
name: "Lowercase claims",
header: HeaderConfig{Name: "X-Email", Value: "{{.claims.email}}"},
expectedError: "header template '{{.claims.email}}' appears to use lowercase 'claims' - use '{{.Claims...' instead (case sensitive)",
},
{
name: "Lowercase accessToken",
header: HeaderConfig{Name: "X-Token", Value: "Bearer {{.accessToken}}"},
expectedError: "header template 'Bearer {{.accessToken}}' appears to use lowercase 'accessToken' - use '{{.AccessToken...' instead (case sensitive)",
},
{
name: "Lowercase idToken",
header: HeaderConfig{Name: "X-Token", Value: "Bearer {{.idToken}}"},
expectedError: "header template 'Bearer {{.idToken}}' appears to use lowercase 'idToken' - use '{{.IdToken...' instead (case sensitive)",
},
{
name: "Lowercase refreshToken",
header: HeaderConfig{Name: "X-Refresh", Value: "Bearer {{.refreshToken}}"},
expectedError: "header template 'Bearer {{.refreshToken}}' appears to use lowercase 'refreshToken' - use '{{.RefreshToken...' instead (case sensitive)",
},
{
name: "Valid Template",
header: HeaderConfig{Name: "X-Email", Value: "{{.Claims.email}}"},
expectedError: "",
},
{
name: "Valid Bearer Token Template",
header: HeaderConfig{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
expectedError: "",
},
{
name: "Complex Valid Template",
header: HeaderConfig{Name: "X-User-Info", Value: "{{.Claims.sub}}-{{.Claims.email}}"},
expectedError: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
config := &Config{
ProviderURL: "https://provider.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "this-is-a-long-enough-encryption-key",
RateLimit: 10,
Headers: []HeaderConfig{tc.header},
}
err := config.Validate()
if tc.expectedError == "" {
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
} else {
if err == nil {
t.Errorf("Expected error: %s, got nil", tc.expectedError)
} else if err.Error() != tc.expectedError {
t.Errorf("Expected error: %s, got: %s", tc.expectedError, err.Error())
}
}
})
}
}
*/
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))
}
})
}
}
// ============================================================================
// Auth Config Tests
// ============================================================================
func TestAuthConfig(t *testing.T) {
// AuthURL field removed from Config - test skipped
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)
}
})
}
})
}
// ============================================================================
// 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
}