mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
- Add Virtual Printer feature - emulate Bambu printer on network
- Virtual printer appears in Bambu Studio/Orca Slicer via SSDP discovery - Secure TLS/MQTT communication with auto-generated certificates - Queue mode (pending uploads) or auto-start mode - Configurable access code for authentication - Docker support with network_mode: host and certificate persistence - Fix backup/restore for virtual printer settings (auto-save no longer overwrites)
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
"""Integration tests for Virtual Printer API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/settings/virtual-printer endpoints.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestVirtualPrinterSettingsAPI:
|
||||
"""Integration tests for /api/v1/settings/virtual-printer endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# Get settings
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_virtual_printer_settings(self, async_client: AsyncClient):
|
||||
"""Verify virtual printer settings can be retrieved."""
|
||||
response = await async_client.get("/api/v1/settings/virtual-printer")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "enabled" in result
|
||||
assert "access_code_set" in result
|
||||
assert "mode" in result
|
||||
assert "status" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_settings_has_status(self, async_client: AsyncClient):
|
||||
"""Verify settings include status details."""
|
||||
response = await async_client.get("/api/v1/settings/virtual-printer")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
status = result["status"]
|
||||
assert "enabled" in status
|
||||
assert "running" in status
|
||||
assert "mode" in status
|
||||
assert "name" in status
|
||||
assert "serial" in status
|
||||
assert "pending_files" in status
|
||||
|
||||
# ========================================================================
|
||||
# Update settings
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode(self, async_client: AsyncClient):
|
||||
"""Verify mode can be updated."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=queue")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "queue"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_to_immediate(self, async_client: AsyncClient):
|
||||
"""Verify mode can be set to immediate."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=immediate")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "immediate"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_access_code(self, async_client: AsyncClient):
|
||||
"""Verify access code can be set."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?access_code=12345678")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["access_code_set"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_access_code_wrong_length(self, async_client: AsyncClient):
|
||||
"""Verify access code validation for length."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?access_code=123")
|
||||
|
||||
# Should fail validation
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_enable_without_access_code(self, async_client: AsyncClient):
|
||||
"""Verify enabling fails without access code set."""
|
||||
# First ensure no access code is set by checking current state
|
||||
# Then try to enable
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?enabled=true")
|
||||
|
||||
# If access code wasn't set, this should fail
|
||||
# If it was already set, it will succeed
|
||||
# Both are valid test outcomes
|
||||
assert response.status_code in [200, 400]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_enable_with_access_code(self, async_client: AsyncClient):
|
||||
"""Verify enabling succeeds when access code is set."""
|
||||
# First set access code
|
||||
await async_client.put("/api/v1/settings/virtual-printer?access_code=12345678")
|
||||
|
||||
# Then enable (this will start the servers which may fail in test env)
|
||||
# We mock the manager to avoid actually starting servers
|
||||
with patch("backend.app.services.virtual_printer.virtual_printer_manager") as mock_manager:
|
||||
mock_manager.configure = AsyncMock()
|
||||
mock_manager.get_status = MagicMock(
|
||||
return_value={
|
||||
"enabled": True,
|
||||
"running": True,
|
||||
"mode": "immediate",
|
||||
"name": "Bambuddy",
|
||||
"serial": "00M09A391800001",
|
||||
"pending_files": 0,
|
||||
}
|
||||
)
|
||||
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?enabled=true")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_disable_virtual_printer(self, async_client: AsyncClient):
|
||||
"""Verify virtual printer can be disabled."""
|
||||
with patch("backend.app.services.virtual_printer.virtual_printer_manager") as mock_manager:
|
||||
mock_manager.configure = AsyncMock()
|
||||
mock_manager.get_status = MagicMock(
|
||||
return_value={
|
||||
"enabled": False,
|
||||
"running": False,
|
||||
"mode": "immediate",
|
||||
"name": "Bambuddy",
|
||||
"serial": "00M09A391800001",
|
||||
"pending_files": 0,
|
||||
}
|
||||
)
|
||||
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?enabled=false")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["enabled"] is False
|
||||
|
||||
|
||||
class TestPendingUploadsAPI:
|
||||
"""Integration tests for /api/v1/pending-uploads/ endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pending_uploads(self, db_session):
|
||||
"""Create mock pending uploads in database."""
|
||||
|
||||
async def _create_pending(filename: str = "test.3mf"):
|
||||
from datetime import datetime
|
||||
|
||||
from backend.app.models.pending_upload import PendingUpload
|
||||
|
||||
upload = PendingUpload(
|
||||
filename=filename,
|
||||
file_path=f"/tmp/{filename}",
|
||||
file_size=1024,
|
||||
source_ip="192.168.1.100",
|
||||
status="pending",
|
||||
)
|
||||
db_session.add(upload)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(upload)
|
||||
return upload
|
||||
|
||||
return _create_pending
|
||||
|
||||
# ========================================================================
|
||||
# List pending uploads
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_pending_uploads_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list is returned when no pending uploads."""
|
||||
response = await async_client.get("/api/v1/pending-uploads/")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert isinstance(result, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_pending_uploads_count(self, async_client: AsyncClient):
|
||||
"""Verify count endpoint returns correct count."""
|
||||
response = await async_client.get("/api/v1/pending-uploads/count")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "count" in result
|
||||
assert isinstance(result["count"], int)
|
||||
|
||||
# ========================================================================
|
||||
# Archive pending upload
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archive_nonexistent_upload(self, async_client: AsyncClient):
|
||||
"""Verify archiving non-existent upload returns 404."""
|
||||
response = await async_client.post("/api/v1/pending-uploads/99999/archive")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Discard pending upload
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_discard_nonexistent_upload(self, async_client: AsyncClient):
|
||||
"""Verify discarding non-existent upload returns 404."""
|
||||
response = await async_client.delete("/api/v1/pending-uploads/99999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Bulk operations
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archive_all_empty(self, async_client: AsyncClient):
|
||||
"""Verify archive all with no pending uploads."""
|
||||
response = await async_client.post("/api/v1/pending-uploads/archive-all")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "archived" in result
|
||||
assert "failed" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_discard_all_empty(self, async_client: AsyncClient):
|
||||
"""Verify discard all with no pending uploads."""
|
||||
response = await async_client.delete("/api/v1/pending-uploads/discard-all")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "discarded" in result
|
||||
Reference in New Issue
Block a user