"""E2E: Backup and restore flow. Tests creating entities, backing up (SQLite .db file), deleting, then restoring. """ import io class TestBackupRestoreFlow: """A user backs up their configuration and restores it.""" def _create_device(self, client, name="Backup Device") -> str: resp = client.post("/api/v1/devices", json={ "name": name, "url": "mock://backup", "device_type": "mock", "led_count": 30, }) assert resp.status_code == 201 return resp.json()["id"] def _create_css(self, client, name="Backup CSS") -> str: resp = client.post("/api/v1/color-strip-sources", json={ "name": name, "source_type": "static", "color": [255, 0, 0], "led_count": 30, }) assert resp.status_code == 201 return resp.json()["id"] def test_backup_and_restore_roundtrip(self, client): # 1. Create some entities device_id = self._create_device(client, "Device for Backup") css_id = self._create_css(client, "CSS for Backup") # Verify entities exist resp = client.get("/api/v1/devices") assert resp.json()["count"] == 1 resp = client.get("/api/v1/color-strip-sources") assert resp.json()["count"] == 1 # 2. Create a backup (GET returns a SQLite .db file) resp = client.get("/api/v1/system/backup") assert resp.status_code == 200 backup_bytes = resp.content # SQLite files start with this magic header assert backup_bytes[:16].startswith(b"SQLite format 3") # 3. Delete all created entities resp = client.delete(f"/api/v1/color-strip-sources/{css_id}") assert resp.status_code == 204 resp = client.delete(f"/api/v1/devices/{device_id}") assert resp.status_code == 204 # Verify they're gone resp = client.get("/api/v1/devices") assert resp.json()["count"] == 0 resp = client.get("/api/v1/color-strip-sources") assert resp.json()["count"] == 0 # 4. Restore from backup (POST with the .db file upload) resp = client.post( "/api/v1/system/restore", files={"file": ("backup.db", io.BytesIO(backup_bytes), "application/octet-stream")}, ) assert resp.status_code == 200, f"Restore failed: {resp.text}" restore_result = resp.json() assert restore_result["status"] == "restored" assert restore_result["restart_scheduled"] is True def test_backup_is_valid_sqlite(self, client): """Backup response is a valid SQLite database file.""" resp = client.get("/api/v1/system/backup") assert resp.status_code == 200 assert resp.content[:16].startswith(b"SQLite format 3") # Should have Content-Disposition header for download assert "attachment" in resp.headers.get("content-disposition", "") def test_restore_rejects_invalid_format(self, client): """Uploading a non-SQLite file should fail validation.""" bad_data = b"not a database file at all, just random text content" resp = client.post( "/api/v1/system/restore", files={"file": ("bad.db", io.BytesIO(bad_data), "application/octet-stream")}, ) assert resp.status_code == 400 def test_restore_rejects_empty_file(self, client): """A tiny file should fail validation.""" resp = client.post( "/api/v1/system/restore", files={"file": ("tiny.db", io.BytesIO(b"x" * 50), "application/octet-stream")}, ) assert resp.status_code == 400