mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Non-proxy VPs (Archive / Review / Queue) with a target printer set up a live-mirror bridge that forwards the slicer's MQTT and RTSPS auth bytes through to the real printer. The slicer holds one code in its profile (the one it bound the VP with), and that code has to satisfy both the VP listener and the real printer at the far end of the bridge. If the codes diverge the bridge silently fails at the second hop — slicer reaches .49:8883, FINs before sending a ClientHello, retries identically. The wiki framed the code-match requirement as a camera-only concern; it isn't, all bridged protocols inherit. Fix removes the foot-gun instead of re-documenting it. When a target is selected on a non-proxy VP the access-code field switches to a read-only display showing the target's code with an Eye-toggle reveal; the backend auto-inherits on every create / update (any explicit access_code submitted alongside a target is silently overridden as belt-and-braces for non-UI clients). The required-when- enabling check now treats target-set as satisfying the access-code requirement. Standalone (no-target) non-proxy VPs still get the editable input + Save button. One-shot startup migration corrects any pre-existing mismatched rows: SELECTs diverged VPs and logs one INFO line per row for the audit trail, then UPDATEs via correlated subquery. Idempotent and portable between SQLite and Postgres.
602 lines
23 KiB
Python
602 lines
23 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_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 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"
|