Some checks failed
Lint & Test / test (push) Failing after 1m33s
All store tests were passing file paths instead of Database objects after the JSON-to-SQLite migration. Updated fixtures to create temp Database instances, rewrote backup e2e tests for binary .db format, and fixed config tests for the simplified StorageConfig.
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""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
|