Files
Charon/backend/internal/caddy/manager_multicred_integration_test.go
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

428 lines
13 KiB
Go

package caddy
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/crypto"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Auto-register DNS providers
)
// encryptCredentials is a helper to encrypt credentials for test fixtures
func encryptCredentials(t *testing.T, credentials map[string]string) string {
t.Helper()
// Always use a valid 32-byte base64-encoded key (decodes to exactly 32 bytes)
// base64.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012"))
// = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
encryptionKey := "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", encryptionKey))
encryptor, err := crypto.NewEncryptionService(encryptionKey)
require.NoError(t, err)
credJSON, err := json.Marshal(credentials)
require.NoError(t, err)
encrypted, err := encryptor.Encrypt(credJSON)
require.NoError(t, err)
return encrypted
}
// setupTestDB creates an in-memory database for testing
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// Auto-migrate all models including related ones
err = db.AutoMigrate(
&models.ProxyHost{},
&models.Location{},
&models.DNSProvider{},
&models.DNSProviderCredential{},
&models.SSLCertificate{},
&models.Setting{},
&models.SecurityConfig{},
&models.AccessList{},
&models.SecurityHeaderProfile{},
)
require.NoError(t, err)
return db
}
// TestApplyConfig_SingleCredential_BackwardCompatibility tests that single-credential
// providers continue to work as before (backward compatibility)
func TestApplyConfig_SingleCredential_BackwardCompatibility(t *testing.T) {
db := setupTestDB(t)
// Create a single-credential provider
provider := models.DNSProvider{
ProviderType: "cloudflare",
UseMultiCredentials: false,
CredentialsEncrypted: encryptCredentials(t, map[string]string{
"api_token": "test-single-token",
}),
PropagationTimeout: 60,
Enabled: true,
}
require.NoError(t, db.Create(&provider).Error)
// Create a proxy host with wildcard domain
host := models.ProxyHost{
DomainNames: "*.example.com",
DNSProviderID: &provider.ID,
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
}
require.NoError(t, db.Create(&host).Error)
// Create ACME email setting
setting := models.Setting{
Key: "caddy.acme_email",
Value: "test@example.com",
}
require.NoError(t, db.Create(&setting).Error)
// Create manager with mock client
mockClient := &MockClient{}
manager := NewManager(mockClient, db, t.TempDir(), "", false, config.SecurityConfig{})
// Apply config
err := manager.ApplyConfig(context.Background())
require.NoError(t, err)
// Verify the generated config has DNS challenge with single credential
assert.True(t, mockClient.LoadCalled, "Load should have been called")
assert.NotNil(t, mockClient.LastLoadedConfig, "Config should have been loaded")
// Verify TLS automation policies exist
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS.Automation)
require.Greater(t, len(mockClient.LastLoadedConfig.Apps.TLS.Automation.Policies), 0)
// Find the DNS challenge policy
var dnsPolicy *AutomationPolicy
for _, policy := range mockClient.LastLoadedConfig.Apps.TLS.Automation.Policies {
if len(policy.Subjects) > 0 && policy.Subjects[0] == "*.example.com" {
dnsPolicy = policy
break
}
}
require.NotNil(t, dnsPolicy, "DNS challenge policy should exist for *.example.com")
// Verify it uses the single credential
require.Greater(t, len(dnsPolicy.IssuersRaw), 0)
issuer := dnsPolicy.IssuersRaw[0].(map[string]any)
require.NotNil(t, issuer["challenges"])
challenges := issuer["challenges"].(map[string]any)
require.NotNil(t, challenges["dns"])
dnsChallenge := challenges["dns"].(map[string]any)
require.NotNil(t, dnsChallenge["provider"])
providerConfig := dnsChallenge["provider"].(map[string]any)
assert.Equal(t, "cloudflare", providerConfig["name"])
assert.Equal(t, "test-single-token", providerConfig["api_token"])
}
// TestApplyConfig_MultiCredential_ExactMatch tests that multi-credential providers
// correctly match credentials by exact zone match
func TestApplyConfig_MultiCredential_ExactMatch(t *testing.T) {
db := setupTestDB(t)
// Create a multi-credential provider
provider := models.DNSProvider{
ProviderType: "cloudflare",
UseMultiCredentials: true,
PropagationTimeout: 60,
Enabled: true,
}
require.NoError(t, db.Create(&provider).Error)
// Create zone-specific credentials
exampleComCred := models.DNSProviderCredential{
UUID: uuid.New().String(),
DNSProviderID: provider.ID,
Label: "Example.com Credential",
ZoneFilter: "example.com",
CredentialsEncrypted: encryptCredentials(t, map[string]string{
"api_token": "token-example-com",
}),
Enabled: true,
}
require.NoError(t, db.Create(&exampleComCred).Error)
exampleOrgCred := models.DNSProviderCredential{
UUID: uuid.New().String(),
DNSProviderID: provider.ID,
Label: "Example.org Credential",
ZoneFilter: "example.org",
CredentialsEncrypted: encryptCredentials(t, map[string]string{
"api_token": "token-example-org",
}),
Enabled: true,
}
require.NoError(t, db.Create(&exampleOrgCred).Error)
// Create proxy hosts for different domains
hostCom := models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "*.example.com",
DNSProviderID: &provider.ID,
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
}
require.NoError(t, db.Create(&hostCom).Error)
hostOrg := models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "*.example.org",
DNSProviderID: &provider.ID,
ForwardHost: "localhost",
ForwardPort: 8081,
Enabled: true,
}
require.NoError(t, db.Create(&hostOrg).Error)
// Create ACME email setting
setting := models.Setting{
Key: "caddy.acme_email",
Value: "test@example.com",
}
require.NoError(t, db.Create(&setting).Error)
// Create manager with mock client
mockClient := &MockClient{}
manager := NewManager(mockClient, db, t.TempDir(), "", false, config.SecurityConfig{})
// Apply config
err := manager.ApplyConfig(context.Background())
require.NoError(t, err)
// Verify the generated config has separate DNS challenge policies
assert.True(t, mockClient.LoadCalled)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS.Automation)
policies := mockClient.LastLoadedConfig.Apps.TLS.Automation.Policies
require.Greater(t, len(policies), 1, "Should have separate policies for each domain")
// Find policies for each domain
var comPolicy, orgPolicy *AutomationPolicy
for _, policy := range policies {
if len(policy.Subjects) > 0 {
switch policy.Subjects[0] {
case "*.example.com":
comPolicy = policy
case "*.example.org":
orgPolicy = policy
}
}
}
require.NotNil(t, comPolicy, "Policy for *.example.com should exist")
require.NotNil(t, orgPolicy, "Policy for *.example.org should exist")
// Verify each policy uses the correct credential
assertDNSChallengeCredential(t, comPolicy, "cloudflare", "token-example-com")
assertDNSChallengeCredential(t, orgPolicy, "cloudflare", "token-example-org")
}
// TestApplyConfig_MultiCredential_WildcardMatch tests wildcard zone matching
func TestApplyConfig_MultiCredential_WildcardMatch(t *testing.T) {
db := setupTestDB(t)
// Create a multi-credential provider
provider := models.DNSProvider{
ProviderType: "cloudflare",
UseMultiCredentials: true,
PropagationTimeout: 60,
Enabled: true,
}
require.NoError(t, db.Create(&provider).Error)
// Create wildcard credential for *.example.com (matches app.example.com, api.example.com, etc.)
wildcardCred := models.DNSProviderCredential{
UUID: uuid.New().String(),
DNSProviderID: provider.ID,
Label: "Wildcard Example.com",
ZoneFilter: "*.example.com",
CredentialsEncrypted: encryptCredentials(t, map[string]string{
"api_token": "token-wildcard",
}),
Enabled: true,
}
require.NoError(t, db.Create(&wildcardCred).Error)
// Create proxy host for subdomain
host := models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "*.app.example.com",
DNSProviderID: &provider.ID,
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
}
require.NoError(t, db.Create(&host).Error)
// Create ACME email setting
setting := models.Setting{
Key: "caddy.acme_email",
Value: "test@example.com",
}
require.NoError(t, db.Create(&setting).Error)
// Create manager with mock client
mockClient := &MockClient{}
manager := NewManager(mockClient, db, t.TempDir(), "", false, config.SecurityConfig{})
// Apply config
err := manager.ApplyConfig(context.Background())
require.NoError(t, err)
// Verify config was generated
assert.True(t, mockClient.LoadCalled)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS.Automation)
// Find the DNS challenge policy
var dnsPolicy *AutomationPolicy
for _, policy := range mockClient.LastLoadedConfig.Apps.TLS.Automation.Policies {
if len(policy.Subjects) > 0 && policy.Subjects[0] == "*.app.example.com" {
dnsPolicy = policy
break
}
}
require.NotNil(t, dnsPolicy, "DNS challenge policy should exist")
// Verify it uses the wildcard credential
assertDNSChallengeCredential(t, dnsPolicy, "cloudflare", "token-wildcard")
}
// TestApplyConfig_MultiCredential_CatchAll tests catch-all credential (empty zone_filter)
func TestApplyConfig_MultiCredential_CatchAll(t *testing.T) {
db := setupTestDB(t)
// Create a multi-credential provider
provider := models.DNSProvider{
ProviderType: "cloudflare",
UseMultiCredentials: true,
PropagationTimeout: 60,
Enabled: true,
}
require.NoError(t, db.Create(&provider).Error)
// Create catch-all credential (empty zone_filter)
catchAllCred := models.DNSProviderCredential{
UUID: uuid.New().String(),
DNSProviderID: provider.ID,
Label: "Catch-All",
ZoneFilter: "",
CredentialsEncrypted: encryptCredentials(t, map[string]string{
"api_token": "token-catch-all",
}),
Enabled: true,
}
require.NoError(t, db.Create(&catchAllCred).Error)
// Create proxy host for a domain with no specific credential
host := models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "*.random.net",
DNSProviderID: &provider.ID,
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
}
require.NoError(t, db.Create(&host).Error)
// Create ACME email setting
setting := models.Setting{
Key: "caddy.acme_email",
Value: "test@example.com",
}
require.NoError(t, db.Create(&setting).Error)
// Create manager with mock client
mockClient := &MockClient{}
manager := NewManager(mockClient, db, t.TempDir(), "", false, config.SecurityConfig{})
// Apply config
err := manager.ApplyConfig(context.Background())
require.NoError(t, err)
// Verify config was generated
assert.True(t, mockClient.LoadCalled)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS)
require.NotNil(t, mockClient.LastLoadedConfig.Apps.TLS.Automation)
// Find the DNS challenge policy
var dnsPolicy *AutomationPolicy
for _, policy := range mockClient.LastLoadedConfig.Apps.TLS.Automation.Policies {
if len(policy.Subjects) > 0 && policy.Subjects[0] == "*.random.net" {
dnsPolicy = policy
break
}
}
require.NotNil(t, dnsPolicy, "DNS challenge policy should exist")
// Verify it uses the catch-all credential
assertDNSChallengeCredential(t, dnsPolicy, "cloudflare", "token-catch-all")
}
// assertDNSChallengeCredential is a helper to verify DNS challenge uses correct credentials
func assertDNSChallengeCredential(t *testing.T, policy *AutomationPolicy, providerType, expectedToken string) {
t.Helper()
require.Greater(t, len(policy.IssuersRaw), 0, "Policy should have issuers")
issuer := policy.IssuersRaw[0].(map[string]any)
require.NotNil(t, issuer["challenges"], "Issuer should have challenges")
challenges := issuer["challenges"].(map[string]any)
require.NotNil(t, challenges["dns"], "Challenges should have DNS")
dnsChallenge := challenges["dns"].(map[string]any)
require.NotNil(t, dnsChallenge["provider"], "DNS challenge should have provider")
providerConfig := dnsChallenge["provider"].(map[string]any)
assert.Equal(t, providerType, providerConfig["name"], "Provider type should match")
assert.Equal(t, expectedToken, providerConfig["api_token"], "API token should match")
}
// MockClient is a mock Caddy client for testing
type MockClient struct {
LoadCalled bool
LastLoadedConfig *Config
PingError error
LoadError error
GetConfigResult *Config
GetConfigError error
}
func (m *MockClient) Load(ctx context.Context, config *Config) error {
m.LoadCalled = true
m.LastLoadedConfig = config
return m.LoadError
}
func (m *MockClient) Ping(ctx context.Context) error {
return m.PingError
}
func (m *MockClient) GetConfig(ctx context.Context) (*Config, error) {
return m.GetConfigResult, m.GetConfigError
}