mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 22:05:50 +01:00
## Backport bug fixes from `NemesisHubris/litfinder` Forwards a curated set of bug fixes from [NemesisHubris/litfinder](https://github.com/NemesisHubris/litfinder) — a community fork of this project — that address open issues here. All commits preserve original authorship via `git cherry-pick`; this PR is a backport rather than original work. Each fix has been reviewed locally, lint/format-cleaned to match this repo's existing ruff config, and verified with the test suite. Rebrand strings, license switches, and features have been deliberately excluded. ### Upstream issues addressed - **#999** — Mirror URLs with query params no longer break search requests (strip query string/fragment in `normalize_http_url`) - **#956** — Apprise notifications now respect the configured proxy (proxy env vars injected before dispatch) - **#1025** — rTorrent: separate `RTORRENT_AUDIOBOOK_LABEL` setting, falls back to book label if unset - **#1010** — Stop button in Activity no longer makes the panel disappear (snapshot refresh on cancel) - **#1021** — Anna's Archive slow-download countdown now caps retries instead of looping forever - **#1040** — Empty destination directory cleaned up when write probe fails - **PR #1031** — Language detection from Anna's Archive distant path when listing metadata is missing ### Additional fixes (no open issue but clear bugs) - **fix: Python 2 `except` syntax across 27 files** — `except X, Y:` is a SyntaxError in Python 3 and prevents affected modules from importing at runtime. Mechanical sweep to `except (X, Y):`. - **fix(abb): info hash validation with magnet fallback** — adds SHA-1/SHA-256 hex validation on extracted info hashes; falls back to scanning the full page for a magnet link (e.g. posted in comments) when the table value is malformed. Also extends the exact-phrase fallback to manual queries and defaults the ABB listing language to `en` when missing, preventing valid results from being hidden by the language filter. Includes a small test-fixture fix (`test(abb): use valid hex info hashes in scraper test fixtures`) since the existing fixtures used non-hex placeholders that the new validation correctly rejects. - **fix: Anna's Archive title parser** — handles nested edition spans and filters `lgli` catalog descriptor entries (e.g. "Book/Online Audio") that were polluting search results. ### Deliberately not included - LitFinder rebranding (UI strings, Apprise app ID, logo). The `fix: three upstream bugs` commit (#999/#956/#1025) was cherry-picked with Apprise app-id, description, and logo-URL strings reverted from "LitFinder" back to "Shelfmark"; noted in the commit body. - Features from the LitFinder fork (multi-variant title search, multi-book flat-folder grouping, fuzzy text matching, "Leave in Place" output handler, admin display name, custom-source plugin system). These are larger behavior changes that each warrant their own focused review — happy to send any of them separately if of interest. - LitFinder-specific test environment and CI infrastructure. ### Verification - Backend: **1879 passed**, 96 skipped (1 preexisting failure on `seleniumbase`-dependent test in local venv; runs fine in the standard Docker image with the `browser` extra) - Lint, format, dead-code: all clean against this repo's existing ruff/vulture config - One follow-up cleanup commit (`style: ruff lint and format fixes for ported commits`) brings the cherry-picked code into compliance with this repo's ruff settings — no behavior changes there ### Etiquette / credit Per-commit authorship preserved by cherry-pick. The only edits to the original commits are: - `fix: three upstream bugs` — Apprise rebrand strings reverted to "Shelfmark" (noted in commit body, original author retained as `Co-Authored-By` via cherry-pick) - One follow-up `style:` commit for ruff config alignment Big thanks to [@NemesisHubris](https://github.com/NemesisHubris) for the original work in LitFinder; this PR exists to make sure these fixes reach Shelfmark's wider user base. Happy to revise scope, split into smaller PRs, or split off the Py2 cleanup separately if that's preferable. --------- Co-authored-by: NemesisHubris <155838970+NemesisHubris@users.noreply.github.com> Co-authored-by: CaliBrain <calibrain@l4n.xyz>
778 lines
29 KiB
Python
778 lines
29 KiB
Python
"""
|
|
Unit tests for the Transmission client.
|
|
|
|
These tests mock the transmission-rpc library to test the client logic
|
|
without requiring a running Transmission instance.
|
|
"""
|
|
|
|
import sys
|
|
import types
|
|
from datetime import timedelta
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from shelfmark.download.clients import DownloadStatus
|
|
|
|
|
|
class MockTorrentStatus:
|
|
"""Mock for Transmission's torrent status enum."""
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
|
|
class MockTorrent:
|
|
"""Mock Transmission torrent object."""
|
|
|
|
def __init__(
|
|
self,
|
|
hash_string="abc123",
|
|
name="Test Torrent",
|
|
percent_done=0.5,
|
|
status="downloading",
|
|
rate_download=1024000,
|
|
eta=None,
|
|
download_dir="/downloads",
|
|
):
|
|
self.hashString = hash_string
|
|
self.name = name
|
|
self.percent_done = percent_done
|
|
self.status = MockTorrentStatus(status)
|
|
self.rate_download = rate_download
|
|
self.download_dir = download_dir
|
|
if eta is not None:
|
|
self.eta = timedelta(seconds=eta)
|
|
else:
|
|
self.eta = None
|
|
|
|
|
|
class MockSession:
|
|
"""Mock Transmission session object."""
|
|
|
|
def __init__(self, version="4.0.0"):
|
|
self.version = version
|
|
|
|
|
|
def make_config_getter(values):
|
|
"""Create a config.get function that returns values from a dict."""
|
|
|
|
def getter(key, default=""):
|
|
return values.get(key, default)
|
|
|
|
return getter
|
|
|
|
|
|
def create_mock_transmission_rpc_module():
|
|
"""Create a mock transmission_rpc module."""
|
|
mock_module = MagicMock()
|
|
mock_module.Client = MagicMock()
|
|
return mock_module
|
|
|
|
|
|
class TestTransmissionClientIsConfigured:
|
|
"""Tests for TransmissionClient.is_configured()."""
|
|
|
|
def test_is_configured_when_all_set(self, monkeypatch):
|
|
"""Test is_configured returns True when properly configured."""
|
|
config_values = {
|
|
"PROWLARR_TORRENT_CLIENT": "transmission",
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
assert TransmissionClient.is_configured() is True
|
|
|
|
def test_is_configured_wrong_client(self, monkeypatch):
|
|
"""Test is_configured returns False when different client selected."""
|
|
config_values = {
|
|
"PROWLARR_TORRENT_CLIENT": "qbittorrent",
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
assert TransmissionClient.is_configured() is False
|
|
|
|
def test_is_configured_no_url(self, monkeypatch):
|
|
"""Test is_configured returns False when URL not set."""
|
|
config_values = {
|
|
"PROWLARR_TORRENT_CLIENT": "transmission",
|
|
"TRANSMISSION_URL": "",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
assert TransmissionClient.is_configured() is False
|
|
|
|
|
|
class TestTransmissionClientTestConnection:
|
|
"""Tests for TransmissionClient.test_connection()."""
|
|
|
|
def test_init_passes_https_protocol(self, monkeypatch):
|
|
"""Test HTTPS URL causes protocol=https to be passed to transmission-rpc Client."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "https://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_session.return_value = MockSession(version="4.0.5")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
TransmissionClient()
|
|
assert mock_transmission_rpc.Client.call_args.kwargs.get("protocol") == "https"
|
|
|
|
def test_init_applies_certificate_validation_to_session(self, monkeypatch):
|
|
"""Test Transmission client applies verify mode onto transmission-rpc session."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "https://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_http_session = MagicMock()
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance._http_session = mock_http_session
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients import transmission as transmission_module
|
|
|
|
monkeypatch.setattr(transmission_module, "get_ssl_verify", lambda _url: False)
|
|
transmission_module.TransmissionClient()
|
|
|
|
assert mock_http_session.verify is False
|
|
|
|
def test_init_disables_verify_before_constructor_bootstrap(self, monkeypatch):
|
|
"""verify=False must be in place before transmission-rpc constructor bootstraps RPC session."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "https://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
transmission_pkg = types.ModuleType("transmission_rpc")
|
|
transmission_pkg.__path__ = [] # Mark as package for submodule imports.
|
|
transmission_client_mod = types.ModuleType("transmission_rpc.client")
|
|
|
|
def _base_session_factory():
|
|
return types.SimpleNamespace(verify=True)
|
|
|
|
transmission_client_mod.requests = types.SimpleNamespace(Session=_base_session_factory)
|
|
|
|
def _fake_client_ctor(**_kwargs):
|
|
bootstrap_session = transmission_client_mod.requests.Session()
|
|
if bootstrap_session.verify is not False:
|
|
raise RuntimeError("verify not disabled during constructor bootstrap")
|
|
client = MagicMock()
|
|
client._http_session = bootstrap_session
|
|
client.get_session.return_value = MockSession(version="4.0.5")
|
|
return client
|
|
|
|
transmission_pkg.Client = _fake_client_ctor
|
|
transmission_pkg.client = transmission_client_mod
|
|
|
|
with patch.dict(
|
|
"sys.modules",
|
|
{
|
|
"transmission_rpc": transmission_pkg,
|
|
"transmission_rpc.client": transmission_client_mod,
|
|
},
|
|
):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients import transmission as transmission_module
|
|
|
|
monkeypatch.setattr(transmission_module, "get_ssl_verify", lambda _url: False)
|
|
client = transmission_module.TransmissionClient()
|
|
assert client._client._http_session.verify is False
|
|
|
|
def test_test_connection_success(self, monkeypatch):
|
|
"""Test successful connection."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_session.return_value = MockSession(version="4.0.5")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
# Force reimport to use mock
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
success, message = client.test_connection()
|
|
|
|
assert success is True
|
|
assert "4.0.5" in message
|
|
|
|
def test_test_connection_failure(self, monkeypatch):
|
|
"""Test failed connection."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "wrong",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_session.side_effect = RuntimeError("Connection refused")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
success, message = client.test_connection()
|
|
|
|
assert success is False
|
|
assert "failed" in message.lower()
|
|
|
|
|
|
class TestTransmissionClientGetStatus:
|
|
"""Tests for TransmissionClient.get_status()."""
|
|
|
|
def test_get_status_downloading(self, monkeypatch):
|
|
"""Test status for downloading torrent."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(percent_done=0.5, status="downloading", rate_download=1024000)
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("abc123")
|
|
|
|
assert status.progress == 50.0
|
|
assert status.state_value == "downloading"
|
|
assert status.complete is False
|
|
assert status.download_speed == 1024000
|
|
|
|
def test_get_status_seeding(self, monkeypatch):
|
|
"""Test status for seeding (complete) torrent."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(
|
|
percent_done=1.0,
|
|
status="seeding",
|
|
download_dir="/downloads",
|
|
)
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("abc123")
|
|
|
|
assert status.progress == 100.0
|
|
assert status.complete is True
|
|
assert "/downloads/Test Torrent" in status.file_path
|
|
|
|
def test_get_status_stopped_treated_as_complete(self, monkeypatch):
|
|
"""Regression: torrents stopped after seeding ratio/idle limit must show complete."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(
|
|
percent_done=1.0,
|
|
status="stopped",
|
|
download_dir="/downloads",
|
|
)
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import TransmissionClient
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("abc123")
|
|
|
|
assert status.complete is True
|
|
assert status.progress == 100.0
|
|
|
|
def test_get_status_not_found(self, monkeypatch):
|
|
"""Test status for non-existent torrent."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.side_effect = KeyError("not found")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("nonexistent")
|
|
|
|
assert status.state_value == "error"
|
|
assert "not found" in status.message.lower()
|
|
|
|
def test_get_status_paused(self, monkeypatch):
|
|
"""Test status for paused torrent."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(percent_done=0.3, status="stopped")
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("abc123")
|
|
|
|
assert status.state_value == "paused"
|
|
|
|
def test_get_status_with_eta(self, monkeypatch):
|
|
"""Test status includes ETA when available."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(percent_done=0.5, status="downloading", eta=3600)
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
status = client.get_status("abc123")
|
|
|
|
assert status.eta == 3600
|
|
|
|
|
|
class TestTransmissionClientAddDownload:
|
|
"""Tests for TransmissionClient.add_download()."""
|
|
|
|
def test_add_download_magnet_success(self, monkeypatch):
|
|
"""Test adding a magnet link."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(hash_string="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0")
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.add_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
magnet = "magnet:?xt=urn:btih:3B245504CF5F11BBDBE1201CEA6A6BF45AEE1BC0&dn=test"
|
|
result = client.add_download(magnet, "Test Download")
|
|
|
|
assert result == "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
|
mock_client_instance.add_torrent.assert_called_once()
|
|
|
|
def test_add_download_uses_labels(self, monkeypatch):
|
|
"""Test that add_download sets labels/category."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "mybooks",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(hash_string="abc123")
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.add_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
magnet = "magnet:?xt=urn:btih:abc123&dn=test"
|
|
client.add_download(magnet, "Test")
|
|
|
|
# Verify labels were passed
|
|
call_kwargs = mock_client_instance.add_torrent.call_args
|
|
assert call_kwargs.kwargs.get("labels") == ["mybooks"]
|
|
|
|
def test_add_download_uses_configured_download_dir(self, monkeypatch):
|
|
"""Test that add_download passes configured download directory."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "mybooks",
|
|
"TRANSMISSION_DOWNLOAD_DIR": "/downloads/books",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(hash_string="abc123")
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.add_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
magnet = "magnet:?xt=urn:btih:abc123&dn=test"
|
|
client.add_download(magnet, "Test")
|
|
|
|
call_kwargs = mock_client_instance.add_torrent.call_args
|
|
assert call_kwargs.kwargs.get("labels") == ["mybooks"]
|
|
assert call_kwargs.kwargs.get("download_dir") == "/downloads/books"
|
|
|
|
|
|
class TestTransmissionClientRemove:
|
|
"""Tests for TransmissionClient.remove()."""
|
|
|
|
def test_remove_success(self, monkeypatch):
|
|
"""Test successful torrent removal."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
result = client.remove("abc123", delete_files=True)
|
|
|
|
assert result is True
|
|
mock_client_instance.remove_torrent.assert_called_once_with("abc123", delete_data=True)
|
|
|
|
def test_remove_failure(self, monkeypatch):
|
|
"""Test failed torrent removal."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.remove_torrent.side_effect = RuntimeError("Not found")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
result = client.remove("abc123")
|
|
|
|
assert result is False
|
|
|
|
|
|
class TestTransmissionClientFindExisting:
|
|
"""Tests for TransmissionClient.find_existing()."""
|
|
|
|
def test_find_existing_found(self, monkeypatch):
|
|
"""Test finding existing torrent by magnet hash."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_torrent = MockTorrent(
|
|
hash_string="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0",
|
|
percent_done=0.5,
|
|
status="downloading",
|
|
)
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.return_value = mock_torrent
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
magnet = "magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=test"
|
|
result = client.find_existing(magnet)
|
|
|
|
assert result is not None
|
|
download_id, status = result
|
|
assert download_id == "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
|
assert isinstance(status, DownloadStatus)
|
|
|
|
def test_find_existing_not_found(self, monkeypatch):
|
|
"""Test finding non-existent torrent."""
|
|
config_values = {
|
|
"TRANSMISSION_URL": "http://localhost:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "password",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
}
|
|
monkeypatch.setattr(
|
|
"shelfmark.download.clients.transmission.config.get",
|
|
make_config_getter(config_values),
|
|
)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_client_instance.get_torrent.side_effect = KeyError("not found")
|
|
|
|
mock_transmission_rpc = create_mock_transmission_rpc_module()
|
|
mock_transmission_rpc.Client.return_value = mock_client_instance
|
|
|
|
with patch.dict("sys.modules", {"transmission_rpc": mock_transmission_rpc}):
|
|
if "shelfmark.download.clients.transmission" in sys.modules:
|
|
del sys.modules["shelfmark.download.clients.transmission"]
|
|
|
|
from shelfmark.download.clients.transmission import (
|
|
TransmissionClient,
|
|
)
|
|
|
|
client = TransmissionClient()
|
|
magnet = "magnet:?xt=urn:btih:abc123&dn=test"
|
|
result = client.find_existing(magnet)
|
|
|
|
assert result is None
|