backup_handlers_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package api
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "io"
  7. "mime/multipart"
  8. "net/http"
  9. "net/http/httptest"
  10. "os"
  11. "path/filepath"
  12. "testing"
  13. "debtik/internal/auth"
  14. "debtik/internal/backup"
  15. )
  16. func TestBackupAPIHandlers(t *testing.T) {
  17. tempDir := t.TempDir()
  18. debtikDir := filepath.Join(tempDir, "debtik")
  19. netDir := filepath.Join(tempDir, "network")
  20. storeDir := filepath.Join(tempDir, "backups")
  21. _ = os.MkdirAll(debtikDir, 0755)
  22. _ = os.MkdirAll(netDir, 0755)
  23. _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), []byte(`{"router": "test"}`), 0644)
  24. _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), []byte(`auto lo`), 0644)
  25. backupMgr := backup.NewManager(debtikDir, netDir, storeDir, nil)
  26. s := &Server{
  27. backupMgr: backupMgr,
  28. mux: http.NewServeMux(),
  29. fsHandler: http.NotFoundHandler(),
  30. }
  31. s.registerRoutes()
  32. // 1. Test Backup Export (Download)
  33. req := httptest.NewRequest(http.MethodGet, "/api/system/backup/export?comment=APITest", nil)
  34. // Context with full admin role
  35. ctx := context.WithValue(req.Context(), sessionContextKey, &auth.Session{
  36. Username: "admin",
  37. Role: auth.RoleFull,
  38. })
  39. req = req.WithContext(ctx)
  40. rec := httptest.NewRecorder()
  41. s.mux.ServeHTTP(rec, req)
  42. if rec.Code != http.StatusOK {
  43. t.Fatalf("Expected 200 OK from backup export, got %d: %s", rec.Code, rec.Body.String())
  44. }
  45. if rec.Header().Get("Content-Type") != "application/zip" {
  46. t.Errorf("Expected application/zip Content-Type, got %s", rec.Header().Get("Content-Type"))
  47. }
  48. zipBytes := rec.Body.Bytes()
  49. if len(zipBytes) == 0 {
  50. t.Fatal("Exported zip is empty")
  51. }
  52. // 2. Test Backup Inspect via multipart upload
  53. bodyBuf := new(bytes.Buffer)
  54. mpWriter := multipart.NewWriter(bodyBuf)
  55. part, err := mpWriter.CreateFormFile("backup", "test-backup.zip")
  56. if err != nil {
  57. t.Fatal(err)
  58. }
  59. _, _ = io.Copy(part, bytes.NewReader(zipBytes))
  60. _ = mpWriter.Close()
  61. inspectReq := httptest.NewRequest(http.MethodPost, "/api/system/backup/inspect", bodyBuf)
  62. inspectReq.Header.Set("Content-Type", mpWriter.FormDataContentType())
  63. inspectReq = inspectReq.WithContext(ctx)
  64. inspectRec := httptest.NewRecorder()
  65. s.mux.ServeHTTP(inspectRec, inspectReq)
  66. if inspectRec.Code != http.StatusOK {
  67. t.Fatalf("Expected 200 OK from backup inspect, got %d: %s", inspectRec.Code, inspectRec.Body.String())
  68. }
  69. var inspectResp struct {
  70. Success bool `json:"success"`
  71. Manifest *backup.Manifest `json:"manifest"`
  72. }
  73. if err := json.Unmarshal(inspectRec.Body.Bytes(), &inspectResp); err != nil {
  74. t.Fatalf("Failed unmarshaling inspect response: %v", err)
  75. }
  76. if !inspectResp.Success || inspectResp.Manifest == nil {
  77. t.Fatal("Expected success and manifest in inspect response")
  78. }
  79. if inspectResp.Manifest.Comment != "APITest" {
  80. t.Errorf("Manifest comment mismatch: %s != APITest", inspectResp.Manifest.Comment)
  81. }
  82. // 3. Test Backup Restore
  83. restoreBuf := new(bytes.Buffer)
  84. mpWriter2 := multipart.NewWriter(restoreBuf)
  85. part2, _ := mpWriter2.CreateFormFile("backup", "restore-backup.zip")
  86. _, _ = io.Copy(part2, bytes.NewReader(zipBytes))
  87. _ = mpWriter2.Close()
  88. restoreReq := httptest.NewRequest(http.MethodPost, "/api/system/backup/restore", restoreBuf)
  89. restoreReq.Header.Set("Content-Type", mpWriter2.FormDataContentType())
  90. restoreReq = restoreReq.WithContext(ctx)
  91. restoreRec := httptest.NewRecorder()
  92. s.mux.ServeHTTP(restoreRec, restoreReq)
  93. if restoreRec.Code != http.StatusOK {
  94. t.Fatalf("Expected 200 OK from backup restore, got %d: %s", restoreRec.Code, restoreRec.Body.String())
  95. }
  96. var restoreRes backup.RestoreResult
  97. if err := json.Unmarshal(restoreRec.Body.Bytes(), &restoreRes); err != nil {
  98. t.Fatalf("Failed unmarshaling restore response: %v", err)
  99. }
  100. if !restoreRes.Success {
  101. t.Error("Expected restore success true")
  102. }
  103. // 4. Test Backup List (should contain pre-restore safety backup)
  104. listReq := httptest.NewRequest(http.MethodGet, "/api/system/backups", nil)
  105. listReq = listReq.WithContext(ctx)
  106. listRec := httptest.NewRecorder()
  107. s.mux.ServeHTTP(listRec, listReq)
  108. if listRec.Code != http.StatusOK {
  109. t.Fatalf("Expected 200 OK from backup list, got %d: %s", listRec.Code, listRec.Body.String())
  110. }
  111. var backupsList []backup.BackupInfo
  112. if err := json.Unmarshal(listRec.Body.Bytes(), &backupsList); err != nil {
  113. t.Fatalf("Failed unmarshaling backup list: %v", err)
  114. }
  115. if len(backupsList) == 0 {
  116. t.Error("Expected at least 1 backup in list")
  117. }
  118. // 5. Test Forbidden for Read-Only user
  119. roCtx := context.WithValue(req.Context(), sessionContextKey, &auth.Session{
  120. Username: "readonly-user",
  121. Role: auth.RoleRead,
  122. })
  123. roReq := httptest.NewRequest(http.MethodGet, "/api/system/backup/export", nil)
  124. roReq = roReq.WithContext(roCtx)
  125. roRec := httptest.NewRecorder()
  126. s.mux.ServeHTTP(roRec, roReq)
  127. if roRec.Code != http.StatusForbidden {
  128. t.Errorf("Expected 403 Forbidden for read-only user, got %d", roRec.Code)
  129. }
  130. }