mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
e64fc7f730
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
833 lines
20 KiB
Go
833 lines
20 KiB
Go
//go:build !yaegi
|
|
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestConfigLoader tests the config loader functionality
|
|
func TestConfigLoader(t *testing.T) {
|
|
loader := NewConfigLoader()
|
|
|
|
if loader == nil {
|
|
t.Fatal("NewConfigLoader should not return nil")
|
|
}
|
|
|
|
if loader.migrator == nil {
|
|
t.Error("ConfigLoader should have a migrator")
|
|
}
|
|
|
|
if loader.envPrefix != "TRAEFIKOIDC_" {
|
|
t.Errorf("Expected envPrefix to be 'TRAEFIKOIDC_', got %s", loader.envPrefix)
|
|
}
|
|
|
|
if len(loader.configPaths) == 0 {
|
|
t.Error("ConfigLoader should have default config paths")
|
|
}
|
|
}
|
|
|
|
// TestLoadFromEnv tests loading configuration from environment variables
|
|
func TestLoadFromEnv(t *testing.T) {
|
|
// Set up test environment variables
|
|
testEnvVars := map[string]string{
|
|
"TRAEFIKOIDC_PROVIDER_ISSUER_URL": "https://test.example.com",
|
|
"TRAEFIKOIDC_PROVIDER_CLIENT_ID": "test-client-id",
|
|
"TRAEFIKOIDC_PROVIDER_CLIENT_SECRET": "test-secret",
|
|
"TRAEFIKOIDC_SESSION_ENCRYPTION_KEY": "32-character-encryption-key-12345",
|
|
"TRAEFIKOIDC_SESSION_CHUNKED": "true",
|
|
"TRAEFIKOIDC_REDIS_ENABLED": "true",
|
|
"TRAEFIKOIDC_REDIS_ADDR": "redis.example.com:6379",
|
|
"TRAEFIKOIDC_SECURITY_FORCE_HTTPS": "true",
|
|
"TRAEFIKOIDC_CACHE_ENABLED": "true",
|
|
"TRAEFIKOIDC_CACHE_TYPE": "redis",
|
|
"TRAEFIKOIDC_RATELIMIT_ENABLED": "true",
|
|
"TRAEFIKOIDC_RATELIMIT_RPS": "100",
|
|
}
|
|
|
|
// Set environment variables
|
|
for key, value := range testEnvVars {
|
|
os.Setenv(key, value)
|
|
defer os.Unsetenv(key)
|
|
}
|
|
|
|
loader := NewConfigLoader()
|
|
config := &UnifiedConfig{}
|
|
loader.LoadFromEnv(config)
|
|
|
|
// Verify values were loaded
|
|
if config.Provider.IssuerURL != "https://test.example.com" {
|
|
t.Errorf("Expected IssuerURL to be 'https://test.example.com', got %s", config.Provider.IssuerURL)
|
|
}
|
|
if config.Provider.ClientID != "test-client-id" {
|
|
t.Errorf("Expected ClientID to be 'test-client-id', got %s", config.Provider.ClientID)
|
|
}
|
|
if config.Provider.ClientSecret != "test-secret" {
|
|
t.Errorf("Expected ClientSecret to be 'test-secret', got %s", config.Provider.ClientSecret)
|
|
}
|
|
if config.Session.EncryptionKey != "32-character-encryption-key-12345" {
|
|
t.Errorf("Expected EncryptionKey to be set, got %s", config.Session.EncryptionKey)
|
|
}
|
|
if !config.Security.ForceHTTPS {
|
|
t.Error("Expected ForceHTTPS to be true")
|
|
}
|
|
if !config.Cache.Enabled {
|
|
t.Error("Expected Cache to be enabled")
|
|
}
|
|
if config.Cache.Type != "redis" {
|
|
t.Errorf("Expected Cache.Type to be 'redis', got %s", config.Cache.Type)
|
|
}
|
|
if !config.RateLimit.Enabled {
|
|
t.Error("Expected RateLimit to be enabled")
|
|
}
|
|
if config.RateLimit.RequestsPerSecond != 100 {
|
|
t.Errorf("Expected RequestsPerSecond to be 100, got %d", config.RateLimit.RequestsPerSecond)
|
|
}
|
|
}
|
|
|
|
// TestSaveToFile tests saving configuration to files
|
|
func TestSaveToFile(t *testing.T) {
|
|
// Create a temporary directory for test files
|
|
tmpDir, err := os.MkdirTemp("", "config-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
loader := NewConfigLoader()
|
|
config := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "test-client",
|
|
ClientSecret: "secret",
|
|
},
|
|
Session: SessionConfig{
|
|
EncryptionKey: "32-character-encryption-key-12345",
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "save as JSON",
|
|
filename: "config.json",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "save as YAML",
|
|
filename: "config.yaml",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "save as YML",
|
|
filename: "config.yml",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unsupported extension",
|
|
filename: "config.txt",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "path traversal attempt",
|
|
filename: "../../../etc/config.json",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
filePath := filepath.Join(tmpDir, tt.filename)
|
|
err := loader.SaveToFile(config, filePath)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify file was created with correct permissions
|
|
info, err := os.Stat(filePath)
|
|
if err != nil {
|
|
t.Errorf("Failed to stat saved file: %v", err)
|
|
return
|
|
}
|
|
|
|
// Check file permissions (should be 0600)
|
|
mode := info.Mode().Perm()
|
|
if mode != 0600 {
|
|
t.Errorf("Expected file permissions 0600, got %o", mode)
|
|
}
|
|
|
|
// Verify content can be read back
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Errorf("Failed to read saved file: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify secrets are redacted
|
|
content := string(data)
|
|
if strings.Contains(content, "secret") && !strings.Contains(content, "[REDACTED]") {
|
|
t.Error("Secrets should be redacted in saved file")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoadFile tests loading configuration from files
|
|
func TestLoadFile(t *testing.T) {
|
|
// Create a temporary directory for test files
|
|
tmpDir, err := os.MkdirTemp("", "config-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Test data - using old config format since unified config is not enabled by default
|
|
jsonConfig := `{
|
|
"providerURL": "https://auth.example.com",
|
|
"clientID": "test-client",
|
|
"clientSecret": "secret",
|
|
"sessionEncryptionKey": "32-character-encryption-key-12345"
|
|
}`
|
|
|
|
yamlConfig := `
|
|
providerurl: https://auth.example.com
|
|
clientid: test-client
|
|
clientsecret: secret
|
|
sessionencryptionkey: 32-character-encryption-key-12345
|
|
`
|
|
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
content string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "load JSON config",
|
|
filename: "config.json",
|
|
content: jsonConfig,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "load YAML config",
|
|
filename: "config.yaml",
|
|
content: yamlConfig,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "path traversal attempt",
|
|
filename: "../../../etc/passwd",
|
|
content: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "non-existent file",
|
|
filename: "does-not-exist.json",
|
|
content: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
loader := NewConfigLoader()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var filePath string
|
|
if tt.content != "" {
|
|
filePath = filepath.Join(tmpDir, tt.filename)
|
|
err := os.WriteFile(filePath, []byte(tt.content), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test file: %v", err)
|
|
return
|
|
}
|
|
} else {
|
|
filePath = tt.filename
|
|
}
|
|
|
|
config, err := loader.loadFile(filePath)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
if !os.IsNotExist(err) && !strings.Contains(err.Error(), "no such file") {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Verify loaded config
|
|
if config == nil {
|
|
t.Error("Expected config to be loaded")
|
|
return
|
|
}
|
|
|
|
if config.Provider.IssuerURL != "https://auth.example.com" {
|
|
t.Errorf("Expected IssuerURL to be 'https://auth.example.com', got %s", config.Provider.IssuerURL)
|
|
}
|
|
if config.Provider.ClientID != "test-client" {
|
|
t.Errorf("Expected ClientID to be 'test-client', got %s", config.Provider.ClientID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ====================================================================================
|
|
// Tests for untested functions (0% coverage)
|
|
// ====================================================================================
|
|
|
|
// TestConfigLoader_Load tests the full Load pipeline
|
|
func TestConfigLoader_Load(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "config-load-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create a test config file
|
|
configPath := filepath.Join(tmpDir, "traefik-oidc.json")
|
|
configData := `{
|
|
"providerURL": "https://auth.example.com",
|
|
"clientID": "test-client",
|
|
"clientSecret": "test-secret",
|
|
"sessionEncryptionKey": "32-character-encryption-key-12345"
|
|
}`
|
|
err = os.WriteFile(configPath, []byte(configData), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config file: %v", err)
|
|
}
|
|
|
|
// Change to temp directory so loader can find the config
|
|
oldDir, _ := os.Getwd()
|
|
os.Chdir(tmpDir)
|
|
defer os.Chdir(oldDir)
|
|
|
|
// Set some environment variables to test merging
|
|
os.Setenv("TRAEFIKOIDC_SECURITY_FORCE_HTTPS", "true")
|
|
defer os.Unsetenv("TRAEFIKOIDC_SECURITY_FORCE_HTTPS")
|
|
|
|
loader := NewConfigLoader()
|
|
config, err := loader.Load()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Load() failed: %v", err)
|
|
}
|
|
|
|
if config == nil {
|
|
t.Fatal("Load() returned nil config")
|
|
}
|
|
|
|
// Verify file was loaded
|
|
if config.Provider.IssuerURL != "https://auth.example.com" {
|
|
t.Errorf("Expected IssuerURL from file, got %s", config.Provider.IssuerURL)
|
|
}
|
|
|
|
// Verify env vars were loaded
|
|
if !config.Security.ForceHTTPS {
|
|
t.Error("Expected ForceHTTPS from env var to be true")
|
|
}
|
|
}
|
|
|
|
// TestConfigLoader_LoadFromFile tests the LoadFromFile function
|
|
func TestConfigLoader_LoadFromFile(t *testing.T) {
|
|
t.Run("NoConfigFile", func(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "config-nofile-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
oldDir, _ := os.Getwd()
|
|
os.Chdir(tmpDir)
|
|
defer os.Chdir(oldDir)
|
|
|
|
loader := NewConfigLoader()
|
|
config, err := loader.LoadFromFile()
|
|
|
|
// Should not error when no config file found
|
|
if err != nil {
|
|
t.Errorf("LoadFromFile() should not error when no file found: %v", err)
|
|
}
|
|
|
|
// Should return nil config
|
|
if config != nil {
|
|
t.Error("LoadFromFile() should return nil config when no file found")
|
|
}
|
|
})
|
|
|
|
t.Run("LoadFromEnvPath", func(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "config-envpath-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create config file
|
|
configPath := filepath.Join(tmpDir, "custom-config.json")
|
|
configData := `{
|
|
"providerURL": "https://custom.example.com",
|
|
"clientID": "custom-client"
|
|
}`
|
|
err = os.WriteFile(configPath, []byte(configData), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
// Set env variable pointing to config
|
|
os.Setenv("TRAEFIKOIDC_CONFIG_FILE", configPath)
|
|
defer os.Unsetenv("TRAEFIKOIDC_CONFIG_FILE")
|
|
|
|
loader := NewConfigLoader()
|
|
config, err := loader.LoadFromFile()
|
|
|
|
if err != nil {
|
|
t.Fatalf("LoadFromFile() failed: %v", err)
|
|
}
|
|
|
|
if config == nil {
|
|
t.Fatal("LoadFromFile() returned nil config")
|
|
}
|
|
|
|
if config.Provider.IssuerURL != "https://custom.example.com" {
|
|
t.Errorf("Expected IssuerURL 'https://custom.example.com', got %s", config.Provider.IssuerURL)
|
|
}
|
|
})
|
|
|
|
t.Run("LoadWithProvidedPaths", func(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "config-provided-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create config file
|
|
configPath := filepath.Join(tmpDir, "specific.json")
|
|
configData := `{
|
|
"providerURL": "https://specific.example.com",
|
|
"clientID": "specific-client"
|
|
}`
|
|
err = os.WriteFile(configPath, []byte(configData), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
loader := NewConfigLoader()
|
|
config, err := loader.LoadFromFile(configPath)
|
|
|
|
if err != nil {
|
|
t.Fatalf("LoadFromFile() with path failed: %v", err)
|
|
}
|
|
|
|
if config == nil {
|
|
t.Fatal("LoadFromFile() returned nil config")
|
|
}
|
|
|
|
if config.Provider.IssuerURL != "https://specific.example.com" {
|
|
t.Errorf("Expected IssuerURL 'https://specific.example.com', got %s", config.Provider.IssuerURL)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestSplitAndTrim tests the splitAndTrim helper function
|
|
func TestSplitAndTrim(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "Simple comma-separated",
|
|
input: "a,b,c",
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "With spaces",
|
|
input: "a, b , c",
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "Empty strings filtered out",
|
|
input: "a,,b, ,c",
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "Leading and trailing spaces",
|
|
input: " a , b , c ",
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "Single value",
|
|
input: "single",
|
|
expected: []string{"single"},
|
|
},
|
|
{
|
|
name: "Empty string",
|
|
input: "",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "Only commas and spaces",
|
|
input: " , , , ",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "Complex real-world example",
|
|
input: "openid, profile, email, groups",
|
|
expected: []string{"openid", "profile", "email", "groups"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := splitAndTrim(tt.input)
|
|
|
|
if len(result) != len(tt.expected) {
|
|
t.Errorf("Expected %d items, got %d: %v", len(tt.expected), len(result), result)
|
|
return
|
|
}
|
|
|
|
for i, expected := range tt.expected {
|
|
if result[i] != expected {
|
|
t.Errorf("At index %d: expected %q, got %q", i, expected, result[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigLoader_MergeConfigs tests the mergeConfigs function
|
|
func TestConfigLoader_MergeConfigs(t *testing.T) {
|
|
loader := NewConfigLoader()
|
|
|
|
t.Run("MergeNilSource", func(t *testing.T) {
|
|
target := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://target.example.com",
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(target, nil)
|
|
|
|
if result != target {
|
|
t.Error("mergeConfigs should return target when source is nil")
|
|
}
|
|
})
|
|
|
|
t.Run("MergeNilTarget", func(t *testing.T) {
|
|
source := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://source.example.com",
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(nil, source)
|
|
|
|
if result != source {
|
|
t.Error("mergeConfigs should return source when target is nil")
|
|
}
|
|
})
|
|
|
|
t.Run("MergeSimpleFields", func(t *testing.T) {
|
|
target := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://target.example.com",
|
|
ClientID: "",
|
|
},
|
|
}
|
|
|
|
source := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://source.example.com",
|
|
ClientID: "source-client",
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(target, source)
|
|
|
|
if result.Provider.IssuerURL != "https://source.example.com" {
|
|
t.Errorf("Expected IssuerURL to be overridden, got %s", result.Provider.IssuerURL)
|
|
}
|
|
|
|
if result.Provider.ClientID != "source-client" {
|
|
t.Errorf("Expected ClientID to be set, got %s", result.Provider.ClientID)
|
|
}
|
|
})
|
|
|
|
t.Run("MergeSlices", func(t *testing.T) {
|
|
target := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
Scopes: []string{"openid", "profile"},
|
|
},
|
|
}
|
|
|
|
source := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
Scopes: []string{"email", "groups"},
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(target, source)
|
|
|
|
// Source slice should replace target slice
|
|
if len(result.Provider.Scopes) != 2 {
|
|
t.Errorf("Expected 2 scopes, got %d", len(result.Provider.Scopes))
|
|
}
|
|
|
|
if result.Provider.Scopes[0] != "email" {
|
|
t.Errorf("Expected first scope 'email', got %s", result.Provider.Scopes[0])
|
|
}
|
|
})
|
|
|
|
t.Run("MergeMaps", func(t *testing.T) {
|
|
target := &UnifiedConfig{
|
|
Middleware: MiddlewareConfig{
|
|
CustomHeaders: map[string]string{
|
|
"X-Target-Header": "target-value",
|
|
},
|
|
},
|
|
}
|
|
|
|
source := &UnifiedConfig{
|
|
Middleware: MiddlewareConfig{
|
|
CustomHeaders: map[string]string{
|
|
"X-Source-Header": "source-value",
|
|
"X-Target-Header": "overridden-value",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(target, source)
|
|
|
|
if len(result.Middleware.CustomHeaders) != 2 {
|
|
t.Errorf("Expected 2 headers, got %d", len(result.Middleware.CustomHeaders))
|
|
}
|
|
|
|
if result.Middleware.CustomHeaders["X-Target-Header"] != "overridden-value" {
|
|
t.Errorf("Expected X-Target-Header to be overridden")
|
|
}
|
|
|
|
if result.Middleware.CustomHeaders["X-Source-Header"] != "source-value" {
|
|
t.Errorf("Expected X-Source-Header to be added")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestConfigLoader_MergeStructs tests the mergeStructs function indirectly
|
|
func TestConfigLoader_MergeStructs(t *testing.T) {
|
|
loader := NewConfigLoader()
|
|
|
|
t.Run("NestedStructMerge", func(t *testing.T) {
|
|
target := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
IssuerURL: "https://target.example.com",
|
|
ClientID: "target-client",
|
|
},
|
|
Session: SessionConfig{
|
|
Name: "target-session",
|
|
MaxAge: 3600,
|
|
},
|
|
}
|
|
|
|
source := &UnifiedConfig{
|
|
Provider: ProviderConfig{
|
|
ClientID: "source-client",
|
|
ClientSecret: "source-secret",
|
|
},
|
|
Session: SessionConfig{
|
|
MaxAge: 7200,
|
|
},
|
|
}
|
|
|
|
result := loader.mergeConfigs(target, source)
|
|
|
|
// Provider.IssuerURL should remain (zero value in source)
|
|
if result.Provider.IssuerURL != "https://target.example.com" {
|
|
t.Errorf("Expected IssuerURL to remain, got %s", result.Provider.IssuerURL)
|
|
}
|
|
|
|
// Provider.ClientID should be overridden
|
|
if result.Provider.ClientID != "source-client" {
|
|
t.Errorf("Expected ClientID to be overridden, got %s", result.Provider.ClientID)
|
|
}
|
|
|
|
// Provider.ClientSecret should be added
|
|
if result.Provider.ClientSecret != "source-secret" {
|
|
t.Errorf("Expected ClientSecret to be added, got %s", result.Provider.ClientSecret)
|
|
}
|
|
|
|
// Session.Name should remain (zero value in source)
|
|
if result.Session.Name != "target-session" {
|
|
t.Errorf("Expected Session.Name to remain, got %s", result.Session.Name)
|
|
}
|
|
|
|
// Session.MaxAge should be overridden
|
|
if result.Session.MaxAge != 7200 {
|
|
t.Errorf("Expected Session.MaxAge to be overridden, got %d", result.Session.MaxAge)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIsZeroValue tests the isZeroValue helper function
|
|
func TestIsZeroValue(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Zero string",
|
|
value: "",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-zero string",
|
|
value: "hello",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Zero int",
|
|
value: 0,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-zero int",
|
|
value: 42,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Zero bool",
|
|
value: false,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-zero bool",
|
|
value: true,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Nil pointer",
|
|
value: (*string)(nil),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-nil pointer",
|
|
value: stringPtr("test"),
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Nil slice",
|
|
value: ([]string)(nil),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Empty slice",
|
|
value: []string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-empty slice",
|
|
value: []string{"a"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Nil map",
|
|
value: (map[string]string)(nil),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Empty map",
|
|
value: map[string]string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Non-empty map",
|
|
value: map[string]string{"key": "value"},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
v := reflect.ValueOf(tt.value)
|
|
result := isZeroValue(v)
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("Expected isZeroValue to be %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsZeroValue_Struct tests isZeroValue with struct types
|
|
func TestIsZeroValue_Struct(t *testing.T) {
|
|
type TestStruct struct {
|
|
Field1 string
|
|
Field2 int
|
|
}
|
|
|
|
t.Run("Zero struct", func(t *testing.T) {
|
|
s := TestStruct{}
|
|
v := reflect.ValueOf(s)
|
|
result := isZeroValue(v)
|
|
|
|
if !result {
|
|
t.Error("Expected zero struct to return true")
|
|
}
|
|
})
|
|
|
|
t.Run("Non-zero struct - Field1 set", func(t *testing.T) {
|
|
s := TestStruct{Field1: "test"}
|
|
v := reflect.ValueOf(s)
|
|
result := isZeroValue(v)
|
|
|
|
if result {
|
|
t.Error("Expected non-zero struct to return false")
|
|
}
|
|
})
|
|
|
|
t.Run("Non-zero struct - Field2 set", func(t *testing.T) {
|
|
s := TestStruct{Field2: 42}
|
|
v := reflect.ValueOf(s)
|
|
result := isZeroValue(v)
|
|
|
|
if result {
|
|
t.Error("Expected non-zero struct to return false")
|
|
}
|
|
})
|
|
|
|
t.Run("Non-zero struct - Both fields set", func(t *testing.T) {
|
|
s := TestStruct{Field1: "test", Field2: 42}
|
|
v := reflect.ValueOf(s)
|
|
result := isZeroValue(v)
|
|
|
|
if result {
|
|
t.Error("Expected non-zero struct to return false")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper function for pointer tests
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|