Files
Charon/backend/internal/caddy/manager_helpers_test.go
GitHub Actions d7939bed70 feat: add ManualDNSChallenge component and related hooks for manual DNS challenge management
- Implemented `useManualChallenge`, `useChallengePoll`, and `useManualChallengeMutations` hooks for managing manual DNS challenges.
- Created tests for the `useManualChallenge` hooks to ensure correct fetching and mutation behavior.
- Added `ManualDNSChallenge` component for displaying challenge details and actions.
- Developed end-to-end tests for the Manual DNS Provider feature, covering provider selection, challenge UI, and accessibility compliance.
- Included error handling tests for verification failures and network errors.
2026-01-12 04:01:40 +00:00

390 lines
14 KiB
Go

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)
}