backup_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package system
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "io"
  7. "mime/multipart"
  8. "net/http"
  9. "net/http/httptest"
  10. "os"
  11. "path/filepath"
  12. "strings"
  13. "testing"
  14. "github.com/0xJacky/Nginx-UI/internal/backup"
  15. "github.com/0xJacky/Nginx-UI/settings"
  16. "github.com/gin-gonic/gin"
  17. "github.com/stretchr/testify/assert"
  18. "github.com/stretchr/testify/mock"
  19. "github.com/uozi-tech/cosy/logger"
  20. cosysettings "github.com/uozi-tech/cosy/settings"
  21. )
  22. // MockBackupService is used to mock the backup service
  23. type MockBackupService struct {
  24. mock.Mock
  25. }
  26. func (m *MockBackupService) Backup() (backup.BackupResult, error) {
  27. return backup.BackupResult{
  28. BackupName: "backup-test.zip",
  29. AESKey: "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=", // base64 encoded test key
  30. AESIv: "YWJjZGVmZ2hpamtsbW5vcA==", // base64 encoded test IV
  31. BackupContent: []byte("test backup content"),
  32. }, nil
  33. }
  34. func (m *MockBackupService) Restore(options backup.RestoreOptions) (backup.RestoreResult, error) {
  35. return backup.RestoreResult{
  36. RestoreDir: options.RestoreDir,
  37. NginxUIRestored: options.RestoreNginxUI,
  38. NginxRestored: options.RestoreNginx,
  39. HashMatch: options.VerifyHash,
  40. }, nil
  41. }
  42. // MockedCreateBackup is a mocked version of CreateBackup that uses the mock service
  43. func MockedCreateBackup(c *gin.Context) {
  44. mockService := &MockBackupService{}
  45. result, err := mockService.Backup()
  46. if err != nil {
  47. c.JSON(http.StatusInternalServerError, gin.H{
  48. "error": err.Error(),
  49. })
  50. return
  51. }
  52. // Concatenate Key and IV
  53. securityToken := result.AESKey + ":" + result.AESIv
  54. // Set HTTP headers for file download
  55. fileName := result.BackupName
  56. c.Header("Content-Description", "File Transfer")
  57. c.Header("Content-Type", "application/zip")
  58. c.Header("Content-Disposition", "attachment; filename="+fileName)
  59. c.Header("Content-Transfer-Encoding", "binary")
  60. c.Header("X-Backup-Security", securityToken) // Pass security token in header
  61. c.Header("Expires", "0")
  62. c.Header("Cache-Control", "must-revalidate")
  63. c.Header("Pragma", "public")
  64. // Send file content
  65. c.Data(http.StatusOK, "application/zip", result.BackupContent)
  66. }
  67. // MockedRestoreBackup is a mocked version of RestoreBackup that uses the mock service
  68. func MockedRestoreBackup(c *gin.Context) {
  69. // Get restore options
  70. restoreNginx := c.PostForm("restore_nginx") == "true"
  71. restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
  72. verifyHash := c.PostForm("verify_hash") == "true"
  73. securityToken := c.PostForm("security_token")
  74. // Get backup file - we're just checking it exists for the test
  75. _, err := c.FormFile("backup_file")
  76. if err != nil {
  77. c.JSON(http.StatusBadRequest, gin.H{
  78. "error": "Backup file not found",
  79. })
  80. return
  81. }
  82. // Validate security token
  83. if securityToken == "" {
  84. c.JSON(http.StatusBadRequest, gin.H{
  85. "error": "Invalid security token",
  86. })
  87. return
  88. }
  89. // Split security token to get Key and IV
  90. parts := strings.Split(securityToken, ":")
  91. if len(parts) != 2 {
  92. c.JSON(http.StatusBadRequest, gin.H{
  93. "error": "Invalid security token format",
  94. })
  95. return
  96. }
  97. // Create temporary directory
  98. tempDir, err := os.MkdirTemp("", "nginx-ui-restore-test-*")
  99. if err != nil {
  100. c.JSON(http.StatusInternalServerError, gin.H{
  101. "error": "Failed to create temporary directory",
  102. })
  103. return
  104. }
  105. mockService := &MockBackupService{}
  106. result, err := mockService.Restore(backup.RestoreOptions{
  107. RestoreDir: tempDir,
  108. RestoreNginx: restoreNginx,
  109. RestoreNginxUI: restoreNginxUI,
  110. VerifyHash: verifyHash,
  111. })
  112. if err != nil {
  113. c.JSON(http.StatusInternalServerError, gin.H{
  114. "error": err.Error(),
  115. })
  116. return
  117. }
  118. c.JSON(http.StatusOK, RestoreResponse{
  119. NginxUIRestored: result.NginxUIRestored,
  120. NginxRestored: result.NginxRestored,
  121. HashMatch: result.HashMatch,
  122. })
  123. }
  124. func TestSetupEnvironment(t *testing.T) {
  125. logger.Init(gin.DebugMode)
  126. // Set up test environment
  127. tempDir, err := os.MkdirTemp("", "nginx-ui-test-*")
  128. assert.NoError(t, err)
  129. defer os.RemoveAll(tempDir)
  130. // Set up necessary directories and config files
  131. nginxDir := filepath.Join(tempDir, "nginx")
  132. configDir := filepath.Join(tempDir, "config")
  133. err = os.MkdirAll(nginxDir, 0755)
  134. assert.NoError(t, err)
  135. err = os.MkdirAll(configDir, 0755)
  136. assert.NoError(t, err)
  137. // Create a config.ini file
  138. configPath := filepath.Join(configDir, "config.ini")
  139. err = os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
  140. assert.NoError(t, err)
  141. // Create a database file
  142. dbName := settings.DatabaseSettings.GetName()
  143. dbPath := filepath.Join(configDir, dbName+".db")
  144. err = os.WriteFile(dbPath, []byte("test database content"), 0644)
  145. assert.NoError(t, err)
  146. // Save original settings for restoration later
  147. originalConfigDir := settings.NginxSettings.ConfigDir
  148. originalConfPath := cosysettings.ConfPath
  149. t.Logf("Original config path: %s", cosysettings.ConfPath)
  150. t.Logf("Setting config path to: %s", configPath)
  151. // Set the temporary directory as the Nginx config directory for testing
  152. settings.NginxSettings.ConfigDir = nginxDir
  153. cosysettings.ConfPath = configPath
  154. t.Logf("Config path after setting: %s", cosysettings.ConfPath)
  155. // Restore original settings after test
  156. defer func() {
  157. settings.NginxSettings.ConfigDir = originalConfigDir
  158. cosysettings.ConfPath = originalConfPath
  159. }()
  160. }
  161. func setupMockedRouter() *gin.Engine {
  162. gin.SetMode(gin.TestMode)
  163. r := gin.New()
  164. // Setup router with mocked API endpoints to avoid environment issues
  165. systemGroup := r.Group("/api/system")
  166. systemGroup.POST("/backup", MockedCreateBackup)
  167. systemGroup.POST("/backup/restore", MockedRestoreBackup)
  168. return r
  169. }
  170. func TestCreateBackupAPI(t *testing.T) {
  171. // Set up test environment
  172. TestSetupEnvironment(t)
  173. router := setupMockedRouter()
  174. w := httptest.NewRecorder()
  175. req, _ := http.NewRequest("POST", "/api/system/backup", nil)
  176. router.ServeHTTP(w, req)
  177. // If there's an error, it might be because the config path is empty
  178. if w.Code != http.StatusOK {
  179. var errorResponse map[string]interface{}
  180. err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
  181. if err == nil {
  182. t.Logf("Error response: %v", errorResponse)
  183. }
  184. // Skip the test if there's a configuration issue
  185. if strings.Contains(w.Body.String(), "Config path is empty") {
  186. t.Skip("Skipping test due to empty config path")
  187. return
  188. }
  189. }
  190. // Check response code - should be OK
  191. assert.Equal(t, http.StatusOK, w.Code)
  192. // Verify the backup API response
  193. assert.Equal(t, "application/zip", w.Header().Get("Content-Type"))
  194. // Check that Content-Disposition contains "attachment; filename=backup-"
  195. contentDisposition := w.Header().Get("Content-Disposition")
  196. assert.True(t, strings.HasPrefix(contentDisposition, "attachment; filename=backup-"),
  197. "Content-Disposition should start with 'attachment; filename=backup-'")
  198. assert.NotEmpty(t, w.Header().Get("X-Backup-Security"))
  199. assert.NotEmpty(t, w.Body.Bytes())
  200. // Verify security token format
  201. securityToken := w.Header().Get("X-Backup-Security")
  202. parts := bytes.Split([]byte(securityToken), []byte(":"))
  203. assert.Equal(t, 2, len(parts))
  204. // Verify key and IV can be decoded
  205. key, err := base64.StdEncoding.DecodeString(string(parts[0]))
  206. assert.NoError(t, err)
  207. assert.Equal(t, 32, len(key))
  208. iv, err := base64.StdEncoding.DecodeString(string(parts[1]))
  209. assert.NoError(t, err)
  210. assert.Equal(t, 16, len(iv))
  211. }
  212. func TestRestoreBackupAPI(t *testing.T) {
  213. // Set up test environment
  214. TestSetupEnvironment(t)
  215. // First create a backup to restore
  216. backupRouter := setupMockedRouter()
  217. w1 := httptest.NewRecorder()
  218. req1, _ := http.NewRequest("POST", "/api/system/backup", nil)
  219. backupRouter.ServeHTTP(w1, req1)
  220. // If there's an error creating the backup, skip the test
  221. if w1.Code != http.StatusOK {
  222. var errorResponse map[string]interface{}
  223. err := json.Unmarshal(w1.Body.Bytes(), &errorResponse)
  224. if err == nil {
  225. t.Logf("Error response during backup creation: %v", errorResponse)
  226. }
  227. t.Skip("Skipping test due to backup creation failure")
  228. return
  229. }
  230. assert.Equal(t, http.StatusOK, w1.Code)
  231. // Get the security token from the backup response
  232. securityToken := w1.Header().Get("X-Backup-Security")
  233. assert.NotEmpty(t, securityToken)
  234. // Get backup content
  235. backupContent := w1.Body.Bytes()
  236. assert.NotEmpty(t, backupContent)
  237. // Setup temporary directory and save backup file
  238. tempDir, err := os.MkdirTemp("", "restore-api-test-*")
  239. assert.NoError(t, err)
  240. defer os.RemoveAll(tempDir)
  241. backupName := "backup-test.zip"
  242. backupPath := filepath.Join(tempDir, backupName)
  243. err = os.WriteFile(backupPath, backupContent, 0644)
  244. assert.NoError(t, err)
  245. // Setup router
  246. router := setupMockedRouter()
  247. // Create multipart form
  248. body := new(bytes.Buffer)
  249. writer := multipart.NewWriter(body)
  250. // Add form fields
  251. _ = writer.WriteField("restore_nginx", "false")
  252. _ = writer.WriteField("restore_nginx_ui", "false")
  253. _ = writer.WriteField("verify_hash", "true")
  254. _ = writer.WriteField("security_token", securityToken)
  255. // Add backup file
  256. file, err := os.Open(backupPath)
  257. assert.NoError(t, err)
  258. defer file.Close()
  259. part, err := writer.CreateFormFile("backup_file", backupName)
  260. assert.NoError(t, err)
  261. _, err = io.Copy(part, file)
  262. assert.NoError(t, err)
  263. err = writer.Close()
  264. assert.NoError(t, err)
  265. // Create request
  266. w := httptest.NewRecorder()
  267. req, _ := http.NewRequest("POST", "/api/system/backup/restore", body)
  268. req.Header.Set("Content-Type", writer.FormDataContentType())
  269. // Perform request
  270. router.ServeHTTP(w, req)
  271. // Check status code
  272. t.Logf("Response: %s", w.Body.String())
  273. assert.Equal(t, http.StatusOK, w.Code)
  274. // Verify response structure
  275. var response RestoreResponse
  276. err = json.Unmarshal(w.Body.Bytes(), &response)
  277. assert.NoError(t, err)
  278. assert.Equal(t, false, response.NginxUIRestored)
  279. assert.Equal(t, false, response.NginxRestored)
  280. assert.Equal(t, true, response.HashMatch)
  281. }
  282. func TestRestoreBackupAPIErrors(t *testing.T) {
  283. // Set up test environment
  284. TestSetupEnvironment(t)
  285. // Setup router
  286. router := setupMockedRouter()
  287. // Test case 1: Missing backup file
  288. w1 := httptest.NewRecorder()
  289. body1 := new(bytes.Buffer)
  290. writer1 := multipart.NewWriter(body1)
  291. _ = writer1.WriteField("security_token", "invalid:token")
  292. writer1.Close()
  293. req1, _ := http.NewRequest("POST", "/api/system/backup/restore", body1)
  294. req1.Header.Set("Content-Type", writer1.FormDataContentType())
  295. router.ServeHTTP(w1, req1)
  296. assert.NotEqual(t, http.StatusOK, w1.Code)
  297. // Test case 2: Invalid security token
  298. w2 := httptest.NewRecorder()
  299. body2 := new(bytes.Buffer)
  300. writer2 := multipart.NewWriter(body2)
  301. _ = writer2.WriteField("security_token", "invalidtoken") // No colon separator
  302. writer2.Close()
  303. req2, _ := http.NewRequest("POST", "/api/system/backup/restore", body2)
  304. req2.Header.Set("Content-Type", writer2.FormDataContentType())
  305. router.ServeHTTP(w2, req2)
  306. assert.NotEqual(t, http.StatusOK, w2.Code)
  307. // Test case 3: Invalid base64 encoding
  308. w3 := httptest.NewRecorder()
  309. body3 := new(bytes.Buffer)
  310. writer3 := multipart.NewWriter(body3)
  311. _ = writer3.WriteField("security_token", "invalid!base64:alsoinvalid!")
  312. writer3.Close()
  313. req3, _ := http.NewRequest("POST", "/api/system/backup/restore", body3)
  314. req3.Header.Set("Content-Type", writer3.FormDataContentType())
  315. router.ServeHTTP(w3, req3)
  316. assert.NotEqual(t, http.StatusOK, w3.Code)
  317. }