feat: add transient import preview and commit functionality with tests
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user