- Removed redundant `gin.SetMode(gin.TestMode)` calls from individual test files. - Introduced a centralized `TestMain` function in `testmain_test.go` to set the Gin mode for all tests. - Ensured consistent test environment setup across various handler test files.
915 lines
25 KiB
Go
915 lines
25 KiB
Go
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)
|
|
}
|