Files
Charon/backend/internal/caddy/manager_multicred_integration_test.go
GitHub Actions 032d475fba chore: remediate 61 Go linting issues and tighten pre-commit config
Complete lint remediation addressing errcheck, gosec, and staticcheck
violations across backend test files. Tighten pre-commit configuration
to prevent future blind spots.

Key Changes:
- Fix 61 Go linting issues (errcheck, gosec G115/G301/G304/G306, bodyclose)
- Add proper error handling for json.Unmarshal, os.Setenv, db.Close(), w.Write()
- Fix gosec G115 integer overflow with strconv.FormatUint
- Add #nosec annotations with justifications for test fixtures
- Fix SecurityService goroutine leaks (add Close() calls)
- Fix CrowdSec tar.gz non-deterministic ordering with sorted keys

Pre-commit Hardening:
- Remove test file exclusion from golangci-lint hook
- Add gosec to .golangci-fast.yml with critical checks (G101, G110, G305)
- Replace broad .golangci.yml exclusions with targeted path-specific rules
- Test files now linted on every commit

Test Fixes:
- Fix emergency route count assertions (1→2 for dual-port setup)
- Fix DNS provider service tests with proper mock setup
- Fix certificate service tests with deterministic behavior

Backend: 27 packages pass, 83.5% coverage
Frontend: 0 lint warnings, 0 TypeScript errors
Pre-commit: All 14 hooks pass (~37s)
2026-02-02 06:17:48 +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
}