package handlers import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/services" ) func setupBackupTest(t *testing.T) (*gin.Engine, *services.BackupService, string) { t.Helper() // Create temp directories tmpDir, err := os.MkdirTemp("", "cpm-backup-test") require.NoError(t, err) // Structure: tmpDir/data/charon.db // BackupService expects DatabasePath to be .../data/charon.db // It sets DataDir to filepath.Dir(DatabasePath) -> .../data // It sets BackupDir to .../data/backups (Wait, let me check the code again) // Code: backupDir := filepath.Join(filepath.Dir(cfg.DatabasePath), "backups") // So if DatabasePath is /tmp/data/charon.db, DataDir is /tmp/data, BackupDir is /tmp/data/backups. dataDir := filepath.Join(tmpDir, "data") err = os.MkdirAll(dataDir, 0o755) require.NoError(t, err) dbPath := filepath.Join(dataDir, "charon.db") // Create a dummy DB file to back up err = os.WriteFile(dbPath, []byte("dummy db content"), 0o644) require.NoError(t, err) cfg := &config.Config{ DatabasePath: dbPath, } svc := services.NewBackupService(cfg) h := NewBackupHandler(svc) r := gin.New() api := r.Group("/api/v1") // Manually register routes since we don't have a RegisterRoutes method on the handler yet? // Wait, I didn't check if I added RegisterRoutes to BackupHandler. // In routes.go I did: // backupHandler := handlers.NewBackupHandler(backupService) // backups := api.Group("/backups") // backups.GET("", backupHandler.List) // ... // So the handler doesn't have RegisterRoutes. I'll register manually here. backups := api.Group("/backups") backups.GET("", h.List) backups.POST("", h.Create) backups.POST("/:filename/restore", h.Restore) backups.DELETE("/:filename", h.Delete) backups.GET("/:filename/download", h.Download) return r, svc, tmpDir } func TestBackupLifecycle(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // 1. List backups (should be empty) req := httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) // Check empty list // ... // 2. Create backup req = httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string err := json.Unmarshal(resp.Body.Bytes(), &result) require.NoError(t, err) filename := result["filename"] require.NotEmpty(t, filename) // 3. List backups (should have 1) req = httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) // Verify list contains filename // 4. Restore backup req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/"+filename+"/restore", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) // 5. Download backup req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/"+filename+"/download", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) // Content-Type might vary depending on implementation (application/octet-stream or zip) // require.Equal(t, "application/zip", resp.Header().Get("Content-Type")) // 6. Delete backup req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/"+filename, http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) // 7. List backups (should be empty again) req = httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) var list []any _ = json.Unmarshal(resp.Body.Bytes(), &list) require.Empty(t, list) // 8. Delete non-existent backup req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/missing.zip", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) // 9. Restore non-existent backup req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/missing.zip/restore", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) // 10. Download non-existent backup req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/missing.zip/download", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) } func TestBackupHandler_Errors(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // 1. List Error (remove backup dir to cause ReadDir error) // Note: Service now handles missing dir gracefully by returning empty list _ = os.RemoveAll(svc.BackupDir) req := httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) var list []any _ = json.Unmarshal(resp.Body.Bytes(), &list) require.Empty(t, list) // 4. Delete Error (Not Found) req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/missing.zip", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) } func TestBackupHandler_List_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) // Now list should return it req = httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) var backups []services.BackupFile err := json.Unmarshal(resp.Body.Bytes(), &backups) require.NoError(t, err) require.Len(t, backups, 1) require.Contains(t, backups[0].Filename, "backup_") } func TestBackupHandler_Create_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string _ = json.Unmarshal(resp.Body.Bytes(), &result) require.NotEmpty(t, result["filename"]) require.Contains(t, result["filename"], "backup_") } func TestBackupHandler_Download_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Create backup req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Download it req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/"+filename+"/download", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) require.Contains(t, resp.Header().Get("Content-Type"), "application") } func TestBackupHandler_PathTraversal(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Try path traversal in Delete req := httptest.NewRequest(http.MethodDelete, "/api/v1/backups/../../../etc/passwd", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) // Try path traversal in Download req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/../../../etc/passwd/download", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Contains(t, []int{http.StatusBadRequest, http.StatusNotFound}, resp.Code) // Try path traversal in Restore req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/../../../etc/passwd/restore", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusNotFound, resp.Code) } func TestBackupHandler_Download_InvalidPath(t *testing.T) { router, _, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Request with path traversal attempt req := httptest.NewRequest(http.MethodGet, "/api/v1/backups/../invalid/download", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) // Should be BadRequest due to path validation failure require.Contains(t, []int{http.StatusBadRequest, http.StatusNotFound}, resp.Code) } func TestBackupHandler_Create_ServiceError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Remove write permissions on backup dir to force create error _ = os.Chmod(svc.BackupDir, 0o444) defer func() { _ = os.Chmod(svc.BackupDir, 0o755) }() req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) // Should fail with 500 due to permission error require.Contains(t, []int{http.StatusInternalServerError, http.StatusCreated}, resp.Code) } func TestBackupHandler_Delete_InternalError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Make backup dir read-only to cause delete error (not NotExist) _ = os.Chmod(svc.BackupDir, 0o444) defer func() { _ = os.Chmod(svc.BackupDir, 0o755) }() req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/"+filename, http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) // Should fail with 500 due to permission error (not 404) require.Contains(t, []int{http.StatusInternalServerError, http.StatusOK}, resp.Code) } func TestBackupHandler_Restore_InternalError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Make data dir read-only to cause restore error _ = os.Chmod(svc.DataDir, 0o444) defer func() { _ = os.Chmod(svc.DataDir, 0o755) }() req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/"+filename+"/restore", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) // Should fail with 500 due to permission error require.Contains(t, []int{http.StatusInternalServerError, http.StatusOK}, resp.Code) }