| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- """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_queue(self, async_client: AsyncClient):
- """Verify mode can be set to the canonical 'queue' value."""
- 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_legacy_print_queue_normalises_to_queue(self, async_client: AsyncClient):
- """Legacy `print_queue` is accepted on input and translated to `queue` on
- storage so the UI button label and the support-bundle field agree
- (#1429 mode-label discrepancy)."""
- response = await async_client.put("/api/v1/settings/virtual-printer?mode=print_queue")
- assert response.status_code == 200
- result = response.json()
- assert result["mode"] == "queue"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_mode_legacy_immediate_normalises_to_archive(self, async_client: AsyncClient):
- """Legacy `immediate` is accepted on input and translated to `archive`
- on storage (#1429 mode-label discrepancy)."""
- response = await async_client.put("/api/v1/settings/virtual-printer?mode=immediate")
- assert response.status_code == 200
- result = response.json()
- assert result["mode"] == "archive"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_mode_to_archive(self, async_client: AsyncClient):
- """Verify mode can be set to the canonical 'archive' value."""
- response = await async_client.put("/api/v1/settings/virtual-printer?mode=archive")
- assert response.status_code == 200
- result = response.json()
- assert result["mode"] == "archive"
- @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": "archive",
- "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": "archive",
- "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
- class TestVirtualPrinterAutoDispatchAPI:
- """Integration tests for auto_dispatch on /api/v1/virtual-printers endpoints."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_virtual_printer_auto_dispatch_default(self, async_client: AsyncClient):
- """Verify creating a VP without auto_dispatch defaults to true."""
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestDefaultDispatch",
- "mode": "queue",
- "access_code": "12345678",
- },
- )
- assert response.status_code == 200
- result = response.json()
- assert result["auto_dispatch"] is True
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_virtual_printer_auto_dispatch_false(self, async_client: AsyncClient):
- """Verify creating a VP with auto_dispatch=false persists correctly."""
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestManualDispatch",
- "mode": "queue",
- "access_code": "12345678",
- "auto_dispatch": False,
- },
- )
- assert response.status_code == 200
- result = response.json()
- assert result["auto_dispatch"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_virtual_printer_auto_dispatch(self, async_client: AsyncClient):
- """Verify auto_dispatch can be toggled via PUT and persists."""
- # Create with auto_dispatch=True (default)
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestToggleDispatch",
- "mode": "queue",
- "access_code": "12345678",
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- # Update to auto_dispatch=False
- update_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"auto_dispatch": False},
- )
- assert update_resp.status_code == 200
- assert update_resp.json()["auto_dispatch"] is False
- # Verify it persists by fetching
- get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp_id}")
- assert get_resp.status_code == 200
- assert get_resp.json()["auto_dispatch"] is False
- class TestVirtualPrinterGcodeInjectionAPI:
- """Integration tests for gcode_injection (#1516) on /api/v1/virtual-printers endpoints."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_virtual_printer_gcode_injection_default_off(self, async_client: AsyncClient):
- """Verify creating a VP without gcode_injection defaults to false (opt-in)."""
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestDefaultInjection",
- "mode": "queue",
- "access_code": "12345678",
- },
- )
- assert response.status_code == 200
- assert response.json()["gcode_injection"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_virtual_printer_gcode_injection(self, async_client: AsyncClient):
- """Verify gcode_injection can be toggled via PUT and persists."""
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestToggleInjection",
- "mode": "queue",
- "access_code": "12345678",
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- assert create_resp.json()["gcode_injection"] is False
- update_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"gcode_injection": True},
- )
- assert update_resp.status_code == 200
- assert update_resp.json()["gcode_injection"] is True
- get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp_id}")
- assert get_resp.status_code == 200
- assert get_resp.json()["gcode_injection"] is True
- class TestVirtualPrinterTailscaleToggleAPI:
- """The Tailscale toggle is informational — toggling either way always succeeds.
- There used to be a 409 guard rejecting "enable" when the daemon was unreachable,
- back when the toggle controlled LE cert provisioning. That path was removed:
- the slicer's printer-MQTT trust validates against its bundled BBL CA, not the
- system trust store, so even an LE cert wouldn't be accepted. The toggle now
- only surfaces the host's Tailscale IP/FQDN on the VP card; daemon presence is
- irrelevant to whether the toggle can be flipped.
- """
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_toggle_does_not_consult_tailscale_daemon(self, async_client: AsyncClient):
- """PUT tailscale_disabled never calls tailscale_service.get_status — always succeeds."""
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "TestTailscaleToggle",
- "mode": "archive",
- "access_code": "12345678",
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- assert create_resp.json()["tailscale_disabled"] is True
- with patch(
- "backend.app.services.virtual_printer.tailscale.tailscale_service.get_status",
- new=AsyncMock(side_effect=AssertionError("get_status must not be called for toggle")),
- ):
- enable_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"tailscale_disabled": False},
- )
- disable_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"tailscale_disabled": True},
- )
- assert enable_resp.status_code == 200
- assert enable_resp.json()["tailscale_disabled"] is False
- assert disable_resp.status_code == 200
- assert disable_resp.json()["tailscale_disabled"] is True
- class TestVirtualPrinterCaCertificateAPI:
- """Integration tests for GET /api/v1/virtual-printers/ca-certificate."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_ca_certificate_returns_pem(self, async_client: AsyncClient):
- """The shared CA certificate is returned as PEM with identifying metadata."""
- response = await async_client.get("/api/v1/virtual-printers/ca-certificate")
- assert response.status_code == 200
- result = response.json()
- assert result["pem"].startswith("-----BEGIN CERTIFICATE-----")
- assert "PRIVATE KEY" not in result["pem"] # never expose the CA key
- assert len(result["fingerprint_sha256"].split(":")) == 32
- assert result["not_valid_after"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_ca_certificate_route_precedes_vp_id_route(self, async_client: AsyncClient):
- """'ca-certificate' must not be swallowed by the /{vp_id} int route."""
- response = await async_client.get("/api/v1/virtual-printers/ca-certificate")
- # A 200 (not 422 from int-parsing "ca-certificate") proves route ordering.
- assert response.status_code == 200
- class TestVirtualPrinterDiagnosticAPI:
- """Integration tests for GET /api/v1/virtual-printers/{vp_id}/diagnostic."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_diagnose_unknown_vp_returns_404(self, async_client: AsyncClient):
- response = await async_client.get("/api/v1/virtual-printers/999999/diagnostic")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_diagnose_disabled_vp_reports_problems(self, async_client: AsyncClient):
- """A freshly created (disabled) VP fails the 'enabled' check."""
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={"name": "TestDiagVP", "mode": "archive", "access_code": "12345678"},
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- response = await async_client.get(f"/api/v1/virtual-printers/{vp_id}/diagnostic")
- assert response.status_code == 200
- result = response.json()
- assert result["vp_id"] == vp_id
- assert result["overall"] == "problems"
- by_id = {c["id"]: c["status"] for c in result["checks"]}
- assert by_id["enabled"] == "fail"
- assert by_id["running"] == "skip"
- class TestVirtualPrinterAccessCodeInheritance:
- """Non-proxy VPs with a target printer must inherit the target's access
- code at write time.
- The live-mirror bridge forwards the slicer's MQTT/RTSPS auth bytes to
- the real printer — if the codes diverge the slicer binds the VP but the
- bridge fails at the second hop. The route layer force-derives the code
- on every create / update so a non-UI client can't introduce a divergence
- either.
- """
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_with_target_ignores_submitted_access_code(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- from sqlalchemy import select
- from backend.app.models.virtual_printer import VirtualPrinter
- target = await printer_factory(name="Real X1C", access_code="REALCODE")
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "QueueVP",
- "mode": "queue",
- "access_code": "WRONGAAA",
- "target_printer_id": target.id,
- },
- )
- assert response.status_code == 200
- vp_id = response.json()["id"]
- assert response.json()["access_code_set"] is True
- vp = (await db_session.execute(select(VirtualPrinter).where(VirtualPrinter.id == vp_id))).scalar_one()
- assert vp.access_code == "REALCODE"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_with_target_and_no_access_code_still_enables(
- self, async_client: AsyncClient, printer_factory
- ):
- """A non-proxy VP with a target set can be enabled without supplying
- access_code separately — the inheritance makes the explicit field
- redundant, and the validator now knows this."""
- target = await printer_factory(name="Real X1C", access_code="REALCODE")
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "QueueVP",
- "mode": "queue",
- "target_printer_id": target.id,
- "bind_ip": "192.168.1.50",
- "enabled": True,
- },
- )
- assert response.status_code == 200
- assert response.json()["access_code_set"] is True
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_without_target_still_requires_access_code_on_enable(self, async_client: AsyncClient):
- """The relaxation only kicks in when a target is set. A standalone
- non-proxy VP still needs its own access code."""
- response = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "StandaloneVP",
- "mode": "archive",
- "bind_ip": "192.168.1.51",
- "enabled": True,
- },
- )
- assert response.status_code == 400
- assert "access code" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_target_resyncs_access_code(self, async_client: AsyncClient, printer_factory, db_session):
- from sqlalchemy import select
- from backend.app.models.virtual_printer import VirtualPrinter
- first = await printer_factory(name="Printer A", access_code="AAAAAAAA")
- second = await printer_factory(name="Printer B", access_code="BBBBBBBB")
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "MovingTarget",
- "mode": "queue",
- "target_printer_id": first.id,
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- vp = (await db_session.execute(select(VirtualPrinter).where(VirtualPrinter.id == vp_id))).scalar_one()
- assert vp.access_code == "AAAAAAAA"
- # Repoint to the second printer — access code should follow.
- update_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"target_printer_id": second.id},
- )
- assert update_resp.status_code == 200
- await db_session.refresh(vp)
- assert vp.access_code == "BBBBBBBB"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_explicit_access_code_with_target_is_overridden(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """An update that submits both an explicit access_code AND keeps a
- target_printer_id silently uses the target's code — belt-and-braces
- for non-UI clients that might try to set a divergent value."""
- from sqlalchemy import select
- from backend.app.models.virtual_printer import VirtualPrinter
- target = await printer_factory(name="Real X1C", access_code="REALCODE")
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "BeltBraces",
- "mode": "queue",
- "target_printer_id": target.id,
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- update_resp = await async_client.put(
- f"/api/v1/virtual-printers/{vp_id}",
- json={"access_code": "FORGEDCD"},
- )
- assert update_resp.status_code == 200
- vp = (await db_session.execute(select(VirtualPrinter).where(VirtualPrinter.id == vp_id))).scalar_one()
- assert vp.access_code == "REALCODE"
- class TestVirtualPrinterSerialSurface:
- """Proxy-mode VPs must surface the target printer's serial in API responses.
- The bridge advertises the target's serial over SSDP and forwards the
- target's identity to the slicer; the VP-settings card should show the
- same serial so the user sees one consistent identity per VP. Archive /
- queue / review VPs keep the self-generated suffix-based serial since
- those modes never speak the target's identity.
- """
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_proxy_vp_response_uses_target_printer_serial(self, async_client: AsyncClient, printer_factory):
- target = await printer_factory(name="Real X1C", access_code="REALCODE", serial_number="00M09A123456789")
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "ProxyVP",
- "mode": "proxy",
- "target_printer_id": target.id,
- },
- )
- assert create_resp.status_code == 200
- vp_id = create_resp.json()["id"]
- assert create_resp.json()["serial"] == "00M09A123456789"
- get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp_id}")
- assert get_resp.status_code == 200
- assert get_resp.json()["serial"] == "00M09A123456789"
- list_resp = await async_client.get("/api/v1/virtual-printers")
- assert list_resp.status_code == 200
- listed = next(p for p in list_resp.json()["printers"] if p["id"] == vp_id)
- assert listed["serial"] == "00M09A123456789"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_non_proxy_vp_keeps_self_generated_serial(self, async_client: AsyncClient, printer_factory):
- # Even with a target printer set (#1429 access-code inheritance flow),
- # archive / queue / review VPs are NOT bridging the target's identity
- # to the slicer — they synthesise their own. Self-generated serial.
- target = await printer_factory(name="Real X1C", access_code="REALCODE", serial_number="00M09A123456789")
- create_resp = await async_client.post(
- "/api/v1/virtual-printers",
- json={
- "name": "QueueVP",
- "mode": "queue",
- "target_printer_id": target.id,
- },
- )
- assert create_resp.status_code == 200
- assert create_resp.json()["serial"] != "00M09A123456789"
- # The synthesised serial follows _get_serial_for_model's `<prefix><suffix>`
- # shape — model-specific prefix + 8-char hex suffix from `vp.serial_suffix`.
- assert len(create_resp.json()["serial"]) >= 8
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_proxy_vp_falls_back_to_self_generated_when_target_missing(
- self, async_client: AsyncClient, db_session
- ):
- # Defensive fallback: a proxy VP whose target_printer_id points at a
- # row that no longer exists (printer deleted mid-config, manual SQL
- # tweak, race) must not 500 — it returns the self-generated serial
- # so the card still renders and the user can fix the target.
- from backend.app.models.virtual_printer import VirtualPrinter
- vp = VirtualPrinter(name="OrphanProxy", mode="proxy", target_printer_id=99999, enabled=False)
- db_session.add(vp)
- await db_session.commit()
- get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp.id}")
- assert get_resp.status_code == 200
- body = get_resp.json()
- assert body["serial"] # non-empty
- assert len(body["serial"]) >= 8
|