mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
558 lines
15 KiB
Go
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
|
|
}
|