mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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.
44 lines
1.8 KiB
Python
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)
|