123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- package system
- import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "io"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "github.com/0xJacky/Nginx-UI/internal/backup"
- "github.com/0xJacky/Nginx-UI/settings"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/uozi-tech/cosy/logger"
- cosysettings "github.com/uozi-tech/cosy/settings"
- )
- // MockBackupService is used to mock the backup service
- type MockBackupService struct {
- mock.Mock
- }
- func (m *MockBackupService) Backup() (backup.BackupResult, error) {
- return backup.BackupResult{
- BackupName: "backup-test.zip",
- AESKey: "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=", // base64 encoded test key
- AESIv: "YWJjZGVmZ2hpamtsbW5vcA==", // base64 encoded test IV
- BackupContent: []byte("test backup content"),
- }, nil
- }
- func (m *MockBackupService) Restore(options backup.RestoreOptions) (backup.RestoreResult, error) {
- return backup.RestoreResult{
- RestoreDir: options.RestoreDir,
- NginxUIRestored: options.RestoreNginxUI,
- NginxRestored: options.RestoreNginx,
- HashMatch: options.VerifyHash,
- }, nil
- }
- // MockedCreateBackup is a mocked version of CreateBackup that uses the mock service
- func MockedCreateBackup(c *gin.Context) {
- mockService := &MockBackupService{}
- result, err := mockService.Backup()
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{
- "error": err.Error(),
- })
- return
- }
- // Concatenate Key and IV
- securityToken := result.AESKey + ":" + result.AESIv
- // Set HTTP headers for file download
- fileName := result.BackupName
- c.Header("Content-Description", "File Transfer")
- c.Header("Content-Type", "application/zip")
- c.Header("Content-Disposition", "attachment; filename="+fileName)
- c.Header("Content-Transfer-Encoding", "binary")
- c.Header("X-Backup-Security", securityToken) // Pass security token in header
- c.Header("Expires", "0")
- c.Header("Cache-Control", "must-revalidate")
- c.Header("Pragma", "public")
- // Send file content
- c.Data(http.StatusOK, "application/zip", result.BackupContent)
- }
- // MockedRestoreBackup is a mocked version of RestoreBackup that uses the mock service
- func MockedRestoreBackup(c *gin.Context) {
- // Get restore options
- restoreNginx := c.PostForm("restore_nginx") == "true"
- restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
- verifyHash := c.PostForm("verify_hash") == "true"
- securityToken := c.PostForm("security_token")
- // Get backup file - we're just checking it exists for the test
- _, err := c.FormFile("backup_file")
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{
- "error": "Backup file not found",
- })
- return
- }
- // Validate security token
- if securityToken == "" {
- c.JSON(http.StatusBadRequest, gin.H{
- "error": "Invalid security token",
- })
- return
- }
- // Split security token to get Key and IV
- parts := strings.Split(securityToken, ":")
- if len(parts) != 2 {
- c.JSON(http.StatusBadRequest, gin.H{
- "error": "Invalid security token format",
- })
- return
- }
- // Create temporary directory
- tempDir, err := os.MkdirTemp("", "nginx-ui-restore-test-*")
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{
- "error": "Failed to create temporary directory",
- })
- return
- }
- mockService := &MockBackupService{}
- result, err := mockService.Restore(backup.RestoreOptions{
- RestoreDir: tempDir,
- RestoreNginx: restoreNginx,
- RestoreNginxUI: restoreNginxUI,
- VerifyHash: verifyHash,
- })
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{
- "error": err.Error(),
- })
- return
- }
- c.JSON(http.StatusOK, RestoreResponse{
- NginxUIRestored: result.NginxUIRestored,
- NginxRestored: result.NginxRestored,
- HashMatch: result.HashMatch,
- })
- }
- func TestSetupEnvironment(t *testing.T) {
- logger.Init(gin.DebugMode)
- // Set up test environment
- tempDir, err := os.MkdirTemp("", "nginx-ui-test-*")
- assert.NoError(t, err)
- defer os.RemoveAll(tempDir)
- // Set up necessary directories and config files
- nginxDir := filepath.Join(tempDir, "nginx")
- configDir := filepath.Join(tempDir, "config")
- err = os.MkdirAll(nginxDir, 0755)
- assert.NoError(t, err)
- err = os.MkdirAll(configDir, 0755)
- assert.NoError(t, err)
- // Create a config.ini file
- configPath := filepath.Join(configDir, "config.ini")
- err = os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
- assert.NoError(t, err)
- // Create a database file
- dbName := settings.DatabaseSettings.GetName()
- dbPath := filepath.Join(configDir, dbName+".db")
- err = os.WriteFile(dbPath, []byte("test database content"), 0644)
- assert.NoError(t, err)
- // Save original settings for restoration later
- originalConfigDir := settings.NginxSettings.ConfigDir
- originalConfPath := cosysettings.ConfPath
- t.Logf("Original config path: %s", cosysettings.ConfPath)
- t.Logf("Setting config path to: %s", configPath)
- // Set the temporary directory as the Nginx config directory for testing
- settings.NginxSettings.ConfigDir = nginxDir
- cosysettings.ConfPath = configPath
- t.Logf("Config path after setting: %s", cosysettings.ConfPath)
- // Restore original settings after test
- defer func() {
- settings.NginxSettings.ConfigDir = originalConfigDir
- cosysettings.ConfPath = originalConfPath
- }()
- }
- func setupMockedRouter() *gin.Engine {
- gin.SetMode(gin.TestMode)
- r := gin.New()
- // Setup router with mocked API endpoints to avoid environment issues
- systemGroup := r.Group("/api/system")
- systemGroup.POST("/backup", MockedCreateBackup)
- systemGroup.POST("/backup/restore", MockedRestoreBackup)
- return r
- }
- func TestCreateBackupAPI(t *testing.T) {
- // Set up test environment
- TestSetupEnvironment(t)
- router := setupMockedRouter()
- w := httptest.NewRecorder()
- req, _ := http.NewRequest("POST", "/api/system/backup", nil)
- router.ServeHTTP(w, req)
- // If there's an error, it might be because the config path is empty
- if w.Code != http.StatusOK {
- var errorResponse map[string]interface{}
- err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
- if err == nil {
- t.Logf("Error response: %v", errorResponse)
- }
- // Skip the test if there's a configuration issue
- if strings.Contains(w.Body.String(), "Config path is empty") {
- t.Skip("Skipping test due to empty config path")
- return
- }
- }
- // Check response code - should be OK
- assert.Equal(t, http.StatusOK, w.Code)
- // Verify the backup API response
- assert.Equal(t, "application/zip", w.Header().Get("Content-Type"))
- // Check that Content-Disposition contains "attachment; filename=backup-"
- contentDisposition := w.Header().Get("Content-Disposition")
- assert.True(t, strings.HasPrefix(contentDisposition, "attachment; filename=backup-"),
- "Content-Disposition should start with 'attachment; filename=backup-'")
- assert.NotEmpty(t, w.Header().Get("X-Backup-Security"))
- assert.NotEmpty(t, w.Body.Bytes())
- // Verify security token format
- securityToken := w.Header().Get("X-Backup-Security")
- parts := bytes.Split([]byte(securityToken), []byte(":"))
- assert.Equal(t, 2, len(parts))
- // Verify key and IV can be decoded
- key, err := base64.StdEncoding.DecodeString(string(parts[0]))
- assert.NoError(t, err)
- assert.Equal(t, 32, len(key))
- iv, err := base64.StdEncoding.DecodeString(string(parts[1]))
- assert.NoError(t, err)
- assert.Equal(t, 16, len(iv))
- }
- func TestRestoreBackupAPI(t *testing.T) {
- // Set up test environment
- TestSetupEnvironment(t)
- // First create a backup to restore
- backupRouter := setupMockedRouter()
- w1 := httptest.NewRecorder()
- req1, _ := http.NewRequest("POST", "/api/system/backup", nil)
- backupRouter.ServeHTTP(w1, req1)
- // If there's an error creating the backup, skip the test
- if w1.Code != http.StatusOK {
- var errorResponse map[string]interface{}
- err := json.Unmarshal(w1.Body.Bytes(), &errorResponse)
- if err == nil {
- t.Logf("Error response during backup creation: %v", errorResponse)
- }
- t.Skip("Skipping test due to backup creation failure")
- return
- }
- assert.Equal(t, http.StatusOK, w1.Code)
- // Get the security token from the backup response
- securityToken := w1.Header().Get("X-Backup-Security")
- assert.NotEmpty(t, securityToken)
- // Get backup content
- backupContent := w1.Body.Bytes()
- assert.NotEmpty(t, backupContent)
- // Setup temporary directory and save backup file
- tempDir, err := os.MkdirTemp("", "restore-api-test-*")
- assert.NoError(t, err)
- defer os.RemoveAll(tempDir)
- backupName := "backup-test.zip"
- backupPath := filepath.Join(tempDir, backupName)
- err = os.WriteFile(backupPath, backupContent, 0644)
- assert.NoError(t, err)
- // Setup router
- router := setupMockedRouter()
- // Create multipart form
- body := new(bytes.Buffer)
- writer := multipart.NewWriter(body)
- // Add form fields
- _ = writer.WriteField("restore_nginx", "false")
- _ = writer.WriteField("restore_nginx_ui", "false")
- _ = writer.WriteField("verify_hash", "true")
- _ = writer.WriteField("security_token", securityToken)
- // Add backup file
- file, err := os.Open(backupPath)
- assert.NoError(t, err)
- defer file.Close()
- part, err := writer.CreateFormFile("backup_file", backupName)
- assert.NoError(t, err)
- _, err = io.Copy(part, file)
- assert.NoError(t, err)
- err = writer.Close()
- assert.NoError(t, err)
- // Create request
- w := httptest.NewRecorder()
- req, _ := http.NewRequest("POST", "/api/system/backup/restore", body)
- req.Header.Set("Content-Type", writer.FormDataContentType())
- // Perform request
- router.ServeHTTP(w, req)
- // Check status code
- t.Logf("Response: %s", w.Body.String())
- assert.Equal(t, http.StatusOK, w.Code)
- // Verify response structure
- var response RestoreResponse
- err = json.Unmarshal(w.Body.Bytes(), &response)
- assert.NoError(t, err)
- assert.Equal(t, false, response.NginxUIRestored)
- assert.Equal(t, false, response.NginxRestored)
- assert.Equal(t, true, response.HashMatch)
- }
- func TestRestoreBackupAPIErrors(t *testing.T) {
- // Set up test environment
- TestSetupEnvironment(t)
- // Setup router
- router := setupMockedRouter()
- // Test case 1: Missing backup file
- w1 := httptest.NewRecorder()
- body1 := new(bytes.Buffer)
- writer1 := multipart.NewWriter(body1)
- _ = writer1.WriteField("security_token", "invalid:token")
- writer1.Close()
- req1, _ := http.NewRequest("POST", "/api/system/backup/restore", body1)
- req1.Header.Set("Content-Type", writer1.FormDataContentType())
- router.ServeHTTP(w1, req1)
- assert.NotEqual(t, http.StatusOK, w1.Code)
- // Test case 2: Invalid security token
- w2 := httptest.NewRecorder()
- body2 := new(bytes.Buffer)
- writer2 := multipart.NewWriter(body2)
- _ = writer2.WriteField("security_token", "invalidtoken") // No colon separator
- writer2.Close()
- req2, _ := http.NewRequest("POST", "/api/system/backup/restore", body2)
- req2.Header.Set("Content-Type", writer2.FormDataContentType())
- router.ServeHTTP(w2, req2)
- assert.NotEqual(t, http.StatusOK, w2.Code)
- // Test case 3: Invalid base64 encoding
- w3 := httptest.NewRecorder()
- body3 := new(bytes.Buffer)
- writer3 := multipart.NewWriter(body3)
- _ = writer3.WriteField("security_token", "invalid!base64:alsoinvalid!")
- writer3.Close()
- req3, _ := http.NewRequest("POST", "/api/system/backup/restore", body3)
- req3.Header.Set("Content-Type", writer3.FormDataContentType())
- router.ServeHTTP(w3, req3)
- assert.NotEqual(t, http.StatusOK, w3.Code)
- }
|