a7b3cf38a2
Add CI-specific timeout multipliers (3×) to security E2E tests emergency-token.spec.ts, combined-enforcement.spec.ts waf-enforcement.spec.ts, emergency-server.spec.ts Add missing data-testid="multi-file-import-button" to ImportCaddy.tsx Add accessibility attributes to ImportSitesModal.tsx (aria-modal, aria-labelledby) Add ProxyHostServiceInterface for mock injection in tests Fix TestImportHandler_Commit_UpdateFailure (was skipped) Backend coverage: 43.7% → 86.2% for Commit function Resolves: E2E Shard 4 failures, Frontend Quality Check failures, Codecov patch coverage
1216 lines
37 KiB
Go
1216 lines
37 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
func setupImportTestDB(t *testing.T) *gorm.DB {
|
|
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
panic("failed to connect to test database")
|
|
}
|
|
_ = db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Location{})
|
|
return db
|
|
}
|
|
|
|
func TestImportHandler_GetStatus(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Case 1: No active session, no mount
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.GET("/import/status", handler.GetStatus)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/import/status", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, false, resp["has_pending"])
|
|
|
|
// Case 2: No DB session but has mounted Caddyfile
|
|
tmpDir := t.TempDir()
|
|
mountPath := filepath.Join(tmpDir, "mounted.caddyfile")
|
|
_ = os.WriteFile(mountPath, []byte("example.com"), 0o644) //nolint:gosec // G306: test file
|
|
|
|
handler2 := handlers.NewImportHandler(db, "echo", "/tmp", mountPath)
|
|
router2 := gin.New()
|
|
router2.GET("/import/status", handler2.GetStatus)
|
|
|
|
w = httptest.NewRecorder()
|
|
router2.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
err = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, resp["has_pending"])
|
|
session := resp["session"].(map[string]any)
|
|
assert.Equal(t, "transient", session["state"])
|
|
assert.Equal(t, mountPath, session["source_file"])
|
|
|
|
// Case 3: Active DB session (takes precedence over mount)
|
|
dbSession := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "pending",
|
|
ParsedData: `{"hosts": []}`,
|
|
}
|
|
db.Create(&dbSession)
|
|
|
|
w = httptest.NewRecorder()
|
|
router2.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
err = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, resp["has_pending"])
|
|
session = resp["session"].(map[string]any)
|
|
assert.Equal(t, "pending", session["state"]) // DB session, not transient
|
|
}
|
|
|
|
func TestImportHandler_GetPreview(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.GET("/import/preview", handler.GetPreview)
|
|
|
|
// Case 1: No session
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/import/preview", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Case 2: Active session
|
|
session := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "pending",
|
|
ParsedData: `{"hosts": [{"domain_names": "example.com"}]}`,
|
|
}
|
|
db.Create(&session)
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/import/preview", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var result map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &result)
|
|
|
|
preview := result["preview"].(map[string]any)
|
|
hosts := preview["hosts"].([]any)
|
|
assert.Len(t, hosts, 1)
|
|
|
|
// Verify status changed to reviewing
|
|
var updatedSession models.ImportSession
|
|
db.First(&updatedSession, session.ID)
|
|
assert.Equal(t, "reviewing", updatedSession.Status)
|
|
}
|
|
|
|
func TestImportHandler_Cancel(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
session := models.ImportSession{
|
|
UUID: "test-uuid",
|
|
Status: "pending",
|
|
}
|
|
db.Create(&session)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/import/cancel?session_uuid=test-uuid", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var updatedSession models.ImportSession
|
|
db.First(&updatedSession, session.ID)
|
|
assert.Equal(t, "rejected", updatedSession.Status)
|
|
}
|
|
|
|
func TestImportHandler_Commit(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
session := models.ImportSession{
|
|
UUID: "test-uuid",
|
|
Status: "reviewing",
|
|
ParsedData: `{"hosts": [{"domain_names": "example.com", "forward_host": "127.0.0.1", "forward_port": 8080}]}`,
|
|
}
|
|
db.Create(&session)
|
|
|
|
payload := map[string]any{
|
|
"session_uuid": "test-uuid",
|
|
"resolutions": map[string]string{
|
|
"example.com": "import",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify host created
|
|
var host models.ProxyHost
|
|
err := db.Where("domain_names = ?", "example.com").First(&host).Error
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "127.0.0.1", host.ForwardHost)
|
|
|
|
// Verify session committed
|
|
var updatedSession models.ImportSession
|
|
db.First(&updatedSession, session.ID)
|
|
assert.Equal(t, "committed", updatedSession.Status)
|
|
}
|
|
|
|
func TestImportHandler_Upload(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
|
|
payload := map[string]string{
|
|
"content": "example.com",
|
|
"filename": "Caddyfile",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
|
|
// The fake caddy script returns empty JSON, so import may produce zero hosts.
|
|
// The handler now treats zero-host uploads without imports as a bad request (400).
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestImportHandler_GetPreview_WithContent(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, "echo", tmpDir, "")
|
|
router := gin.New()
|
|
router.GET("/import/preview", handler.GetPreview)
|
|
|
|
// Case: Active session with source file
|
|
content := "example.com {\n reverse_proxy localhost:8080\n}"
|
|
sourceFile := filepath.Join(tmpDir, "source.caddyfile")
|
|
err := os.WriteFile(sourceFile, []byte(content), 0o644) //nolint:gosec // G306: test file
|
|
assert.NoError(t, err)
|
|
|
|
// Case: Active session with source file
|
|
session := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "pending",
|
|
ParsedData: `{"hosts": []}`,
|
|
SourceFile: sourceFile,
|
|
}
|
|
db.Create(&session)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/import/preview", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var result map[string]any
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, content, result["caddyfile_content"])
|
|
}
|
|
|
|
func TestImportHandler_Commit_Errors(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// Case 1: Invalid JSON
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBufferString("invalid"))
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Case 2: Session not found
|
|
payload := map[string]any{
|
|
"session_uuid": "non-existent",
|
|
"resolutions": map[string]string{},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Case 3: Invalid ParsedData
|
|
session := models.ImportSession{
|
|
UUID: "invalid-data-uuid",
|
|
Status: "reviewing",
|
|
ParsedData: "invalid-json",
|
|
}
|
|
db.Create(&session)
|
|
|
|
payload = map[string]any{
|
|
"session_uuid": "invalid-data-uuid",
|
|
"resolutions": map[string]string{},
|
|
}
|
|
body, _ = json.Marshal(payload)
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
func TestImportHandler_Cancel_Errors(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
// Case 1: Session not found
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/import/cancel?session_uuid=non-existent", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestCheckMountedImport(t *testing.T) {
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
mountPath := filepath.Join(tmpDir, "mounted.caddyfile")
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
// Case 1: File does not exist
|
|
err := handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir)
|
|
assert.NoError(t, err)
|
|
|
|
// Case 2: File exists, not processed
|
|
err = os.WriteFile(mountPath, []byte("example.com"), 0o644) //nolint:gosec // G306: test file
|
|
assert.NoError(t, err)
|
|
|
|
err = handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir)
|
|
assert.NoError(t, err)
|
|
|
|
// Check if session created (transient preview behavior: no DB session should be created)
|
|
var count int64
|
|
db.Model(&models.ImportSession{}).Where("source_file = ?", mountPath).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
// Case 3: Already processed
|
|
err = handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestImportHandler_Upload_Failure(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Use fake caddy script that fails
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_fail.sh")
|
|
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
|
|
payload := map[string]string{
|
|
"content": "invalid caddyfile",
|
|
"filename": "Caddyfile",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
// The error message comes from Upload -> ImportFile -> "import failed: ..."
|
|
assert.Contains(t, resp["error"], "import failed")
|
|
}
|
|
|
|
func TestImportHandler_Upload_Conflict(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Pre-create a host to cause conflict
|
|
db.Create(&models.ProxyHost{
|
|
DomainNames: "example.com",
|
|
ForwardHost: "127.0.0.1",
|
|
ForwardPort: 9090,
|
|
})
|
|
|
|
// Use fake caddy script that returns hosts
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
|
|
payload := map[string]string{
|
|
"content": "example.com",
|
|
"filename": "Caddyfile",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify response contains conflict in preview (upload is transient)
|
|
var resp map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
preview := resp["preview"].(map[string]any)
|
|
conflicts := preview["conflicts"].([]any)
|
|
found := false
|
|
for _, c := range conflicts {
|
|
if c.(string) == "example.com" || strings.Contains(c.(string), "example.com") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "expected conflict for example.com in preview")
|
|
}
|
|
|
|
func TestImportHandler_GetPreview_BackupContent(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, "echo", tmpDir, "")
|
|
router := gin.New()
|
|
router.GET("/import/preview", handler.GetPreview)
|
|
|
|
// Create backup file
|
|
backupDir := filepath.Join(tmpDir, "backups")
|
|
_ = os.MkdirAll(backupDir, 0o755) //nolint:gosec // G301: test dir
|
|
content := "backup content"
|
|
backupFile := filepath.Join(backupDir, "source.caddyfile")
|
|
_ = os.WriteFile(backupFile, []byte(content), 0o644) //nolint:gosec // G306: test file
|
|
|
|
// Case: Active session with missing source file but existing backup
|
|
session := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "pending",
|
|
ParsedData: `{"hosts": []}`,
|
|
SourceFile: "/non/existent/source.caddyfile",
|
|
}
|
|
db.Create(&session)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/import/preview", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var result map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &result)
|
|
|
|
assert.Equal(t, content, result["caddyfile_content"])
|
|
}
|
|
|
|
func TestImportHandler_RegisterRoutes(t *testing.T) {
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
api := router.Group("/api/v1")
|
|
handler.RegisterRoutes(api)
|
|
|
|
// Verify routes exist by making requests
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/import/status", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestImportHandler_GetPreview_TransientMount(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
mountPath := filepath.Join(tmpDir, "mounted.caddyfile")
|
|
|
|
// Create a mounted Caddyfile
|
|
content := "example.com"
|
|
err := os.WriteFile(mountPath, []byte(content), 0o644) //nolint:gosec // G306: test file
|
|
assert.NoError(t, err)
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, mountPath)
|
|
router := gin.New()
|
|
router.GET("/import/preview", handler.GetPreview)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/import/preview", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Response body: %s", w.Body.String())
|
|
var result map[string]any
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify transient session
|
|
session, ok := result["session"].(map[string]any)
|
|
assert.True(t, ok, "session should be present in response")
|
|
assert.Equal(t, "transient", session["state"])
|
|
assert.Equal(t, mountPath, session["source_file"])
|
|
|
|
// Verify preview contains hosts
|
|
preview, ok := result["preview"].(map[string]any)
|
|
assert.True(t, ok, "preview should be present in response")
|
|
assert.NotNil(t, preview["hosts"])
|
|
|
|
// Verify content
|
|
assert.Equal(t, content, result["caddyfile_content"])
|
|
}
|
|
|
|
func TestImportHandler_Commit_TransientUpload(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// First upload to create transient session
|
|
uploadPayload := map[string]string{
|
|
"content": "uploaded.com",
|
|
"filename": "Caddyfile",
|
|
}
|
|
uploadBody, _ := json.Marshal(uploadPayload)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(uploadBody))
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Extract session ID
|
|
var uploadResp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &uploadResp)
|
|
session := uploadResp["session"].(map[string]any)
|
|
sessionID := session["id"].(string)
|
|
|
|
// Now commit the transient upload
|
|
commitPayload := map[string]any{
|
|
"session_uuid": sessionID,
|
|
"resolutions": map[string]string{
|
|
"uploaded.com": "import",
|
|
},
|
|
}
|
|
commitBody, _ := json.Marshal(commitPayload)
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer(commitBody))
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify host created
|
|
var host models.ProxyHost
|
|
err := db.Where("domain_names = ?", "uploaded.com").First(&host).Error
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "uploaded.com", host.DomainNames)
|
|
|
|
// Verify session persisted
|
|
var importSession models.ImportSession
|
|
err = db.Where("uuid = ?", sessionID).First(&importSession).Error
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "committed", importSession.Status)
|
|
}
|
|
|
|
func TestImportHandler_Commit_TransientMount(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
mountPath := filepath.Join(tmpDir, "mounted.caddyfile")
|
|
|
|
// Create a mounted Caddyfile
|
|
err := os.WriteFile(mountPath, []byte("mounted.com"), 0o644) //nolint:gosec // G306: test file
|
|
assert.NoError(t, err)
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, mountPath)
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// Commit the mount with a random session ID (transient)
|
|
sessionID := uuid.NewString()
|
|
commitPayload := map[string]any{
|
|
"session_uuid": sessionID,
|
|
"resolutions": map[string]string{
|
|
"mounted.com": "import",
|
|
},
|
|
}
|
|
commitBody, _ := json.Marshal(commitPayload)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(commitBody))
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify host created
|
|
var host models.ProxyHost
|
|
err = db.Where("domain_names = ?", "mounted.com").First(&host).Error
|
|
assert.NoError(t, err)
|
|
|
|
// Verify session persisted
|
|
var importSession models.ImportSession
|
|
err = db.Where("uuid = ?", sessionID).First(&importSession).Error
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "committed", importSession.Status)
|
|
}
|
|
|
|
func TestImportHandler_Cancel_TransientUpload(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
// Upload to create transient file
|
|
uploadPayload := map[string]string{
|
|
"content": "test.com",
|
|
"filename": "Caddyfile",
|
|
}
|
|
uploadBody, _ := json.Marshal(uploadPayload)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(uploadBody))
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Extract session ID and file path
|
|
var uploadResp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &uploadResp)
|
|
session := uploadResp["session"].(map[string]any)
|
|
sessionID := session["id"].(string)
|
|
sourceFile := session["source_file"].(string)
|
|
|
|
// Verify file exists
|
|
_, err := os.Stat(sourceFile)
|
|
assert.NoError(t, err)
|
|
|
|
// Cancel should delete the file
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/import/cancel?session_uuid="+sessionID, http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify file deleted
|
|
_, err = os.Stat(sourceFile)
|
|
assert.True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
func TestImportHandler_Errors(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
router.POST("/import/commit", handler.Commit)
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
// Upload - Invalid JSON
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer([]byte("invalid")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Commit - Invalid JSON
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer([]byte("invalid")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Commit - Session Not Found
|
|
body := map[string]any{
|
|
"session_uuid": "non-existent",
|
|
"resolutions": map[string]string{},
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Cancel - Session Not Found
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/import/cancel?session_uuid=non-existent", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestImportHandler_DetectImports(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/detect-imports", handler.DetectImports)
|
|
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
hasImport bool
|
|
imports []string
|
|
}{
|
|
{
|
|
name: "no imports",
|
|
content: "example.com { reverse_proxy localhost:8080 }",
|
|
hasImport: false,
|
|
imports: []string{},
|
|
},
|
|
{
|
|
name: "single import",
|
|
content: "import sites/*\nexample.com { reverse_proxy localhost:8080 }",
|
|
hasImport: true,
|
|
imports: []string{"sites/*"},
|
|
},
|
|
{
|
|
name: "multiple imports",
|
|
content: "import sites/*\nimport config/ssl.conf\nexample.com { reverse_proxy localhost:8080 }",
|
|
hasImport: true,
|
|
imports: []string{"sites/*", "config/ssl.conf"},
|
|
},
|
|
{
|
|
name: "import with comment",
|
|
content: "import sites/* # Load all sites\nexample.com { reverse_proxy localhost:8080 }",
|
|
hasImport: true,
|
|
imports: []string{"sites/*"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
payload := map[string]string{"content": tt.content}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/detect-imports", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.hasImport, resp["has_imports"])
|
|
|
|
imports := resp["imports"].([]any)
|
|
assert.Len(t, imports, len(tt.imports))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImportHandler_DetectImports_InvalidJSON(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/detect-imports", handler.DetectImports)
|
|
|
|
// Invalid JSON
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/detect-imports", strings.NewReader("invalid"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestImportHandler_UploadMulti(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Use fake caddy script
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload-multi", handler.UploadMulti)
|
|
|
|
t.Run("single Caddyfile", func(t *testing.T) {
|
|
payload := map[string]any{
|
|
"files": []map[string]string{
|
|
{"filename": "Caddyfile", "content": "example.com"},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NotNil(t, resp["session"])
|
|
assert.NotNil(t, resp["preview"])
|
|
})
|
|
|
|
t.Run("Caddyfile with site files", func(t *testing.T) {
|
|
payload := map[string]any{
|
|
"files": []map[string]string{
|
|
{"filename": "Caddyfile", "content": "import sites/*\n"},
|
|
{"filename": "sites/site1", "content": "site1.com"},
|
|
{"filename": "sites/site2", "content": "site2.com"},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
session := resp["session"].(map[string]any)
|
|
assert.Equal(t, "transient", session["state"])
|
|
})
|
|
|
|
t.Run("missing Caddyfile", func(t *testing.T) {
|
|
payload := map[string]any{
|
|
"files": []map[string]string{
|
|
{"filename": "sites/site1", "content": "site1.com"},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("path traversal in filename", func(t *testing.T) {
|
|
payload := map[string]any{
|
|
"files": []map[string]string{
|
|
{"filename": "Caddyfile", "content": "import sites/*\n"},
|
|
{"filename": "../etc/passwd", "content": "sensitive"},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("empty file content", func(t *testing.T) {
|
|
payload := map[string]any{
|
|
"files": []map[string]string{
|
|
{"filename": "Caddyfile", "content": "example.com"},
|
|
{"filename": "sites/site1", "content": " "},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload-multi", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp["error"], "empty")
|
|
})
|
|
}
|
|
|
|
// Additional tests for comprehensive coverage
|
|
|
|
func TestImportHandler_Cancel_MissingSessionUUID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
// Missing session_uuid parameter
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/import/cancel", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Equal(t, "session_uuid required", resp["error"])
|
|
}
|
|
|
|
func TestImportHandler_Cancel_InvalidSessionUUID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.DELETE("/import/cancel", handler.Cancel)
|
|
|
|
// Test "." which becomes empty after filepath.Base processing
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/import/cancel?session_uuid=.", http.NoBody)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Equal(t, "invalid session_uuid", resp["error"])
|
|
}
|
|
|
|
func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// Test "." which becomes empty after filepath.Base processing
|
|
payload := map[string]any{
|
|
"session_uuid": ".",
|
|
"resolutions": map[string]string{},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Equal(t, "invalid session_uuid", resp["error"])
|
|
}
|
|
|
|
// mockProxyHostService is a mock implementation of ProxyHostServiceInterface for testing.
|
|
type mockProxyHostService struct {
|
|
createFunc func(host *models.ProxyHost) error
|
|
updateFunc func(host *models.ProxyHost) error
|
|
listFunc func() ([]models.ProxyHost, error)
|
|
}
|
|
|
|
func (m *mockProxyHostService) Create(host *models.ProxyHost) error {
|
|
if m.createFunc != nil {
|
|
return m.createFunc(host)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockProxyHostService) Update(host *models.ProxyHost) error {
|
|
if m.updateFunc != nil {
|
|
return m.updateFunc(host)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockProxyHostService) List() ([]models.ProxyHost, error) {
|
|
if m.listFunc != nil {
|
|
return m.listFunc()
|
|
}
|
|
return []models.ProxyHost{}, nil
|
|
}
|
|
|
|
// TestImportHandler_Commit_UpdateFailure tests the error logging path when Update fails (line 676)
|
|
func TestImportHandler_Commit_UpdateFailure(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Create an existing host that we'll try to overwrite
|
|
existingHost := models.ProxyHost{
|
|
UUID: uuid.NewString(),
|
|
DomainNames: "existing.com",
|
|
}
|
|
db.Create(&existingHost)
|
|
|
|
// Create an import session with a host matching the existing one
|
|
session := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "reviewing",
|
|
ParsedData: `{
|
|
"hosts": [
|
|
{
|
|
"domain_names": "existing.com",
|
|
"forward_host": "192.168.1.1",
|
|
"forward_port": 80,
|
|
"forward_scheme": "http"
|
|
}
|
|
]
|
|
}`,
|
|
}
|
|
db.Create(&session)
|
|
|
|
// Create a mock service that returns existing hosts and fails on Update
|
|
mockSvc := &mockProxyHostService{
|
|
listFunc: func() ([]models.ProxyHost, error) {
|
|
return []models.ProxyHost{existingHost}, nil
|
|
},
|
|
updateFunc: func(host *models.ProxyHost) error {
|
|
return errors.New("mock update failure: database connection lost")
|
|
},
|
|
}
|
|
|
|
handler := handlers.NewImportHandlerWithService(db, mockSvc, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// Request to overwrite existing.com
|
|
payload := map[string]any{
|
|
"session_uuid": session.UUID,
|
|
"resolutions": map[string]string{
|
|
"existing.com": "overwrite",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// The commit should complete but with errors (line 676 executed)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
// Should have errors due to update failure
|
|
respErrors, ok := resp["errors"].([]interface{})
|
|
assert.True(t, ok, "expected errors array in response")
|
|
assert.Greater(t, len(respErrors), 0, "expected at least one error")
|
|
assert.Contains(t, respErrors[0].(string), "existing.com")
|
|
assert.Contains(t, respErrors[0].(string), "mock update failure")
|
|
|
|
// updated count should be 0
|
|
assert.Equal(t, float64(0), resp["updated"])
|
|
}
|
|
|
|
// TestImportHandler_Commit_CreateFailure tests the error logging path when Create fails (line 682)
|
|
func TestImportHandler_Commit_CreateFailure(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Create an existing host to cause a duplicate error
|
|
existingHost := models.ProxyHost{
|
|
UUID: uuid.NewString(),
|
|
DomainNames: "duplicate.com",
|
|
}
|
|
db.Create(&existingHost)
|
|
|
|
// Create an import session that tries to create a duplicate host
|
|
session := models.ImportSession{
|
|
UUID: uuid.NewString(),
|
|
Status: "reviewing",
|
|
ParsedData: `{
|
|
"hosts": [
|
|
{
|
|
"domain_names": "duplicate.com",
|
|
"forward_host": "192.168.1.1",
|
|
"forward_port": 80,
|
|
"forward_scheme": "http"
|
|
}
|
|
]
|
|
}`,
|
|
}
|
|
db.Create(&session)
|
|
|
|
handler := handlers.NewImportHandler(db, "echo", "/tmp", "")
|
|
router := gin.New()
|
|
router.POST("/import/commit", handler.Commit)
|
|
|
|
// Don't provide resolution, so it defaults to create (not overwrite)
|
|
payload := map[string]any{
|
|
"session_uuid": session.UUID,
|
|
"resolutions": map[string]string{},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// The commit should complete but with errors
|
|
// Line 682 should be executed: logging the create error
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
// Should have errors due to duplicate domain
|
|
errors, ok := resp["errors"].([]interface{})
|
|
assert.True(t, ok)
|
|
assert.Greater(t, len(errors), 0)
|
|
// Verify the error mentions the duplicate
|
|
assert.Contains(t, errors[0].(string), "duplicate.com")
|
|
}
|
|
|
|
// TestUpload_NormalizationSuccess tests the success path where NormalizeCaddyfile succeeds (line 271)
|
|
func TestUpload_NormalizationSuccess(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Use fake caddy script that handles both fmt and adapt
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_fmt_success.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
|
|
// Use single-line Caddyfile format (triggers normalization)
|
|
singleLineCaddyfile := `test.local { reverse_proxy localhost:3000 }`
|
|
|
|
payload := map[string]string{
|
|
"content": singleLineCaddyfile,
|
|
"filename": "Caddyfile",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should succeed with 200 (normalization worked)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify response contains hosts (parsing succeeded)
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify preview contains hosts
|
|
preview, ok := response["preview"].(map[string]any)
|
|
assert.True(t, ok, "response should contain preview")
|
|
hosts, ok := preview["hosts"].([]any)
|
|
assert.True(t, ok, "preview should contain hosts")
|
|
assert.Greater(t, len(hosts), 0, "should have at least one parsed host")
|
|
}
|
|
|
|
// TestUpload_NormalizationFallback tests the fallback path where NormalizeCaddyfile fails (line 269)
|
|
func TestUpload_NormalizationFallback(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupImportTestDB(t)
|
|
|
|
// Use fake caddy script that fails fmt but succeeds on adapt
|
|
cwd, _ := os.Getwd()
|
|
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_fmt_fail.sh")
|
|
_ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions
|
|
|
|
tmpDir := t.TempDir()
|
|
handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "")
|
|
router := gin.New()
|
|
router.POST("/import/upload", handler.Upload)
|
|
|
|
// Valid Caddyfile that would parse successfully (even if normalization fails)
|
|
caddyfile := `test.local {
|
|
reverse_proxy localhost:3000
|
|
}`
|
|
|
|
payload := map[string]string{
|
|
"content": caddyfile,
|
|
"filename": "Caddyfile",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should still succeed (falls back to original content)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify hosts were parsed from original content
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify preview contains hosts
|
|
preview, ok := response["preview"].(map[string]any)
|
|
assert.True(t, ok, "response should contain preview")
|
|
hosts, ok := preview["hosts"].([]any)
|
|
assert.True(t, ok, "preview should contain hosts")
|
|
assert.Greater(t, len(hosts), 0, "should have at least one parsed host from original content")
|
|
}
|