| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 | package backupimport (	"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 servicetype 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 servicefunc 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 servicefunc 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)}
 |