mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
Changed tests for new backup module
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user