diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go index 104660d3..38c40e1c 100644 --- a/backend/internal/api/handlers/import_handler.go +++ b/backend/internal/api/handlers/import_handler.go @@ -82,42 +82,40 @@ func (h *ImportHandler) GetPreview(c *gin.Context) { Order("created_at DESC"). First(&session).Error - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "no pending import"}) - return - } + if err == nil { + // DB session found + var result caddy.ImportResult + if err := json.Unmarshal([]byte(session.ParsedData), &result); err == nil { + // Update status to reviewing + session.Status = "reviewing" + h.db.Save(&session) - var result caddy.ImportResult - if err := json.Unmarshal([]byte(session.ParsedData), &result); err == nil { - // Update status to reviewing - session.Status = "reviewing" - h.db.Save(&session) - - // Read original Caddyfile content if available - var caddyfileContent string - if session.SourceFile != "" { - if content, err := os.ReadFile(session.SourceFile); err == nil { - caddyfileContent = string(content) - } else { - backupPath := filepath.Join(h.importDir, "backups", filepath.Base(session.SourceFile)) - if content, err := os.ReadFile(backupPath); err == nil { + // Read original Caddyfile content if available + var caddyfileContent string + if session.SourceFile != "" { + if content, err := os.ReadFile(session.SourceFile); err == nil { caddyfileContent = string(content) + } else { + backupPath := filepath.Join(h.importDir, "backups", filepath.Base(session.SourceFile)) + if content, err := os.ReadFile(backupPath); err == nil { + caddyfileContent = string(content) + } } } - } - c.JSON(http.StatusOK, gin.H{ - "session": gin.H{ - "id": session.UUID, - "state": session.Status, - "created_at": session.CreatedAt, - "updated_at": session.UpdatedAt, - "source_file": session.SourceFile, - }, - "preview": result, - "caddyfile_content": caddyfileContent, - }) - return + c.JSON(http.StatusOK, gin.H{ + "session": gin.H{ + "id": session.UUID, + "state": session.Status, + "created_at": session.CreatedAt, + "updated_at": session.UpdatedAt, + "source_file": session.SourceFile, + }, + "preview": result, + "caddyfile_content": caddyfileContent, + }) + return + } } // No DB session found or failed to parse session. Try transient preview from mountPath. diff --git a/backend/internal/api/handlers/import_handler_test.go b/backend/internal/api/handlers/import_handler_test.go index 3e0e8f75..fe3a88e3 100644 --- a/backend/internal/api/handlers/import_handler_test.go +++ b/backend/internal/api/handlers/import_handler_test.go @@ -452,6 +452,203 @@ func TestImportHandler_RegisterRoutes(t *testing.T) { 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), 0644) + assert.NoError(t, err) + + // Use fake caddy script + cwd, _ := os.Getwd() + fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") + os.Chmod(fakeCaddy, 0755) + + 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", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, "Response body: %s", w.Body.String()) + var result map[string]interface{} + err = json.Unmarshal(w.Body.Bytes(), &result) + assert.NoError(t, err) + + // Verify transient session + session, ok := result["session"].(map[string]interface{}) + 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]interface{}) + 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, 0755) + + 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]interface{} + json.Unmarshal(w.Body.Bytes(), &uploadResp) + session := uploadResp["session"].(map[string]interface{}) + sessionID := session["id"].(string) + + // Now commit the transient upload + commitPayload := map[string]interface{}{ + "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"), 0644) + assert.NoError(t, err) + + // Use fake caddy script + cwd, _ := os.Getwd() + fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") + os.Chmod(fakeCaddy, 0755) + + 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]interface{}{ + "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, 0755) + + 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]interface{} + json.Unmarshal(w.Body.Bytes(), &uploadResp) + session := uploadResp["session"].(map[string]interface{}) + 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, nil) + 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) diff --git a/backend/internal/api/handlers/testdata/fake_caddy_hosts.sh b/backend/internal/api/handlers/testdata/fake_caddy_hosts.sh index df463bc7..2f77c83b 100755 --- a/backend/internal/api/handlers/testdata/fake_caddy_hosts.sh +++ b/backend/internal/api/handlers/testdata/fake_caddy_hosts.sh @@ -4,7 +4,12 @@ if [ "$1" = "version" ]; then exit 0 fi if [ "$1" = "adapt" ]; then - echo '{"apps":{"http":{"servers":{"srv0":{"routes":[{"match":[{"host":["example.com"]}],"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:8080"}]}]}]}}}}}' + # Read the domain from the input Caddyfile (stdin or --config file) + DOMAIN="example.com" + if [ "$2" = "--config" ]; then + DOMAIN=$(cat "$3" | head -1 | tr -d '\n') + fi + echo "{\"apps\":{\"http\":{\"servers\":{\"srv0\":{\"routes\":[{\"match\":[{\"host\":[\"$DOMAIN\"]}],\"handle\":[{\"handler\":\"reverse_proxy\",\"upstreams\":[{\"dial\":\"localhost:8080\"}]}]}]}}}}}" exit 0 fi exit 1 diff --git a/backend/internal/api/routes/routes_import_test.go b/backend/internal/api/routes/routes_import_test.go new file mode 100644 index 00000000..23c0ca27 --- /dev/null +++ b/backend/internal/api/routes/routes_import_test.go @@ -0,0 +1,53 @@ +package routes_test + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/routes" + "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" +) + +func setupTestImportDB(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 { + t.Fatalf("failed to connect to test database: %v", err) + } + db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}) + return db +} + +func TestRegisterImportHandler(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupTestImportDB(t) + + router := gin.New() + routes.RegisterImportHandler(router, db, "echo", "/tmp", "/import/Caddyfile") + + // Verify routes are registered by checking the routes list + routeInfo := router.Routes() + + expectedRoutes := map[string]bool{ + "GET /api/v1/import/status": false, + "GET /api/v1/import/preview": false, + "POST /api/v1/import/upload": false, + "POST /api/v1/import/commit": false, + "DELETE /api/v1/import/cancel": false, + } + + for _, route := range routeInfo { + key := route.Method + " " + route.Path + if _, exists := expectedRoutes[key]; exists { + expectedRoutes[key] = true + } + } + + for route, found := range expectedRoutes { + assert.True(t, found, "route %s should be registered", route) + } +}