mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-06-11 23:04:41 -04:00
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.
175 lines
6.9 KiB
Python
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
|