Files
Charon/backend/internal/caddy/manager_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

536 lines
18 KiB
Go

package caddy
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestManager_ApplyConfig(t *testing.T) {
// Mock Caddy Admin API
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/load" && r.Method == "POST" {
// Verify payload
var config Config
err := json.NewDecoder(r.Body).Decode(&config)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer caddyServer.Close()
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager
tmpDir := t.TempDir()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Create a host
host := models.ProxyHost{
DomainNames: "example.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8080,
}
db.Create(&host)
// Apply Config
err = manager.ApplyConfig(context.Background())
assert.NoError(t, err)
// Verify config was saved to DB
var caddyConfig models.CaddyConfig
err = db.First(&caddyConfig).Error
assert.NoError(t, err)
assert.True(t, caddyConfig.Success)
}
func TestManager_ApplyConfig_Failure(t *testing.T) {
// Mock Caddy Admin API to fail
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer caddyServer.Close()
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager
tmpDir := t.TempDir()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Create a host
host := models.ProxyHost{
DomainNames: "example.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8080,
}
require.NoError(t, db.Create(&host).Error)
// Apply Config - should fail
err = manager.ApplyConfig(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "apply failed")
// Verify failure was recorded
var caddyConfig models.CaddyConfig
err = db.First(&caddyConfig).Error
assert.NoError(t, err)
assert.False(t, caddyConfig.Success)
assert.NotEmpty(t, caddyConfig.ErrorMsg)
}
func TestManager_Ping(t *testing.T) {
// Mock Caddy Admin API
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/config/" && r.Method == "GET" {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer caddyServer.Close()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, nil, "", "", false, config.SecurityConfig{})
err := manager.Ping(context.Background())
assert.NoError(t, err)
}
func TestManager_GetCurrentConfig(t *testing.T) {
// Mock Caddy Admin API
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/config/" && r.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"apps": {"http": {}}}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer caddyServer.Close()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, nil, "", "", false, config.SecurityConfig{})
cfg, err := manager.GetCurrentConfig(context.Background())
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.NotNil(t, cfg.Apps)
assert.NotNil(t, cfg.Apps.HTTP)
}
func TestManager_RotateSnapshots(t *testing.T) {
// Setup Manager
tmpDir := t.TempDir()
// Mock Caddy Admin API (Success)
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer caddyServer.Close()
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.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Create 15 dummy config files
for i := 0; i < 15; i++ {
// Use past timestamps
ts := time.Now().Add(-time.Duration(i+1) * time.Minute).Unix()
fname := fmt.Sprintf("config-%d.json", ts)
f, _ := os.Create(filepath.Join(tmpDir, fname)) // #nosec G304 -- Test creates files in temp dir
_ = f.Close()
}
// Call ApplyConfig once
err = manager.ApplyConfig(context.Background())
assert.NoError(t, err)
// Check number of files
files, _ := os.ReadDir(tmpDir)
// Count files matching config-*.json
count := 0
for _, f := range files {
if filepath.Ext(f.Name()) == ".json" {
count++
}
}
// Should be 10 (kept)
assert.Equal(t, 10, count)
}
func TestManager_Rollback_Success(t *testing.T) {
// Mock Caddy Admin API
// First call succeeds (initial setup), second call fails (bad config), third call succeeds (rollback)
callCount := 0
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
if r.URL.Path == "/load" && r.Method == "POST" {
if callCount == 2 {
w.WriteHeader(http.StatusInternalServerError) // Fail the second apply
return
}
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer caddyServer.Close()
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager
tmpDir := t.TempDir()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// 1. Apply valid config (creates snapshot)
host1 := models.ProxyHost{
UUID: "uuid-1",
DomainNames: "example.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8080,
}
db.Create(&host1)
err = manager.ApplyConfig(context.Background())
assert.NoError(t, err)
// Verify snapshot exists
snapshots, _ := manager.listSnapshots()
assert.Len(t, snapshots, 1)
// Sleep to ensure different timestamp for next snapshot
time.Sleep(1100 * time.Millisecond)
// 2. Apply another config (will fail at Caddy level)
host2 := models.ProxyHost{
UUID: "uuid-2",
DomainNames: "fail.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8081,
}
db.Create(&host2)
// This should fail, trigger rollback, and succeed in rolling back
err = manager.ApplyConfig(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "apply failed (rolled back)")
// Verify we still have 1 snapshot (the failed one was removed)
snapshots, _ = manager.listSnapshots()
assert.Len(t, snapshots, 1)
}
func TestManager_ApplyConfig_DBError(t *testing.T) {
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager
tmpDir := t.TempDir()
client := NewClient("http://localhost")
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Close DB to force error
sqlDB, _ := db.DB()
_ = sqlDB.Close()
err = manager.ApplyConfig(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "fetch proxy hosts")
}
func TestManager_ApplyConfig_ValidationError(t *testing.T) {
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager with a file as configDir to force saveSnapshot error
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config-file")
_ = os.WriteFile(configDir, []byte("not a dir"), 0o600) // #nosec G306 -- test fixture
client := NewClient("http://localhost")
manager := NewManager(client, db, configDir, "", false, config.SecurityConfig{})
host := models.ProxyHost{
DomainNames: "example.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8080,
}
db.Create(&host)
err = manager.ApplyConfig(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "save snapshot")
}
func TestManager_Rollback_Failure(t *testing.T) {
// Mock Caddy Admin API - Always Fail
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer caddyServer.Close()
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
// Setup Manager
tmpDir := t.TempDir()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Create a dummy snapshot manually so rollback has something to try
_ = os.WriteFile(filepath.Join(tmpDir, "config-123.json"), []byte("{}"), 0o600) // #nosec G306 -- test fixture
// Apply Config - will fail, try rollback, rollback will fail
err = manager.ApplyConfig(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "rollback also failed")
}
func TestComputeEffectiveFlags_DefaultsNoDB(t *testing.T) {
// No DB - rely on SecurityConfig defaults only
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
manager := NewManager(nil, nil, "", "", false, secCfg)
cerb, acl, waf, rl, cs := manager.computeEffectiveFlags(context.Background())
require.True(t, cerb)
require.True(t, acl)
require.True(t, waf)
require.True(t, rl)
require.True(t, cs)
// If Cerberus disabled, all subcomponents must be disabled
secCfg.CerberusEnabled = false
manager = NewManager(nil, nil, "", "", false, secCfg)
cerb, acl, waf, rl, cs = manager.computeEffectiveFlags(context.Background())
require.False(t, cerb)
require.False(t, acl)
require.False(t, waf)
require.False(t, rl)
require.False(t, cs)
// Unknown/unrecognized CrowdSec mode should disable CrowdSec in computed flags
secCfg = config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "unknown"}
manager = NewManager(nil, nil, "", "", false, secCfg)
cerb, acl, waf, rl, cs = manager.computeEffectiveFlags(context.Background())
require.True(t, cerb)
require.True(t, acl)
require.True(t, waf)
require.True(t, rl)
require.False(t, cs)
}
// Removed combined DB overrides test - replaced by smaller, focused DB tests
func TestComputeEffectiveFlags_DB_CerberusDisabled(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: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
manager := NewManager(nil, db, "", "", false, secCfg)
// Set runtime override to disable cerberus
res := db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "false"})
require.NoError(t, res.Error)
cerb, acl, waf, rl, cs := manager.computeEffectiveFlags(context.Background())
require.False(t, cerb)
require.False(t, acl)
require.False(t, waf)
require.False(t, rl)
require.False(t, cs)
}
// TestComputeEffectiveFlags_DB_ACLDisables: replaced by TestComputeEffectiveFlags_DB_ACLTrueAndFalse
// TestComputeEffectiveFlags_DB_ACLDisables: Replaced by focused tests TestComputeEffectiveFlags_DB_ACLTrueAndFalse
func TestComputeEffectiveFlags_DB_CrowdSecExternal(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: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
manager := NewManager(nil, db, "", "", false, secCfg)
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "unknown"})
require.NoError(t, res.Error)
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
require.False(t, cs)
}
func TestComputeEffectiveFlags_DB_CrowdSecUnknown(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: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
manager := NewManager(nil, db, "", "", false, secCfg)
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "unknown"})
require.NoError(t, res.Error)
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
require.False(t, cs)
}
func TestComputeEffectiveFlags_DB_CrowdSecLocal(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: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
manager := NewManager(nil, db, "", "", false, secCfg)
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "local"})
require.NoError(t, res.Error)
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
require.True(t, cs)
}
func TestComputeEffectiveFlags_DB_ACLTrueAndFalse(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: true, ACLMode: "enabled"}
manager := NewManager(nil, db, "", "", false, secCfg)
// Set acl true
res := db.Create(&models.Setting{Key: "security.acl.enabled", Value: "true"})
require.NoError(t, res.Error)
_, acl, _, _, _ := manager.computeEffectiveFlags(context.Background())
require.True(t, acl)
// Set acl false
db.Where("key = ?", "security.acl.enabled").Delete(&models.Setting{})
res = db.Create(&models.Setting{Key: "security.acl.enabled", Value: "false"})
require.NoError(t, res.Error)
_, acl, _, _, _ = manager.computeEffectiveFlags(context.Background())
require.False(t, acl)
}
func TestComputeEffectiveFlags_DB_WAFMonitor(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 monitor
res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor"})
require.NoError(t, res.Error)
_, _, waf, _, _ := manager.computeEffectiveFlags(context.Background())
require.True(t, waf) // Should still be true (enabled)
}
func TestManager_ApplyConfig_WAFMonitor(t *testing.T) {
// Mock Caddy Admin API
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/load" && r.Method == "POST" {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer caddyServer.Close()
// 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)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}))
// Set WAF mode to monitor
db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor", AdminWhitelist: "127.0.0.1"})
// Create a ruleset
db.Create(&models.SecurityRuleSet{Name: "owasp-crs", Content: "SecRule REQUEST_URI \"@rx ^/admin\" \"id:101,phase:1,deny,status:403\""})
// Setup Manager
tmpDir := t.TempDir()
client := newTestClient(t, caddyServer.URL)
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"})
// Capture file writes to verify WAF mode injection
var writtenContent string
originalWriteFile := writeFileFunc
defer func() { writeFileFunc = originalWriteFile }()
writeFileFunc = func(filename string, data []byte, perm os.FileMode) error {
if strings.Contains(filename, "owasp-crs") && strings.HasSuffix(filename, ".conf") {
writtenContent = string(data)
}
return originalWriteFile(filename, data, perm)
}
// Create a host
host := models.ProxyHost{
DomainNames: "example.com",
ForwardHost: "127.0.0.1",
ForwardPort: 8080,
}
db.Create(&host)
// Apply Config
err = manager.ApplyConfig(context.Background())
assert.NoError(t, err)
// Verify that DetectionOnly was injected into the ruleset file
assert.Contains(t, writtenContent, "SecRuleEngine DetectionOnly")
assert.Contains(t, writtenContent, "SecRequestBodyAccess On")
}