package caddy import ( "context" "fmt" "os" "strings" "testing" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" ) // TestExtractBaseDomain_EmptyInput verifies empty input returns empty string func TestExtractBaseDomain_EmptyInput(t *testing.T) { result := extractBaseDomain("") require.Equal(t, "", result) } // TestExtractBaseDomain_OnlyCommas verifies handling of comma-only input func TestExtractBaseDomain_OnlyCommas(t *testing.T) { // When input is only commas, first element is empty string after split result := extractBaseDomain(",,,") require.Equal(t, "", result) } // TestExtractBaseDomain_SingleDomain verifies single domain extraction func TestExtractBaseDomain_SingleDomain(t *testing.T) { result := extractBaseDomain("example.com") require.Equal(t, "example.com", result) } // TestExtractBaseDomain_WildcardDomain verifies wildcard stripping func TestExtractBaseDomain_WildcardDomain(t *testing.T) { result := extractBaseDomain("*.example.com") require.Equal(t, "example.com", result) } // TestExtractBaseDomain_MultipleDomains verifies first domain is used func TestExtractBaseDomain_MultipleDomains(t *testing.T) { result := extractBaseDomain("first.com, second.com, third.com") require.Equal(t, "first.com", result) } // TestExtractBaseDomain_MultipleDomainsWithWildcard verifies wildcard stripping with multiple domains func TestExtractBaseDomain_MultipleDomainsWithWildcard(t *testing.T) { result := extractBaseDomain("*.example.com, sub.example.com") require.Equal(t, "example.com", result) } // TestExtractBaseDomain_WithWhitespace verifies whitespace trimming func TestExtractBaseDomain_WithWhitespace(t *testing.T) { result := extractBaseDomain(" example.com ") require.Equal(t, "example.com", result) } // TestExtractBaseDomain_CaseNormalization verifies lowercase normalization func TestExtractBaseDomain_CaseNormalization(t *testing.T) { result := extractBaseDomain("EXAMPLE.COM") require.Equal(t, "example.com", result) } // TestExtractBaseDomain_Subdomain verifies subdomain handling func TestExtractBaseDomain_Subdomain(t *testing.T) { // Note: extractBaseDomain returns the first domain as-is (after wildcard removal) // It does NOT extract the registrable domain (like public suffix) result := extractBaseDomain("sub.example.com") require.Equal(t, "sub.example.com", result) } // TestExtractBaseDomain_MultiLevelSubdomain verifies multi-level subdomain handling func TestExtractBaseDomain_MultiLevelSubdomain(t *testing.T) { result := extractBaseDomain("deep.sub.example.com") require.Equal(t, "deep.sub.example.com", result) } // TestMatchesZoneFilter_EmptyFilter verifies empty filter returns false (catch-all handled separately) func TestMatchesZoneFilter_EmptyFilter(t *testing.T) { result := matchesZoneFilter("", "example.com", false) require.False(t, result) result = matchesZoneFilter(" ", "example.com", false) require.False(t, result) } // TestMatchesZoneFilter_EmptyZonesInList verifies empty zones in list are skipped func TestMatchesZoneFilter_EmptyZonesInList(t *testing.T) { // Empty zone entries should be skipped result := matchesZoneFilter(",example.com,", "example.com", false) require.True(t, result) result = matchesZoneFilter(",,,", "example.com", false) require.False(t, result) } // TestMatchesZoneFilter_ExactMatch verifies exact domain matching func TestMatchesZoneFilter_ExactMatch(t *testing.T) { result := matchesZoneFilter("example.com", "example.com", false) require.True(t, result) result = matchesZoneFilter("example.com", "other.com", false) require.False(t, result) } // TestMatchesZoneFilter_ExactMatchOnly verifies exact-only mode func TestMatchesZoneFilter_ExactMatchOnly(t *testing.T) { // With exactOnly=true, wildcard patterns should not match result := matchesZoneFilter("*.example.com", "sub.example.com", true) require.False(t, result) // But exact matches should still work result = matchesZoneFilter("sub.example.com", "sub.example.com", true) require.True(t, result) } // TestMatchesZoneFilter_WildcardMatch verifies wildcard pattern matching func TestMatchesZoneFilter_WildcardMatch(t *testing.T) { // Subdomain should match wildcard result := matchesZoneFilter("*.example.com", "sub.example.com", false) require.True(t, result) // Base domain should match wildcard result = matchesZoneFilter("*.example.com", "example.com", false) require.True(t, result) // Different domain should not match result = matchesZoneFilter("*.example.com", "other.com", false) require.False(t, result) } // TestMatchesZoneFilter_MultipleZones verifies comma-separated zone matching func TestMatchesZoneFilter_MultipleZones(t *testing.T) { result := matchesZoneFilter("example.com, other.com", "other.com", false) require.True(t, result) result = matchesZoneFilter("example.com, other.com", "third.com", false) require.False(t, result) } // TestMatchesZoneFilter_MultipleZonesWithWildcard verifies mixed zone list func TestMatchesZoneFilter_MultipleZonesWithWildcard(t *testing.T) { result := matchesZoneFilter("example.com, *.other.com", "sub.other.com", false) require.True(t, result) result2 := matchesZoneFilter("example.com, *.other.com", "example.com", false) require.True(t, result2) } // TestMatchesZoneFilter_WhitespaceTrimming verifies whitespace handling (different from manager_multicred_test) func TestMatchesZoneFilter_WhitespaceTrimming_Detailed(t *testing.T) { result := matchesZoneFilter(" example.com , other.com ", "example.com", false) require.True(t, result) } // TestMatchesZoneFilter_DeepSubdomain verifies deep subdomain matching func TestMatchesZoneFilter_DeepSubdomain(t *testing.T) { result := matchesZoneFilter("*.example.com", "deep.sub.example.com", false) require.True(t, result) } // TestGetCredentialForDomain_NoEncryptionKey verifies error when no encryption key func TestGetCredentialForDomain_NoEncryptionKey(t *testing.T) { // Save original env vars origKeys := map[string]string{} for _, key := range []string{"CHARON_ENCRYPTION_KEY", "ENCRYPTION_KEY", "CERBERUS_ENCRYPTION_KEY"} { origKeys[key] = os.Getenv(key) _ = os.Unsetenv(key) } defer func() { for key, val := range origKeys { if val != "" { os.Setenv(key, val) } } }() // Setup DB dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) manager := NewManager(nil, db, "", "", false, config.SecurityConfig{}) provider := &models.DNSProvider{ UseMultiCredentials: false, } _, err = manager.getCredentialForDomain(1, "example.com", provider) require.Error(t, err) require.Contains(t, err.Error(), "no encryption key available") } // TestGetCredentialForDomain_MultiCredential_NoMatch verifies error when no credential matches func TestGetCredentialForDomain_MultiCredential_NoMatch(t *testing.T) { // Save original env vars origKey := os.Getenv("CHARON_ENCRYPTION_KEY") os.Setenv("CHARON_ENCRYPTION_KEY", "test-key-32-characters-long!!!!!") defer func() { if origKey != "" { os.Setenv("CHARON_ENCRYPTION_KEY", origKey) } else { os.Unsetenv("CHARON_ENCRYPTION_KEY") } }() // Setup DB dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) manager := NewManager(nil, db, "", "", false, config.SecurityConfig{}) provider := &models.DNSProvider{ UseMultiCredentials: true, Credentials: []models.DNSProviderCredential{ { UUID: "cred-1", Label: "Zone1", ZoneFilter: "zone1.com", Enabled: true, }, { UUID: "cred-2", Label: "Zone2", ZoneFilter: "zone2.com", Enabled: true, }, }, } _, err = manager.getCredentialForDomain(1, "unmatched.com", provider) require.Error(t, err) require.Contains(t, err.Error(), "no matching credential found") } // TestGetCredentialForDomain_MultiCredential_DisabledSkipped verifies disabled credentials are skipped func TestGetCredentialForDomain_MultiCredential_DisabledSkipped(t *testing.T) { // Save original env vars origKey := os.Getenv("CHARON_ENCRYPTION_KEY") os.Setenv("CHARON_ENCRYPTION_KEY", "test-key-32-characters-long!!!!!") defer func() { if origKey != "" { os.Setenv("CHARON_ENCRYPTION_KEY", origKey) } else { os.Unsetenv("CHARON_ENCRYPTION_KEY") } }() // Setup DB dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) manager := NewManager(nil, db, "", "", false, config.SecurityConfig{}) provider := &models.DNSProvider{ UseMultiCredentials: true, Credentials: []models.DNSProviderCredential{ { UUID: "cred-1", Label: "Disabled Zone", ZoneFilter: "example.com", Enabled: false, // Disabled - should be skipped }, }, } // Should fail because the only matching credential is disabled _, err = manager.getCredentialForDomain(1, "example.com", provider) require.Error(t, err) require.Contains(t, err.Error(), "no matching credential found") } // TestGetCredentialForDomain_MultiCredential_CatchAllMatch verifies empty zone_filter as catch-all func TestGetCredentialForDomain_MultiCredential_CatchAllMatch(t *testing.T) { // Save original env vars origKey := os.Getenv("CHARON_ENCRYPTION_KEY") os.Setenv("CHARON_ENCRYPTION_KEY", "test-key-32-characters-long!!!!!") defer func() { if origKey != "" { os.Setenv("CHARON_ENCRYPTION_KEY", origKey) } else { os.Unsetenv("CHARON_ENCRYPTION_KEY") } }() // Setup DB dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) manager := NewManager(nil, db, "", "", false, config.SecurityConfig{}) provider := &models.DNSProvider{ UseMultiCredentials: true, Credentials: []models.DNSProviderCredential{ { UUID: "cred-catch-all", Label: "Catch-All", ZoneFilter: "", // Empty = catch-all Enabled: true, CredentialsEncrypted: "invalid-encrypted-data", // Will fail decryption }, }, } // Should match catch-all but fail on decryption or encryption key processing _, err = manager.getCredentialForDomain(1, "any-domain.com", provider) require.Error(t, err) // The error could be from encryptor creation or decryption require.True(t, strings.Contains(err.Error(), "failed to decrypt") || strings.Contains(err.Error(), "failed to create encryptor"), "expected encryption/decryption error, got: %s", err.Error()) } // TestComputeEffectiveFlags_DB_SecurityConfigWAFDisabled verifies WAF disabled in SecurityConfig func TestComputeEffectiveFlags_DB_SecurityConfigWAFDisabled(t *testing.T) { dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"} manager := NewManager(nil, db, "", "", false, secCfg) // Set WAF mode to disabled in DB res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "disabled"}) require.NoError(t, res.Error) _, _, waf, _, _ := manager.computeEffectiveFlags(context.Background()) require.False(t, waf) } // TestComputeEffectiveFlags_DB_RateLimitFromBooleanField verifies backward compat with boolean field func TestComputeEffectiveFlags_DB_RateLimitFromBooleanField(t *testing.T) { dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) secCfg := config.SecurityConfig{CerberusEnabled: true, RateLimitMode: ""} manager := NewManager(nil, db, "", "", false, secCfg) // Set rate limit via boolean field (backward compatibility) res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, RateLimitEnable: true, RateLimitMode: ""}) require.NoError(t, res.Error) _, _, _, rl, _ := manager.computeEffectiveFlags(context.Background()) require.True(t, rl) } // TestComputeEffectiveFlags_DB_CrowdSecModeFromSecurityConfig verifies CrowdSec mode from DB func TestComputeEffectiveFlags_DB_CrowdSecModeFromSecurityConfig(t *testing.T) { dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) secCfg := config.SecurityConfig{CerberusEnabled: true, CrowdSecMode: ""} manager := NewManager(nil, db, "", "", false, secCfg) // Set CrowdSec mode in SecurityConfig table res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, CrowdSecMode: "local"}) require.NoError(t, res.Error) _, _, _, _, cs := manager.computeEffectiveFlags(context.Background()) require.True(t, cs) } // TestComputeEffectiveFlags_DB_LegacyCerberusKey verifies legacy security.cerberus.enabled key func TestComputeEffectiveFlags_DB_LegacyCerberusKey(t *testing.T) { dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Setting{})) secCfg := config.SecurityConfig{CerberusEnabled: false} // Start with false manager := NewManager(nil, db, "", "", false, secCfg) // Set via legacy key res := db.Create(&models.Setting{Key: "security.cerberus.enabled", Value: "true"}) require.NoError(t, res.Error) cerb, _, _, _, _ := manager.computeEffectiveFlags(context.Background()) require.True(t, cerb) }