| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- """Unit tests for spool assignment notification service."""
- import logging
- from types import SimpleNamespace
- from unittest.mock import AsyncMock, patch
- import pytest
- from backend.app.services.spool_assignment_notifications import notify_missing_spool_assignments_on_print_start
- class _FakeAssignmentsResult:
- def __init__(self, rows):
- self._rows = rows
- def fetchall(self):
- return self._rows
- class _FakeSession:
- """Fake DB session that returns legacy vs. Spoolman assignment rows based
- on which table the SELECT targets, so tests can exercise either mode."""
- def __init__(
- self,
- printer_name: str,
- legacy: list[SimpleNamespace] | None = None,
- spoolman: list[SimpleNamespace] | None = None,
- ):
- self._printer = SimpleNamespace(name=printer_name)
- self._legacy = legacy or []
- self._spoolman = spoolman or []
- async def __aenter__(self):
- return self
- async def __aexit__(self, exc_type, exc, tb):
- return False
- async def get(self, model, key):
- return self._printer
- async def execute(self, statement):
- table = statement.get_final_froms()[0].name
- if table == "spoolman_slot_assignments":
- return _FakeAssignmentsResult(self._spoolman)
- return _FakeAssignmentsResult(self._legacy)
- @pytest.mark.asyncio
- async def test_missing_assignment_broadcasts_websocket_event_and_push_notification():
- """When a mapped tray is unassigned, service emits websocket and notification events."""
- logger = logging.getLogger(__name__)
- data = {
- "ams_mapping": [1],
- "raw_data": {},
- }
- # Assignment exists for A1 (global tray 0), but print uses A2 (global tray 1).
- assignments = [SimpleNamespace(ams_id=0, tray_id=0)]
- with (
- patch(
- "backend.app.services.spool_assignment_notifications.async_session",
- return_value=_FakeSession("Printer A", assignments),
- ),
- patch("backend.app.services.spool_assignment_notifications.printer_manager.get_status", return_value=None),
- patch(
- "backend.app.services.spool_assignment_notifications.ws_manager.send_missing_spool_assignment",
- new_callable=AsyncMock,
- ) as mock_ws,
- patch(
- "backend.app.services.spool_assignment_notifications.notification_service.on_print_missing_spool_assignment",
- new_callable=AsyncMock,
- ) as mock_notify,
- ):
- await notify_missing_spool_assignments_on_print_start(1, data, logger)
- mock_ws.assert_awaited_once()
- ws_kwargs = mock_ws.await_args.kwargs
- assert ws_kwargs["printer_id"] == 1
- assert ws_kwargs["printer_name"] == "Printer A"
- assert ws_kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
- mock_notify.assert_awaited_once()
- notify_kwargs = mock_notify.await_args.kwargs
- assert notify_kwargs["printer_id"] == 1
- assert notify_kwargs["printer_name"] == "Printer A"
- assert notify_kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
- def _patches(session):
- """Common patch set: the fake session + stubbed printer state / emitters."""
- return (
- patch(
- "backend.app.services.spool_assignment_notifications.async_session",
- return_value=session,
- ),
- patch("backend.app.services.spool_assignment_notifications.printer_manager.get_status", return_value=None),
- patch(
- "backend.app.services.spool_assignment_notifications.ws_manager.send_missing_spool_assignment",
- new_callable=AsyncMock,
- ),
- patch(
- "backend.app.services.spool_assignment_notifications.notification_service.on_print_missing_spool_assignment",
- new_callable=AsyncMock,
- ),
- )
- @pytest.mark.asyncio
- async def test_spoolman_only_assignment_suppresses_notification():
- """#1473 — trays bound only via spoolman_slot_assignments must NOT be
- flagged missing (the legacy spool_assignment table is empty in Spoolman
- mode, so checking it alone fired a false positive on every print)."""
- logger = logging.getLogger(__name__)
- data = {"ams_mapping": [0, 1], "raw_data": {}} # print uses A1 + A2
- # Both used trays bound via Spoolman; legacy table empty.
- session = _FakeSession(
- "Printer A",
- legacy=[],
- spoolman=[SimpleNamespace(ams_id=0, tray_id=0), SimpleNamespace(ams_id=0, tray_id=1)],
- )
- p_session, p_status, p_ws, p_notify = _patches(session)
- with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
- await notify_missing_spool_assignments_on_print_start(1, data, logger)
- mock_ws.assert_not_awaited()
- mock_notify.assert_not_awaited()
- @pytest.mark.asyncio
- async def test_spoolman_partial_coverage_flags_only_uncovered_tray():
- """A Spoolman assignment for A1 only, with a print using A1 + A2, flags
- A2 alone."""
- logger = logging.getLogger(__name__)
- data = {"ams_mapping": [0, 1], "raw_data": {}}
- session = _FakeSession(
- "Printer A",
- legacy=[],
- spoolman=[SimpleNamespace(ams_id=0, tray_id=0)], # A1 only
- )
- p_session, p_status, p_ws, p_notify = _patches(session)
- with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
- await notify_missing_spool_assignments_on_print_start(1, data, logger)
- mock_ws.assert_awaited_once()
- assert mock_ws.await_args.kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
- mock_notify.assert_awaited_once()
- @pytest.mark.asyncio
- async def test_mixed_mode_union_covers_all_used_trays():
- """A1 bound in the legacy table, A2 bound in spoolman_slot_assignments —
- the union covers both used trays, so no notification fires."""
- logger = logging.getLogger(__name__)
- data = {"ams_mapping": [0, 1], "raw_data": {}}
- session = _FakeSession(
- "Printer A",
- legacy=[SimpleNamespace(ams_id=0, tray_id=0)], # A1
- spoolman=[SimpleNamespace(ams_id=0, tray_id=1)], # A2
- )
- p_session, p_status, p_ws, p_notify = _patches(session)
- with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
- await notify_missing_spool_assignments_on_print_start(1, data, logger)
- mock_ws.assert_not_awaited()
- mock_notify.assert_not_awaited()
|