Files
Charon/backend/internal/api/handlers/additional_coverage_test.go
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

915 lines
25 KiB
Go
Executable File

package handlers
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/services"
)
func setupImportCoverageDB(t *testing.T) *gorm.DB {
t.Helper()
db := OpenTestDB(t)
_ = db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Domain{})
return db
}
func TestImportHandler_Commit_InvalidJSON(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/commit", bytes.NewBufferString("invalid"))
c.Request.Header.Set("Content-Type", "application/json")
h.Commit(c)
assert.Equal(t, 400, w.Code)
}
func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"session_uuid": "../../../etc/passwd",
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.Commit(c)
// After sanitization, "../../../etc/passwd" becomes "passwd" which doesn't exist
assert.Equal(t, 404, w.Code)
assert.Contains(t, w.Body.String(), "session not found")
}
func TestImportHandler_Commit_SessionNotFound(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"session_uuid": "nonexistent-session",
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.Commit(c)
assert.Equal(t, 404, w.Code)
assert.Contains(t, w.Body.String(), "session not found")
}
// Remote Server Handler additional test
func setupRemoteServerCoverageDB2(t *testing.T) *gorm.DB {
t.Helper()
db := OpenTestDB(t)
_ = db.AutoMigrate(&models.RemoteServer{})
return db
}
func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) {
db := setupRemoteServerCoverageDB2(t)
svc := services.NewRemoteServerService(db)
h := NewRemoteServerHandler(svc, nil)
// Create a server with unreachable host
server := &models.RemoteServer{
Name: "Unreachable",
Host: "192.0.2.1", // TEST-NET - not routable
Port: 65535,
}
_ = svc.Create(server)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "uuid", Value: server.UUID}}
h.TestConnection(c)
// Should return 200 with reachable: false
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"reachable":false`)
}
// Security Handler additional coverage tests
func setupSecurityCoverageDB3(t *testing.T) *gorm.DB {
t.Helper()
db := OpenTestDB(t)
_ = db.AutoMigrate(
&models.SecurityConfig{},
&models.SecurityDecision{},
&models.SecurityRuleSet{},
&models.SecurityAudit{},
)
return db
}
func TestSecurityHandler_GetConfig_InternalError(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop table to cause internal error (not ErrSecurityConfigNotFound)
_ = db.Migrator().DropTable(&models.SecurityConfig{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/security/config", http.NoBody)
h.GetConfig(c)
// Should return internal error
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to read security config")
}
func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) {
db := setupSecurityCoverageDB3(t)
// Create handler with nil caddy manager (ApplyConfig will be called but is nil)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
body, _ := json.Marshal(map[string]any{
"name": "test",
"waf_mode": "block",
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/security/config", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UpdateConfig(c)
// Should succeed (caddy manager is nil so no apply error)
assert.Equal(t, 200, w.Code)
}
func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop the config table so generate fails
_ = db.Migrator().DropTable(&models.SecurityConfig{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/security/breakglass", http.NoBody)
h.GenerateBreakGlass(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to generate break-glass token")
}
func TestSecurityHandler_ListDecisions_Error(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop decisions table
_ = db.Migrator().DropTable(&models.SecurityDecision{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/security/decisions", http.NoBody)
h.ListDecisions(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to list decisions")
}
func TestSecurityHandler_ListRuleSets_Error(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop rulesets table
_ = db.Migrator().DropTable(&models.SecurityRuleSet{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/security/rulesets", http.NoBody)
h.ListRuleSets(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to list rule sets")
}
func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop table to cause upsert to fail
_ = db.Migrator().DropTable(&models.SecurityRuleSet{})
body, _ := json.Marshal(map[string]any{
"name": "test-ruleset",
"enabled": true,
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/security/rulesets", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UpsertRuleSet(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to upsert ruleset")
}
func TestSecurityHandler_CreateDecision_LogError(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop decisions table to cause log to fail
_ = db.Migrator().DropTable(&models.SecurityDecision{})
body, _ := json.Marshal(map[string]any{
"ip": "192.168.1.1",
"action": "block", // Use valid action to pass validation
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.CreateDecision(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to log decision")
}
func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) {
db := setupSecurityCoverageDB3(t)
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
// Drop table to cause delete to fail (not NotFound but table error)
_ = db.Migrator().DropTable(&models.SecurityRuleSet{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Params = gin.Params{{Key: "id", Value: "999"}}
h.DeleteRuleSet(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "failed to delete ruleset")
}
// CrowdSec ImportConfig additional coverage tests
func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) {
db := setupCrowdDB(t)
tmpDir := t.TempDir()
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Create empty file upload
buf := &bytes.Buffer{}
mw := multipart.NewWriter(buf)
fw, _ := mw.CreateFormFile("file", "empty.tar.gz")
// Write nothing to make file empty
_ = fw
_ = mw.Close()
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v1/admin/crowdsec/import", buf)
req.Header.Set("Content-Type", mw.FormDataContentType())
r.ServeHTTP(w, req)
// Empty upload now returns 422 (validation error) instead of 400
assert.Equal(t, 422, w.Code)
assert.Contains(t, w.Body.String(), "validation failed")
}
// Backup Handler additional coverage tests
func TestBackupHandler_List_DBError(t *testing.T) {
// Use a non-writable temp dir to simulate errors
tmpDir := t.TempDir()
cfg := &config.Config{
DatabasePath: filepath.Join(tmpDir, "nonexistent", "charon.db"),
}
svc := services.NewBackupService(cfg)
defer svc.Stop() // Prevent goroutine leaks
h := NewBackupHandler(svc)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
h.List(c)
// Should succeed with empty list (service handles missing dir gracefully)
assert.Equal(t, 200, w.Code)
}
// ImportHandler UploadMulti coverage tests
func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBufferString("invalid"))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
assert.Equal(t, 400, w.Code)
}
func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"files": []map[string]string{
{"filename": "sites/example.com", "content": "example.com {}"},
},
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
assert.Equal(t, 400, w.Code)
assert.Contains(t, w.Body.String(), "must include a main Caddyfile")
}
func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"files": []map[string]string{
{"filename": "Caddyfile", "content": ""},
},
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
assert.Equal(t, 400, w.Code)
assert.Contains(t, w.Body.String(), "is empty")
}
func TestImportHandler_UploadMulti_PathTraversal(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"files": []map[string]string{
{"filename": "Caddyfile", "content": "example.com {}"},
{"filename": "../../../etc/passwd", "content": "bad content"},
},
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
assert.Equal(t, 400, w.Code)
assert.Contains(t, w.Body.String(), "invalid filename")
}
// Logs Handler Download error coverage
func setupLogsDownloadTest(t *testing.T) (h *LogsHandler, logsDir string) {
t.Helper()
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
// #nosec G301 -- Test fixture directory with standard permissions
_ = os.MkdirAll(dataDir, 0o755)
logsDir = filepath.Join(dataDir, "logs")
// #nosec G301 -- Test fixture directory with standard permissions
_ = os.MkdirAll(logsDir, 0o755)
dbPath := filepath.Join(dataDir, "charon.db")
cfg := &config.Config{DatabasePath: dbPath}
svc := services.NewLogService(cfg)
h = NewLogsHandler(svc)
return h, logsDir
}
func TestLogsHandler_Download_PathTraversal(t *testing.T) {
h, _ := setupLogsDownloadTest(t)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "filename", Value: "../../../etc/passwd"}}
c.Request = httptest.NewRequest("GET", "/logs/../../../etc/passwd/download", http.NoBody)
h.Download(c)
assert.Equal(t, 400, w.Code)
assert.Contains(t, w.Body.String(), "invalid filename")
}
func TestLogsHandler_Download_NotFound(t *testing.T) {
h, _ := setupLogsDownloadTest(t)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "filename", Value: "nonexistent.log"}}
c.Request = httptest.NewRequest("GET", "/logs/nonexistent.log/download", http.NoBody)
h.Download(c)
assert.Equal(t, 404, w.Code)
assert.Contains(t, w.Body.String(), "not found")
}
func TestLogsHandler_Download_Success(t *testing.T) {
h, logsDir := setupLogsDownloadTest(t)
// Create a log file to download
// #nosec G306 -- Test fixture file with standard read permissions
_ = os.WriteFile(filepath.Join(logsDir, "test.log"), []byte("log content"), 0o644)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "filename", Value: "test.log"}}
c.Request = httptest.NewRequest("GET", "/logs/test.log/download", http.NoBody)
h.Download(c)
assert.Equal(t, 200, w.Code)
}
// Import Handler Upload error tests
func TestImportHandler_Upload_InvalidJSON(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload", bytes.NewBufferString("not json"))
c.Request.Header.Set("Content-Type", "application/json")
h.Upload(c)
assert.Equal(t, 400, w.Code)
}
func TestImportHandler_Upload_EmptyContent(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]string{
"content": "",
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.Upload(c)
assert.Equal(t, 400, w.Code)
}
// Additional Backup Handler tests
func TestBackupHandler_List_ServiceError(t *testing.T) {
// Create a temp dir with invalid permission for backup dir
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
// #nosec G301 -- Test fixture directory with standard permissions
_ = os.MkdirAll(dataDir, 0o755)
// Create database file so config is valid
dbPath := filepath.Join(dataDir, "charon.db")
// #nosec G306 -- Test fixture file with standard read permissions
_ = os.WriteFile(dbPath, []byte("test"), 0o644)
cfg := &config.Config{
DatabasePath: dbPath,
}
svc := services.NewBackupService(cfg)
h := NewBackupHandler(svc)
// Make backup dir a file to cause ReadDir error
_ = os.RemoveAll(svc.BackupDir)
// #nosec G306 -- Test fixture file intentionally blocking directory creation
_ = os.WriteFile(svc.BackupDir, []byte("not a dir"), 0o644)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("GET", "/backups", http.NoBody)
h.List(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "Failed to list backups")
}
func TestBackupHandler_Delete_PathTraversal(t *testing.T) {
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
_ = os.MkdirAll(dataDir, 0o750)
dbPath := filepath.Join(dataDir, "charon.db")
_ = os.WriteFile(dbPath, []byte("test"), 0o600)
cfg := &config.Config{
DatabasePath: dbPath,
}
svc := services.NewBackupService(cfg)
defer svc.Stop() // Prevent goroutine leaks
h := NewBackupHandler(svc)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Params = gin.Params{{Key: "filename", Value: "../../../etc/passwd"}}
c.Request = httptest.NewRequest("DELETE", "/backups/../../../etc/passwd", http.NoBody)
h.Delete(c)
// Path traversal detection returns 500 with generic error
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "Failed to delete backup")
}
func TestBackupHandler_Delete_InternalError2(t *testing.T) {
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
// #nosec G301 -- Test fixture directory with standard permissions
_ = os.MkdirAll(dataDir, 0o755)
dbPath := filepath.Join(dataDir, "charon.db")
// #nosec G306 -- Test fixture file with standard permissions
_ = os.WriteFile(dbPath, []byte("test"), 0o644)
cfg := &config.Config{
DatabasePath: dbPath,
}
svc := services.NewBackupService(cfg)
defer svc.Stop() // Prevent goroutine leaks
h := NewBackupHandler(svc)
// Create a backup
backupsDir := filepath.Join(dataDir, "backups")
// #nosec G301 -- Test fixture directory with standard permissions
_ = os.MkdirAll(backupsDir, 0o755)
backupFile := filepath.Join(backupsDir, "test.zip")
// #nosec G306 -- Test fixture file with standard read permissions
_ = os.WriteFile(backupFile, []byte("backup"), 0o644)
// Remove write permissions to cause delete error
// #nosec G302 -- Test intentionally uses restrictive perms to simulate error
_ = os.Chmod(backupsDir, 0o555)
defer func() {
// #nosec G302 -- Cleanup restores directory permissions
_ = os.Chmod(backupsDir, 0o755)
}()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Params = gin.Params{{Key: "filename", Value: "test.zip"}}
c.Request = httptest.NewRequest("DELETE", "/backups/test.zip", http.NoBody)
h.Delete(c)
// Permission error
assert.Contains(t, []int{200, 500}, w.Code)
}
// Remote Server TestConnection error paths
func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) {
db := setupRemoteServerCoverageDB2(t)
svc := services.NewRemoteServerService(db)
h := NewRemoteServerHandler(svc, nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "uuid", Value: "nonexistent-uuid"}}
h.TestConnection(c)
assert.Equal(t, 404, w.Code)
}
func TestRemoteServerHandler_TestConnectionCustom_Unreachable2(t *testing.T) {
db := setupRemoteServerCoverageDB2(t)
svc := services.NewRemoteServerService(db)
h := NewRemoteServerHandler(svc, nil)
body, _ := json.Marshal(map[string]any{
"host": "192.0.2.1", // TEST-NET - not routable
"port": 65535,
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/remote-servers/test", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.TestConnectionCustom(c)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"reachable":false`)
}
// Auth Handler Register error paths
func setupAuthCoverageDB(t *testing.T) *gorm.DB {
t.Helper()
db := OpenTestDB(t)
_ = db.AutoMigrate(&models.User{}, &models.Setting{})
return db
}
func TestAuthHandler_Register_InvalidJSON(t *testing.T) {
db := setupAuthCoverageDB(t)
cfg := config.Config{JWTSecret: "test-secret"}
authService := services.NewAuthService(db, cfg)
h := NewAuthHandler(authService)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/register", bytes.NewBufferString("invalid"))
c.Request.Header.Set("Content-Type", "application/json")
h.Register(c)
assert.Equal(t, 400, w.Code)
}
// Health handler coverage
func TestHealthHandler_Basic(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/health", http.NoBody)
HealthHandler(c)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "status")
assert.Contains(t, w.Body.String(), "ok")
}
// Backup Create error coverage
func TestBackupHandler_Create_Error(t *testing.T) {
// Use a path where database file doesn't exist
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
_ = os.MkdirAll(dataDir, 0o750)
// Don't create the database file - this will cause CreateBackup to fail
dbPath := filepath.Join(dataDir, "charon.db")
cfg := &config.Config{
DatabasePath: dbPath,
}
svc := services.NewBackupService(cfg)
defer svc.Stop() // Prevent goroutine leaks
h := NewBackupHandler(svc)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/backups", http.NoBody)
h.Create(c)
// Should fail because database file doesn't exist
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "Failed to create backup")
}
// Settings Handler coverage
func setupSettingsCoverageDB(t *testing.T) *gorm.DB {
t.Helper()
db := OpenTestDB(t)
_ = db.AutoMigrate(&models.Setting{})
return db
}
func TestSettingsHandler_GetSettings_Error(t *testing.T) {
db := setupSettingsCoverageDB(t)
h := NewSettingsHandler(db)
// Drop table to cause error
_ = db.Migrator().DropTable(&models.Setting{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/settings", http.NoBody)
h.GetSettings(c)
assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), "Failed to fetch settings")
}
func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) {
db := setupSettingsCoverageDB(t)
h := NewSettingsHandler(db)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings/test", bytes.NewBufferString("invalid"))
c.Request.Header.Set("Content-Type", "application/json")
h.UpdateSetting(c)
assert.Equal(t, 400, w.Code)
}
// Additional remote server TestConnection tests
func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) {
db := setupRemoteServerCoverageDB2(t)
svc := services.NewRemoteServerService(db)
h := NewRemoteServerHandler(svc, nil)
// Use localhost which should be reachable
server := &models.RemoteServer{
Name: "LocalTest",
Host: "127.0.0.1",
Port: 22, // SSH port typically listening on localhost
}
_ = svc.Create(server)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "uuid", Value: server.UUID}}
h.TestConnection(c)
// Should return 200 regardless of whether port is open
assert.Equal(t, 200, w.Code)
}
func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) {
db := setupRemoteServerCoverageDB2(t)
svc := services.NewRemoteServerService(db)
h := NewRemoteServerHandler(svc, nil)
// Create server with empty host
server := &models.RemoteServer{
Name: "Empty",
Host: "",
Port: 22,
}
db.Create(server)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "uuid", Value: server.UUID}}
h.TestConnection(c)
// Should return 200 - empty host resolves to localhost on some systems
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"reachable":`)
}
// Additional UploadMulti test with valid Caddyfile content
func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"files": []map[string]string{
{"filename": "Caddyfile", "content": "example.com { reverse_proxy localhost:8080 }"},
},
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
// Without caddy binary, will fail with 400 at adapt step - that's fine, we hit the code path
// We just verify we got a response (not a panic)
assert.True(t, w.Code == 200 || w.Code == 400, "Should return valid HTTP response")
}
func TestImportHandler_UploadMulti_SubdirFile(t *testing.T) {
db := setupImportCoverageDB(t)
h := NewImportHandler(db, "", t.TempDir(), "")
body, _ := json.Marshal(map[string]any{
"files": []map[string]string{
{"filename": "Caddyfile", "content": "import sites/*"},
{"filename": "sites/example.com", "content": "example.com {}"},
},
})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.UploadMulti(c)
// Should process the subdirectory file
// Just verify it doesn't crash
assert.True(t, w.Code == 200 || w.Code == 400)
}