Files
shelfmark/tests/newznab/test_handler.py
blades 18a3f0bf44 Newznab capability (#867)
I've added a plugin using the same architecture as the prowlarr plugin
to enable Newznab as a source.
I've tested locally with nzbhydra2 and it all seems to work as intended.
I've added some unit tests for this feature, and found that a couple of
other unit tests weren't behaving so fixed those up while I was at it. I
also ran all of the linters in the makefile against it and fixed those
up, too, so hopefully this should be as clean and as compatible as it
can be.
2026-04-14 09:43:03 +01:00

175 lines
6.9 KiB
Python

"""Unit tests for the Newznab download handler."""
from threading import Event
from unittest.mock import patch
from shelfmark.core.models import DownloadTask
from shelfmark.release_sources.newznab.handler import (
NewznabHandler,
_get_download_url,
_get_protocol,
)
# ── helpers ────────────────────────────────────────────────────────────────────
class ProgressRecorder:
def __init__(self):
self.progress_values: list[float] = []
self.status_updates: list[tuple[str, str | None]] = []
def progress_callback(self, v: float):
self.progress_values.append(v)
def status_callback(self, status: str, message: str | None):
self.status_updates.append((status, message))
@property
def last_status(self) -> str | None:
return self.status_updates[-1][0] if self.status_updates else None
@property
def last_message(self) -> str | None:
return self.status_updates[-1][1] if self.status_updates else None
@property
def statuses(self) -> list[str]:
return [s[0] for s in self.status_updates]
# ── _get_protocol ────────────────────────────────────────────────────────────────
class TestGetProtocol:
def test_explicit_usenet(self):
assert _get_protocol({"protocol": "usenet"}) == "usenet"
def test_explicit_torrent(self):
assert _get_protocol({"protocol": "torrent"}) == "torrent"
def test_magnet_url_infers_torrent(self):
assert _get_protocol({"magnetUrl": "magnet:?xt=urn:btih:abc"}) == "torrent"
def test_torrent_extension_infers_torrent(self):
assert _get_protocol({"downloadUrl": "https://example.com/file.torrent"}) == "torrent"
def test_nzb_extension_infers_usenet(self):
assert _get_protocol({"downloadUrl": "https://example.com/file.nzb"}) == "usenet"
def test_defaults_to_usenet_when_ambiguous(self):
# Newznab is usenet-native; ambiguous URLs default to usenet.
assert _get_protocol({"downloadUrl": "https://example.com/download/123"}) == "usenet"
def test_empty_dict_defaults_to_usenet(self):
assert _get_protocol({}) == "usenet"
# ── _get_download_url ──────────────────────────────────────────────────────────
class TestGetDownloadUrl:
def test_usenet_prefers_download_url(self):
result = {
"protocol": "usenet",
"downloadUrl": "https://example.com/nzb",
"magnetUrl": "magnet:?xt=urn:btih:abc",
}
assert _get_download_url(result) == "https://example.com/nzb"
def test_torrent_prefers_magnet(self):
result = {
"protocol": "torrent",
"downloadUrl": "https://example.com/file.torrent",
"magnetUrl": "magnet:?xt=urn:btih:abc",
}
assert _get_download_url(result) == "magnet:?xt=urn:btih:abc"
def test_falls_back_to_download_url_when_no_magnet(self):
result = {
"protocol": "torrent",
"downloadUrl": "https://example.com/file.torrent",
}
assert _get_download_url(result) == "https://example.com/file.torrent"
# ── error paths ────────────────────────────────────────────────────────────────
class TestHandlerErrors:
def test_cache_miss_returns_error(self):
with patch("shelfmark.release_sources.newznab.handler.get_release", return_value=None):
handler = NewznabHandler()
task = DownloadTask(task_id="missing", source="newznab", title="Book")
recorder = ProgressRecorder()
result = handler.download(
task=task,
cancel_flag=Event(),
progress_callback=recorder.progress_callback,
status_callback=recorder.status_callback,
)
assert result is None
assert recorder.last_status == "error"
assert "cache" in (recorder.last_message or "").lower()
def test_no_download_url_returns_error(self):
with patch(
"shelfmark.release_sources.newznab.handler.get_release",
return_value={"protocol": "usenet", "title": "Book"},
):
handler = NewznabHandler()
task = DownloadTask(task_id="no-url", source="newznab", title="Book")
recorder = ProgressRecorder()
result = handler.download(
task=task,
cancel_flag=Event(),
progress_callback=recorder.progress_callback,
status_callback=recorder.status_callback,
)
assert result is None
assert recorder.last_status == "error"
assert "url" in (recorder.last_message or "").lower()
def test_no_client_configured_returns_error(self):
with (
patch(
"shelfmark.release_sources.newznab.handler.get_release",
return_value={
"protocol": "usenet",
"downloadUrl": "https://example.com/nzb/1",
},
),
patch("shelfmark.release_sources.newznab.handler.get_client", return_value=None),
patch(
"shelfmark.release_sources.newznab.handler.list_configured_clients",
return_value=[],
),
):
handler = NewznabHandler()
task = DownloadTask(task_id="no-client", source="newznab", title="Book")
recorder = ProgressRecorder()
result = handler.download(
task=task,
cancel_flag=Event(),
progress_callback=recorder.progress_callback,
status_callback=recorder.status_callback,
)
assert result is None
assert recorder.last_status == "error"
assert "client" in (recorder.last_message or "").lower()
# ── cancel ─────────────────────────────────────────────────────────────────────
class TestHandlerCancel:
def test_cancel_removes_from_cache(self):
with patch("shelfmark.release_sources.newznab.handler.remove_release") as mock_remove:
result = NewznabHandler().cancel("task-123")
assert result is True
mock_remove.assert_called_once_with("task-123")
def test_cancel_handles_absent_task(self):
with patch("shelfmark.release_sources.newznab.handler.remove_release"):
result = NewznabHandler().cancel("no-such-task")
assert result is True