Files
maziggy a4795c5ca3 Let API keys read and run slicer pipelines (#1425 follow-up)
Every pipeline endpoint answered 403 for API keys whatever scopes the key
carried. PR A parked all three permissions on the admin denylist until the
run dispatch existed to decide about; it landed in PR C and the parking was
never revisited.

PIPELINES_READ now rides can_read_status. PIPELINES_RUN requires
can_queue AND can_manage_library together, so the allowlist gained tuple
values: a run slices into the library and then queues prints, and mapping
it to either flag alone would hand that flag the other one's authority.
The 403 names every flag the key is short of. PIPELINES_WRITE stays
admin-only -- a key can run the recipe, not rewrite it or clear the log.

Opening the run route also needed the cloud-owner fallback the direct
slice route makes: a pipeline can carry Bambu/Orca Cloud presets, and
resolving those reads a token off a user record that an API-keyed request
does not have. retry_failed forwards the new dependency explicitly,
since a direct call receives the Depends marker rather than None.
2026-08-15 10:38:51 +02:00

44 lines
1.8 KiB
Python

"""Patching ``spawn_background_task`` without leaking the coroutine.
Every caller builds its coroutine as a *call argument*::
spawn_background_task(self._watchdog_print_start(...), name=...)
so the coroutine object is constructed whether or not the replacement ever
schedules it. A bare ``MagicMock`` then parks it in ``call_args`` and it is
finalised, never awaited, during some *later* test's garbage collection —
surfacing as a ``PytestUnraisableExceptionWarning`` attributed to whichever
unrelated test happened to be running at the time. That makes the report
useless for finding the leak and, because it depends on GC timing and test
order, it appears and disappears between runs of the same suite.
Closing the coroutine mirrors what the real helper does — take ownership of it
— while still keeping the work from running.
``DEFAULT`` rather than the ``close()`` return: the mock's return value stands
in for the ``asyncio.Task``, and the queue-pool path in ``_process_queue``
calls ``task.add_done_callback(...)`` on it. Returning ``None`` from the
side-effect would replace the usual ``MagicMock`` return with ``None`` and
break that caller.
"""
from unittest.mock import DEFAULT, patch
SCHEDULER_TARGET = "backend.app.services.print_scheduler.spawn_background_task"
MAIN_TARGET = "backend.app.main.spawn_background_task"
def close_and_default(coro, **kwargs):
"""Take ownership of ``coro`` the way the real helper would, then stand down."""
coro.close()
return DEFAULT
def discarding_spawn_patch(target: str = SCHEDULER_TARGET):
"""``patch`` for ``spawn_background_task`` that closes what it is handed.
A drop-in for ``patch(target, MagicMock())`` — still a mock, so call
assertions work — that does not leave an un-awaited coroutine behind.
"""
return patch(target, side_effect=close_and_default)