feat: add transient import preview and commit functionality with tests

This commit is contained in:
Wikid82
2025-11-24 18:14:59 +00:00
parent a698dff33a
commit af5a0b4ef8
4 changed files with 285 additions and 32 deletions
+29 -31
View File
@@ -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.
@@ -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)
+6 -1
View File
@@ -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
@@ -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)
}
}