test(queue,billing,vp): pin dispatch failures, creator scope and busy-printer holds, aborted-print usage fallbacks, end-to-end print completion and billing, VP MQTT pre-login and rate-limit guards; test schema loads every model

This commit is contained in:
maziggy
2026-10-19 09:50:39 +02:00
parent 69c2ff1b44
commit 44a5c7a7f5
8 changed files with 561 additions and 179 deletions
@@ -0,0 +1,159 @@
"""A queued print's completion, end to end on the database.
The archive row, the print log entry, the queue item and the wallet charge are
what the rest of Bambuddy reads afterwards (archives page, statistics, project
totals, the cost centre's budget). Each was only pinned through mocks of the
function that writes it; here ``on_print_complete`` runs against real rows and
only the outside world (websocket, MQTT relay, notifications, plugs, the
printer's SD card) is mocked; the printer manager knows no printer.
"""
import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from backend.app import main as main_module
from backend.app.models.archive import PrintArchive
from backend.app.models.finance import CostCenter, WalletTransaction
from backend.app.models.print_log import PrintLogEntry
from backend.app.models.print_queue import PrintQueueItem
from backend.app.models.settings import Settings
from backend.app.models.user import User
from backend.app.services.bambu_ftp import DeleteResult
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
RUN = "run-end-to-end"
@pytest.fixture(autouse=True)
def _clean():
"""The completion marks the printer as awaiting a plate clear on the real
printer manager; the next test on this worker must not inherit that."""
from backend.app.services.printer_manager import printer_manager
main_module._active_prints.clear()
yield
main_module._active_prints.clear()
printer_manager._awaiting_plate_clear.clear()
@pytest.fixture
async def queued_print(db_session, printer_factory):
printer = await printer_factory(name="P")
db_session.add(Settings(key="billing_enabled", value="true"))
db_session.add(Settings(key="capture_finish_photo", value="false"))
user = User(username="payer", password_hash="x", is_active=True)
center = CostCenter(name="Lab", is_active=True, is_private=False)
db_session.add_all([user, center])
await db_session.flush()
archive = PrintArchive(
printer_id=printer.id,
filename="part.3mf",
print_name="part",
file_path="archives/test/part.3mf",
file_size=1,
status="printing",
cost=5.0,
created_by_id=user.id,
billing_run_id=RUN,
)
db_session.add(archive)
await db_session.flush()
item = PrintQueueItem(
printer_id=printer.id,
archive_id=archive.id,
status="printing",
position=1,
created_by_id=user.id,
cost_center_id=center.id,
estimated_cost=5.0,
billing_run_id=RUN,
)
db_session.add(item)
await db_session.commit()
main_module._active_prints[(printer.id, "part.3mf")] = archive.id
return {"printer": printer.id, "user": user.id, "center": center.id, "archive": archive.id, "item": item.id}
async def _complete(test_engine, printer_id: int, status: str) -> None:
maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
tasks_before = set(asyncio.all_tasks())
with (
patch("backend.app.main.async_session", maker),
patch("backend.app.core.database.async_session", maker),
patch("backend.app.main.ws_manager") as ws,
patch("backend.app.main.mqtt_relay") as relay,
patch("backend.app.main.notification_service") as notif,
patch("backend.app.main.smart_plug_manager") as plug,
patch("backend.app.services.spoolman_tracking.async_session", maker),
# The SD-card cleanup of the uploaded file
patch("backend.app.services.bambu_ftp.delete_file_async", AsyncMock(return_value=DeleteResult.DELETED)),
patch("backend.app.services.bambu_ftp.list_files_async", AsyncMock(return_value=[])),
):
ws.send_print_complete = AsyncMock()
ws.send_archive_updated = AsyncMock()
ws.broadcast = AsyncMock()
relay.on_print_complete = AsyncMock()
notif.on_print_complete = AsyncMock()
plug.on_print_complete = AsyncMock()
await main_module.on_print_complete(
printer_id,
{"status": status, "filename": "part.gcode", "subtask_name": "part", "timelapse_was_active": False},
)
spawned = asyncio.all_tasks() - tasks_before
if spawned:
_done, pending = await asyncio.wait(spawned, timeout=10)
# Left running, they would carry on into the next test unpatched
assert not pending, f"background work still running: {pending}"
async def _rows(db_session, archive_id: int, item_id: int):
db_session.expire_all()
archive = await db_session.get(PrintArchive, archive_id)
item = await db_session.get(PrintQueueItem, item_id)
logs = (await db_session.scalars(select(PrintLogEntry).where(PrintLogEntry.archive_id == archive_id))).all()
charges = (
await db_session.scalars(select(WalletTransaction).where(WalletTransaction.transaction_type == "print_charge"))
).all()
return archive, item, logs, charges
async def test_a_completed_print(test_engine, db_session, queued_print):
await _complete(test_engine, queued_print["printer"], "completed")
archive, item, logs, charges = await _rows(db_session, queued_print["archive"], queued_print["item"])
assert archive.status == "completed"
assert item.status == "completed"
assert [log.status for log in logs] == ["completed"]
assert len(charges) == 1
charge = charges[0]
assert (charge.user_id, charge.cost_center_id, charge.print_queue_id, charge.print_run_id) == (
queued_print["user"],
queued_print["center"],
queued_print["item"],
RUN,
)
assert abs(charge.amount) == pytest.approx(5.0)
async def test_a_failed_print(test_engine, db_session, queued_print):
await _complete(test_engine, queued_print["printer"], "failed")
archive, item, logs, _charges = await _rows(db_session, queued_print["archive"], queued_print["item"])
assert archive.status == "failed"
assert item.status == "failed"
assert [log.status for log in logs] == ["failed"]
async def test_a_second_completion_charges_nothing_more(test_engine, db_session, queued_print):
"""A duplicate completion callback (reconnect replay) is idempotent."""
await _complete(test_engine, queued_print["printer"], "completed")
main_module._active_prints[(queued_print["printer"], "part.3mf")] = queued_print["archive"]
await _complete(test_engine, queued_print["printer"], "completed")
_archive, _item, _logs, charges = await _rows(db_session, queued_print["archive"], queued_print["item"])
assert len(charges) == 1