Changed tests for new backup module

This commit is contained in:
maziggy
2026-02-01 09:18:31 +01:00
parent 40d285db2e
commit 9bd12df6bf
4 changed files with 113 additions and 2143 deletions
+13
View File
@@ -9,6 +9,19 @@ All notable changes to Bambuddy will be documented in this file.
- **Configurable FPS**: Add `?fps=30` parameter to control camera frame rate (1-30, default 15)
- **Status-only mode**: Add `?camera=false` parameter to hide camera and show only status overlay on black background
- Increased default camera FPS from 10 to 15 for smoother video across all camera views
- **Simplified Backup/Restore System**:
- Complete backup now creates a single ZIP file containing the entire database and all data directories
- Includes: database, archives, library files, thumbnails, timelapses, icons, projects, and plate calibration data
- Portable backups: works across different installations and data directories
- Faster backup/restore: direct file copy instead of JSON export/import
- Progress indicator and navigation blocking during backup/restore operations
- Legacy JSON-based backup/restore still available via `/backup-legacy` and `/restore-legacy` endpoints
### Fixes
- **Library thumbnails missing after restore** - Fixed library files using absolute paths that break after restore on different systems:
- Library now stores relative paths in database for portability
- Automatic migration converts existing absolute paths to relative on startup
- Thumbnails and files now display correctly after restoring backups
## [0.1.6-final] - 2026-01-31
File diff suppressed because it is too large Load Diff
@@ -715,3 +715,68 @@ endsolid cube"""
file_ids = {r["file_id"] for r in result["results"]}
assert stl_without_thumb1.id in file_ids
assert stl_without_thumb2.id in file_ids
class TestLibraryPathHelpers:
"""Tests for path handling utilities used for backup portability."""
def test_to_relative_path_converts_absolute(self):
"""Verify absolute paths are converted to relative paths."""
from backend.app.api.routes.library import to_relative_path
from backend.app.core.config import settings
base_dir = str(settings.base_dir)
abs_path = f"{base_dir}/archive/library/files/test.3mf"
rel_path = to_relative_path(abs_path)
assert not rel_path.startswith("/")
assert rel_path == "archive/library/files/test.3mf"
def test_to_relative_path_handles_path_object(self):
"""Verify Path objects are handled correctly."""
from pathlib import Path
from backend.app.api.routes.library import to_relative_path
from backend.app.core.config import settings
abs_path = Path(settings.base_dir) / "archive" / "test.3mf"
rel_path = to_relative_path(abs_path)
assert not rel_path.startswith("/")
assert rel_path == "archive/test.3mf"
def test_to_relative_path_returns_empty_for_empty_input(self):
"""Verify empty input returns empty string."""
from backend.app.api.routes.library import to_relative_path
assert to_relative_path("") == ""
assert to_relative_path(None) == ""
def test_to_absolute_path_converts_relative(self):
"""Verify relative paths are converted to absolute paths."""
from backend.app.api.routes.library import to_absolute_path
from backend.app.core.config import settings
rel_path = "archive/library/files/test.3mf"
abs_path = to_absolute_path(rel_path)
assert abs_path is not None
assert abs_path.is_absolute()
assert str(abs_path) == f"{settings.base_dir}/archive/library/files/test.3mf"
def test_to_absolute_path_handles_already_absolute(self):
"""Verify already absolute paths are returned as-is (for backwards compatibility)."""
from backend.app.api.routes.library import to_absolute_path
abs_path_str = "/data/archive/test.3mf"
result = to_absolute_path(abs_path_str)
assert result is not None
assert str(result) == abs_path_str
def test_to_absolute_path_returns_none_for_empty(self):
"""Verify None/empty input returns None."""
from backend.app.api.routes.library import to_absolute_path
assert to_absolute_path(None) is None
assert to_absolute_path("") is None
+34 -69
View File
@@ -393,85 +393,50 @@ class TestSettingsAPI:
# Default is False as defined in schema
assert isinstance(result["per_printer_mapping_expanded"], bool)
# ========================================================================
# Backup/Restore tests
# ========================================================================
class TestSimplifiedBackupRestore:
"""Integration tests for the simplified backup/restore endpoints (ZIP-based).
Note: Tests that require actual file operations (backup creation) are skipped
because the test suite uses an in-memory database. These tests focus on
validation and error handling which don't require file I/O.
"""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_backup_includes_external_camera_settings(self, async_client: AsyncClient, printer_factory):
"""Verify backup includes external camera settings for printers."""
# Create a printer with external camera settings
_printer = await printer_factory(
name="Camera Test Printer",
external_camera_url="/dev/video0",
external_camera_type="usb",
external_camera_enabled=True,
)
async def test_restore_requires_zip_file(self, async_client: AsyncClient):
"""Verify restore rejects non-ZIP files."""
files = {"file": ("backup.txt", b"not a zip file", "text/plain")}
response = await async_client.post("/api/v1/settings/restore", files=files)
# Request backup with printers
response = await async_client.get("/api/v1/settings/backup?include_printers=true")
assert response.status_code == 200
backup = response.json()
# Find the printer in the backup
assert "printers" in backup
printer_data = next((p for p in backup["printers"] if p["name"] == "Camera Test Printer"), None)
assert printer_data is not None
# Verify external camera fields are included
assert "external_camera_url" in printer_data
assert "external_camera_type" in printer_data
assert "external_camera_enabled" in printer_data
assert printer_data["external_camera_url"] == "/dev/video0"
assert printer_data["external_camera_type"] == "usb"
assert printer_data["external_camera_enabled"] is True
assert response.status_code == 400
assert "zip" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_restore_external_camera_settings_overwrite(self, async_client: AsyncClient, printer_factory):
"""Verify restore with overwrite updates external camera settings."""
async def test_restore_requires_database_in_zip(self, async_client: AsyncClient):
"""Verify restore rejects ZIP without database file."""
import io
import zipfile
# Create a printer without camera settings
printer = await printer_factory(
name="Restore Test",
external_camera_url=None,
external_camera_type=None,
external_camera_enabled=False,
)
# Create a ZIP without bambuddy.db
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("dummy.txt", "dummy content")
zip_buffer.seek(0)
# Create backup data with camera settings
backup_data = {
"version": "1.0",
"included": ["printers"],
"printers": [
{
"name": "Restore Test",
"serial_number": printer.serial_number,
"ip_address": printer.ip_address,
"external_camera_url": "/dev/video1",
"external_camera_type": "usb",
"external_camera_enabled": True,
}
],
}
files = {"file": ("backup.zip", zip_buffer.read(), "application/zip")}
response = await async_client.post("/api/v1/settings/restore", files=files)
# Restore with overwrite
import json
assert response.status_code == 400
assert "missing bambuddy.db" in response.json()["detail"].lower()
files = {"file": ("backup.json", io.BytesIO(json.dumps(backup_data).encode()), "application/json")}
response = await async_client.post("/api/v1/settings/restore?overwrite=true", files=files)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_restore_invalid_zip(self, async_client: AsyncClient):
"""Verify restore rejects corrupted ZIP files."""
files = {"file": ("backup.zip", b"not valid zip content", "application/zip")}
response = await async_client.post("/api/v1/settings/restore", files=files)
assert response.status_code == 200
result = response.json()
assert result["success"] is True
# Verify the printer was updated
response = await async_client.get(f"/api/v1/printers/{printer.id}")
assert response.status_code == 200
updated_printer = response.json()
assert updated_printer["external_camera_url"] == "/dev/video1"
assert updated_printer["external_camera_type"] == "usb"
assert updated_printer["external_camera_enabled"] is True
assert response.status_code == 400
assert "not a valid zip" in response.json()["detail"].lower()