fixup! fixup! ... and another all nighter.

This commit is contained in:
2025-10-22 16:16:54 +01:00
parent ae6c315824
commit 26b592f547
25 changed files with 11426 additions and 17 deletions
+363
View File
@@ -0,0 +1,363 @@
//go:build !yaegi
package config
import (
"testing"
"github.com/lukaszraczylo/traefikoidc/internal/features"
)
// NewLegacyAdapter Tests
func TestNewLegacyAdapter(t *testing.T) {
unified := NewUnifiedConfig()
unified.Provider.IssuerURL = "https://provider.example.com"
unified.Provider.ClientID = "test-client"
unified.Provider.ClientSecret = "test-secret"
adapter := NewLegacyAdapter(unified)
if adapter == nil {
t.Fatal("Expected NewLegacyAdapter to return non-nil")
}
if adapter.unified != unified {
t.Error("Expected adapter to reference the unified config")
}
if adapter.adapter == nil {
t.Error("Expected internal adapter to be initialized")
}
}
// ToOldConfig Tests
func TestLegacyAdapter_ToOldConfig(t *testing.T) {
unified := NewUnifiedConfig()
unified.Provider.IssuerURL = "https://issuer.example.com"
unified.Provider.ClientID = "client-123"
unified.Provider.ClientSecret = "secret-456"
unified.Provider.RedirectURL = "https://app.example.com/callback"
unified.Provider.LogoutURL = "/logout"
unified.Provider.PostLogoutRedirectURI = "https://app.example.com"
unified.Provider.Scopes = []string{"openid", "profile"}
unified.Provider.OverrideScopes = true
unified.Session.EncryptionKey = "test-encryption-key-32-chars!!"
unified.Session.Domain = "example.com"
unified.Security.ForceHTTPS = true
unified.Security.EnablePKCE = true
unified.Security.AllowedUsers = []string{"[email protected]"}
unified.Security.AllowedUserDomains = []string{"example.com"}
unified.Security.AllowedRolesAndGroups = []string{"admin"}
unified.Security.ExcludedURLs = []string{"/health"}
unified.RateLimit.RequestsPerSecond = 100
unified.Logging.Level = "debug"
unified.Middleware.CustomHeaders = map[string]string{
"X-Header-1": "value1",
"X-Header-2": "value2",
}
adapter := NewLegacyAdapter(unified)
oldConfig := adapter.ToOldConfig()
if oldConfig == nil {
t.Fatal("Expected ToOldConfig to return non-nil")
}
// ToOldConfig behavior depends on feature flag
if !features.IsUnifiedConfigEnabled() {
// When feature is disabled, returns default config
if oldConfig.ProviderURL == "" {
t.Log("Feature flag disabled - ToOldConfig returns default config")
}
return
}
// When feature is enabled, verify all fields were correctly mapped
if oldConfig.ProviderURL != unified.Provider.IssuerURL {
t.Errorf("Expected ProviderURL '%s', got '%s'", unified.Provider.IssuerURL, oldConfig.ProviderURL)
}
if oldConfig.ClientID != unified.Provider.ClientID {
t.Errorf("Expected ClientID '%s', got '%s'", unified.Provider.ClientID, oldConfig.ClientID)
}
if oldConfig.ClientSecret != unified.Provider.ClientSecret {
t.Errorf("Expected ClientSecret '%s', got '%s'", unified.Provider.ClientSecret, oldConfig.ClientSecret)
}
if oldConfig.CallbackURL != unified.Provider.RedirectURL {
t.Error("Expected CallbackURL to match RedirectURL")
}
if oldConfig.LogoutURL != unified.Provider.LogoutURL {
t.Error("Expected LogoutURL to match")
}
if oldConfig.ForceHTTPS != unified.Security.ForceHTTPS {
t.Error("Expected ForceHTTPS to match")
}
if oldConfig.EnablePKCE != unified.Security.EnablePKCE {
t.Error("Expected EnablePKCE to match")
}
if oldConfig.RateLimit != unified.RateLimit.RequestsPerSecond {
t.Errorf("Expected RateLimit %d, got %d", unified.RateLimit.RequestsPerSecond, oldConfig.RateLimit)
}
if len(oldConfig.Headers) != 2 {
t.Errorf("Expected 2 headers, got %d", len(oldConfig.Headers))
}
}
// convertHeaders Tests
func TestLegacyAdapter_convertHeaders(t *testing.T) {
unified := NewUnifiedConfig()
unified.Middleware.CustomHeaders = map[string]string{
"X-Custom-Header-1": "value1",
"X-Custom-Header-2": "value2",
"X-Custom-Header-3": "value3",
}
adapter := NewLegacyAdapter(unified)
headers := adapter.convertHeaders()
if len(headers) != 3 {
t.Errorf("Expected 3 headers, got %d", len(headers))
}
// Check that headers were converted
headerMap := make(map[string]string)
for _, h := range headers {
headerMap[h.Name] = h.Value
}
if headerMap["X-Custom-Header-1"] != "value1" {
t.Error("Expected X-Custom-Header-1 to have value 'value1'")
}
if headerMap["X-Custom-Header-2"] != "value2" {
t.Error("Expected X-Custom-Header-2 to have value 'value2'")
}
}
func TestLegacyAdapter_convertHeaders_Empty(t *testing.T) {
unified := NewUnifiedConfig()
// No custom headers
adapter := NewLegacyAdapter(unified)
headers := adapter.convertHeaders()
if len(headers) != 0 {
t.Errorf("Expected 0 headers, got %d", len(headers))
}
}
// GetConfigInterface Tests
func TestGetConfigInterface(t *testing.T) {
cfg := GetConfigInterface()
if cfg == nil {
t.Fatal("Expected GetConfigInterface to return non-nil")
}
// Should return either UnifiedConfig or Config depending on feature flag
_, isUnified := cfg.(*UnifiedConfig)
_, isOld := cfg.(*Config)
if !isUnified && !isOld {
t.Error("Expected either *UnifiedConfig or *Config")
}
// Verify consistency with feature flag
if features.IsUnifiedConfigEnabled() {
if !isUnified {
t.Error("Expected *UnifiedConfig when unified config is enabled")
}
} else {
if !isOld {
t.Error("Expected *Config when unified config is disabled")
}
}
}
// ValidateConfig Tests
func TestValidateConfig_UnifiedConfig(t *testing.T) {
unified := NewUnifiedConfig()
unified.Provider.IssuerURL = "https://provider.example.com"
unified.Provider.ClientID = "client-id"
unified.Provider.ClientSecret = "client-secret"
unified.Session.EncryptionKey = "encryption-key-32-characters!!"
err := ValidateConfig(unified)
// Should succeed regardless of feature flag since we're passing the right type
if err != nil {
t.Errorf("Expected valid unified config to pass validation, got: %v", err)
}
}
func TestValidateConfig_OldConfig(t *testing.T) {
old := CreateConfig()
old.ProviderURL = "https://provider.example.com"
old.ClientID = "client-id"
old.ClientSecret = "client-secret"
old.SessionEncryptionKey = "encryption-key-32-characters!!"
err := ValidateConfig(old)
if err != nil {
t.Errorf("Expected valid old config to pass validation, got: %v", err)
}
}
func TestValidateConfig_InvalidType(t *testing.T) {
// Pass something that's not a config
err := ValidateConfig("not a config")
if err != nil {
t.Errorf("Expected nil for unknown type, got: %v", err)
}
}
// Config.Validate Tests
func TestConfig_Validate_Valid(t *testing.T) {
cfg := CreateConfig()
cfg.ProviderURL = "https://provider.example.com"
cfg.ClientID = "client-id"
cfg.ClientSecret = "client-secret"
cfg.SessionEncryptionKey = "encryption-key-32-characters!!"
err := cfg.Validate()
if err != nil {
t.Errorf("Expected valid config to pass, got: %v", err)
}
}
func TestConfig_Validate_MissingProviderURL(t *testing.T) {
cfg := CreateConfig()
cfg.ClientID = "client-id"
cfg.ClientSecret = "client-secret"
err := cfg.Validate()
if err == nil {
t.Error("Expected error for missing ProviderURL")
}
// Check if it's a ValidationErrors type
if verrs, ok := err.(ValidationErrors); ok {
found := false
for _, verr := range verrs {
if verr.Field == "ProviderURL" {
found = true
break
}
}
if !found {
t.Error("Expected ProviderURL validation error")
}
}
}
func TestConfig_Validate_MissingClientID(t *testing.T) {
cfg := CreateConfig()
cfg.ProviderURL = "https://provider.example.com"
cfg.ClientSecret = "client-secret"
err := cfg.Validate()
if err == nil {
t.Error("Expected error for missing ClientID")
}
if verrs, ok := err.(ValidationErrors); ok {
found := false
for _, verr := range verrs {
if verr.Field == "ClientID" {
found = true
break
}
}
if !found {
t.Error("Expected ClientID validation error")
}
}
}
func TestConfig_Validate_MissingClientSecret_NoPKCE(t *testing.T) {
cfg := CreateConfig()
cfg.ProviderURL = "https://provider.example.com"
cfg.ClientID = "client-id"
cfg.EnablePKCE = false
err := cfg.Validate()
if err == nil {
t.Error("Expected error for missing ClientSecret without PKCE")
}
if verrs, ok := err.(ValidationErrors); ok {
found := false
for _, verr := range verrs {
if verr.Field == "ClientSecret" {
found = true
break
}
}
if !found {
t.Error("Expected ClientSecret validation error")
}
}
}
func TestConfig_Validate_MissingClientSecret_WithPKCE(t *testing.T) {
cfg := CreateConfig()
cfg.ProviderURL = "https://provider.example.com"
cfg.ClientID = "client-id"
cfg.EnablePKCE = true // PKCE enabled, so ClientSecret not required
err := cfg.Validate()
if err != nil {
t.Errorf("Expected no error with PKCE enabled and no ClientSecret, got: %v", err)
}
}
func TestConfig_Validate_ShortEncryptionKey(t *testing.T) {
cfg := CreateConfig()
cfg.ProviderURL = "https://provider.example.com"
cfg.ClientID = "client-id"
cfg.ClientSecret = "client-secret"
cfg.SessionEncryptionKey = "short" // Too short
err := cfg.Validate()
if err == nil {
t.Error("Expected error for short encryption key")
}
if verrs, ok := err.(ValidationErrors); ok {
found := false
for _, verr := range verrs {
if verr.Field == "SessionEncryptionKey" {
found = true
break
}
}
if !found {
t.Error("Expected SessionEncryptionKey validation error")
}
}
}
func TestConfig_Validate_MultipleErrors(t *testing.T) {
cfg := CreateConfig()
// Missing ProviderURL, ClientID, and ClientSecret
err := cfg.Validate()
if err == nil {
t.Fatal("Expected validation errors")
}
verrs, ok := err.(ValidationErrors)
if !ok {
t.Fatal("Expected ValidationErrors type")
}
if len(verrs) < 2 {
t.Errorf("Expected at least 2 validation errors, got %d", len(verrs))
}
}
+540
View File
@@ -5,6 +5,7 @@ package config
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
@@ -290,3 +291,542 @@ sessionencryptionkey: 32-character-encryption-key-12345
})
}
}
// ====================================================================================
// 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
}
File diff suppressed because it is too large Load Diff
+248
View File
@@ -6,6 +6,9 @@ import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestValidateUnifiedConfig tests the validation of UnifiedConfig
@@ -338,3 +341,248 @@ func TestValidateRedisConfig(t *testing.T) {
})
}
}
// ============================================================================
// validateRateLimit Tests
// ============================================================================
func TestValidateRateLimit_Disabled(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = false
errors := config.validateRateLimit()
assert.Empty(t, errors, "Should have no errors when rate limiting is disabled")
}
func TestValidateRateLimit_ValidConfig(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 100
config.RateLimit.Burst = 200
config.RateLimit.KeyType = "ip"
errors := config.validateRateLimit()
assert.Empty(t, errors, "Should have no errors for valid rate limit config")
}
func TestValidateRateLimit_RequestsPerSecondTooLow(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 0
config.RateLimit.Burst = 100
config.RateLimit.KeyType = "ip"
errors := config.validateRateLimit()
require.Len(t, errors, 1)
assert.Equal(t, "RateLimit.RequestsPerSecond", errors[0].Field)
assert.Contains(t, errors[0].Message, "between 1 and 10000")
}
func TestValidateRateLimit_RequestsPerSecondTooHigh(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 15000
config.RateLimit.Burst = 20000
config.RateLimit.KeyType = "ip"
errors := config.validateRateLimit()
require.Len(t, errors, 1)
assert.Equal(t, "RateLimit.RequestsPerSecond", errors[0].Field)
assert.Contains(t, errors[0].Message, "between 1 and 10000")
}
func TestValidateRateLimit_BurstTooSmall(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 100
config.RateLimit.Burst = 50 // Less than RequestsPerSecond
config.RateLimit.KeyType = "ip"
errors := config.validateRateLimit()
require.Len(t, errors, 1)
assert.Equal(t, "RateLimit.Burst", errors[0].Field)
assert.Contains(t, errors[0].Message, "at least as large as requests per second")
}
func TestValidateRateLimit_InvalidKeyType(t *testing.T) {
tests := []struct {
name string
keyType string
}{
{"empty key type", ""},
{"invalid key type", "invalid"},
{"random string", "foobar"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 100
config.RateLimit.Burst = 200
config.RateLimit.KeyType = tt.keyType
errors := config.validateRateLimit()
require.Len(t, errors, 1)
assert.Equal(t, "RateLimit.KeyType", errors[0].Field)
assert.Contains(t, errors[0].Message, "invalid key type")
})
}
}
func TestValidateRateLimit_ValidKeyTypes(t *testing.T) {
validKeyTypes := []string{"ip", "user", "token", "custom"}
for _, keyType := range validKeyTypes {
t.Run(keyType, func(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 100
config.RateLimit.Burst = 200
config.RateLimit.KeyType = keyType
errors := config.validateRateLimit()
assert.Empty(t, errors, "Should have no errors for valid key type: %s", keyType)
})
}
}
func TestValidateRateLimit_MultipleErrors(t *testing.T) {
config := NewUnifiedConfig()
config.RateLimit.Enabled = true
config.RateLimit.RequestsPerSecond = 0 // Too low
config.RateLimit.Burst = 50 // Will pass (0 < 50)
config.RateLimit.KeyType = "invalid" // Invalid
errors := config.validateRateLimit()
// Should have 2 errors (rps and keyType)
assert.Len(t, errors, 2)
// Check each error is present
fields := make(map[string]bool)
for _, err := range errors {
fields[err.Field] = true
}
assert.True(t, fields["RateLimit.RequestsPerSecond"])
assert.True(t, fields["RateLimit.KeyType"])
}
// ============================================================================
// validateMetrics Tests
// ============================================================================
func TestValidateMetrics_Disabled(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = false
errors := config.validateMetrics()
assert.Empty(t, errors, "Should have no errors when metrics are disabled")
}
func TestValidateMetrics_ValidPrometheus(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "prometheus"
config.Metrics.Endpoint = "" // Prometheus doesn't require endpoint
errors := config.validateMetrics()
assert.Empty(t, errors, "Should have no errors for valid prometheus config")
}
func TestValidateMetrics_ValidStatsd(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "statsd"
config.Metrics.Endpoint = "localhost:8125"
errors := config.validateMetrics()
assert.Empty(t, errors, "Should have no errors for valid statsd config")
}
func TestValidateMetrics_ValidOTLP(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "otlp"
config.Metrics.Endpoint = "localhost:4317"
errors := config.validateMetrics()
assert.Empty(t, errors, "Should have no errors for valid otlp config")
}
func TestValidateMetrics_InvalidProvider(t *testing.T) {
tests := []struct {
name string
provider string
}{
{"empty provider", ""},
{"invalid provider", "invalid"},
{"datadog", "datadog"},
{"influx", "influx"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = tt.provider
config.Metrics.Endpoint = "localhost:8080"
errors := config.validateMetrics()
require.Len(t, errors, 1)
assert.Equal(t, "Metrics.Provider", errors[0].Field)
assert.Contains(t, errors[0].Message, "invalid metrics provider")
})
}
}
func TestValidateMetrics_StatsdMissingEndpoint(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "statsd"
config.Metrics.Endpoint = "" // Missing required endpoint
errors := config.validateMetrics()
require.Len(t, errors, 1)
assert.Equal(t, "Metrics.Endpoint", errors[0].Field)
assert.Contains(t, errors[0].Message, "endpoint is required for statsd provider")
}
func TestValidateMetrics_OTLPMissingEndpoint(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "otlp"
config.Metrics.Endpoint = "" // Missing required endpoint
errors := config.validateMetrics()
require.Len(t, errors, 1)
assert.Equal(t, "Metrics.Endpoint", errors[0].Field)
assert.Contains(t, errors[0].Message, "endpoint is required for otlp provider")
}
func TestValidateMetrics_MultipleErrors(t *testing.T) {
config := NewUnifiedConfig()
config.Metrics.Enabled = true
config.Metrics.Provider = "invalid" // Invalid provider
config.Metrics.Endpoint = "" // Would be missing if provider was statsd/otlp
errors := config.validateMetrics()
// Should have at least 1 error for invalid provider
assert.NotEmpty(t, errors)
assert.Equal(t, "Metrics.Provider", errors[0].Field)
}