Files
bambuddy/backend/tests/integration/test_virtual_printer_api.py
T
maziggy 6fd11ddc77 Add virtual printer Queue mode and sidebar badge indicators
Virtual Printer Changes:
- Add new "Queue" mode that archives files and adds them to print queue
- Rename modes: "Immediate" → "Archive", "Queue for Review" → "Review"
- Three modes now: Archive (immediate), Review (pending), Queue (print queue)
- Queue mode creates unassigned queue items (printer_id=null)

Print Queue Changes:
- Make printer_id nullable in PrintQueueItem model/schema
- Add support for unassigned queue items (no printer assigned)
- Queue page shows "Unassigned" filter option
- Edit modal allows assigning printer to unassigned items
- Unassigned items highlighted in orange

Sidebar Badge Indicators:
- Queue icon shows yellow badge with pending queue item count
- Archive icon shows blue badge with pending upload count
- Badges auto-update every 5 seconds and on window focus

Other:
- Update backup/restore to handle nullable printer_id and ams_mapping
- Add database migration for nullable printer_id column
- Update VirtualPrinterSettings tests for new modes
- Update virtual printer API integration tests
- Remove speed controls documentation from wiki (not implemented)
2026-01-17 13:30:00 +01:00

273 lines
10 KiB
Python

"""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=review")
assert response.status_code == 200
result = response.json()
assert result["mode"] == "review"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_mode_to_print_queue(self, async_client: AsyncClient):
"""Verify mode can be set to print_queue."""
response = await async_client.put("/api/v1/settings/virtual-printer?mode=print_queue")
assert response.status_code == 200
result = response.json()
assert result["mode"] == "print_queue"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_mode_legacy_queue_maps_to_review(self, async_client: AsyncClient):
"""Verify legacy 'queue' mode is normalized to 'review'."""
response = await async_client.put("/api/v1/settings/virtual-printer?mode=queue")
assert response.status_code == 200
result = response.json()
assert result["mode"] == "review" # Legacy queue maps to review
@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