Files
shelfmark/tests/download/test_reattached.py
T
splitsec2 e575ff0fcb feat(activity): clearer cards, a Rejected view and Reopen for failed deliveries (#1618)
A batch of activity list changes from running it as an admin on my own
instance. They are separate commits that build on each other, so they
can be reviewed in order.

The cards read better. Every card gets a format pill (violet for audio,
orange for ebooks), the requester is named once, and finished cards say
how long a download took and how long ago it ended ("took 5m · 35m
ago"). A download that picked up work already under way shows a
Reattached pill.

Admins can tell what is automatic and what needs a person. A declined
request shows who declined it and when, anything with an `[auto]` note
reads "Declined automatically", and "Needs Review" becomes "No Match
Yet" with a line saying the search repeats on every sync.

There is a Rejected view next to History for admins, with Reopen on each
card and a way to hide the dead ones.

The part to read most carefully is the backend in `requests_service.py`
and `user_db.py`. An admin can now reopen a fulfilled request whose
download failed or was cancelled, and the reopen a failed download
triggers remembers which release failed, so the next search doesn't pick
it again.

I ran it on a scratch instance (when pulling them together to move from
my fork to upstream) as an admin with seeded requests and looked at the
pills, the durations, the wording and the Rejected view in a browser.
Reopening a fulfilled request I only exercised in the tests. The
frontend and Python suites, type check, ruff and basedpyright pass.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Admins can review rejected requests in a dedicated view, hide or
restore cards, and reopen requests.
* Fulfilled requests with failed or cancelled deliveries can be
reopened; the failed release is skipped in the next automatic search.
* Activity cards now show format badges, download timing, and when a
download resumes existing work.
* **Improvements**
* Activity views clarify automatic retries and actions, and show
reviewer names for admins.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:02:21 -06:00

79 lines
2.9 KiB
Python

"""A download that picks up work already under way says so, so nobody wonders why it started at 40%."""
from __future__ import annotations
from pathlib import Path
from threading import Event
from unittest.mock import MagicMock, patch
import pytest
from shelfmark.core.models import DownloadTask, QueueStatus
from shelfmark.download.clients import DownloadState, DownloadStatus
from shelfmark.download.orchestrator import _task_to_dict
from shelfmark.release_sources.prowlarr.handler import ProwlarrHandler
_STATUS = DownloadStatus(
progress=40.0, state=DownloadState.DOWNLOADING, message=None, complete=False, file_path=None
)
_DONE = DownloadStatus(
progress=100.0, state=DownloadState.COMPLETE, message=None, complete=True, file_path="/x/b"
)
def _run(*, existing: bool, finished: bool = False) -> DownloadTask:
"""Run one Prowlarr download against a mocked client that may already hold the torrent.
With ``finished`` the client's copy is already complete, so the handler copies the file
instead of joining the poll loop.
"""
client = MagicMock()
client.name = "qbittorrent"
found = ("dl", _DONE if finished else _STATUS) if existing else None
client.find_existing.return_value = found
client.add_download.return_value = "dl"
cancel = Event()
def get_status(_id: str) -> DownloadStatus:
cancel.set()
return _STATUS
client.get_status.side_effect = get_status
release = {"protocol": "torrent", "magnetUrl": "magnet:?xt=urn:btih:abc123"}
task = DownloadTask(task_id="t", source="prowlarr", title="Book")
with (
patch("shelfmark.release_sources.prowlarr.handler.get_release", return_value=release),
patch("shelfmark.release_sources.prowlarr.handler.get_client", return_value=client),
patch("shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL", 0.01),
patch.object(
ProwlarrHandler, "_wait_for_completed_path", return_value=(Path("/x/b"), None)
),
patch.object(ProwlarrHandler, "_handle_completed_file", return_value="/lib/b"),
):
ProwlarrHandler().download(
task=task,
cancel_flag=cancel,
progress_callback=lambda _p: None,
status_callback=lambda *_a: None,
)
return task
def test_a_task_that_joins_a_torrent_the_client_had_is_reattached() -> None:
assert _run(existing=True).reattached is True
def test_a_task_whose_torrent_the_client_had_finished_is_reattached_too() -> None:
assert _run(existing=True, finished=True).reattached is True
def test_a_task_that_adds_its_own_torrent_is_not() -> None:
assert _run(existing=False).reattached is False
@pytest.mark.parametrize("flag", [True, False])
def test_the_flag_reaches_the_status_payload(flag: bool) -> None:
task = DownloadTask(task_id="t", source="prowlarr", title="Book", reattached=flag)
assert _task_to_dict(task, current_status=QueueStatus.DOWNLOADING)["reattached"] is flag