Files
shelfmark/tests/prowlarr/test_deluge_client.py
T
splitsec2 1a5b37d9d3 fix(deluge): send seeding ratio limit under Deluge's own keys (#1367)
Deluge's per-torrent options are `stop_at_ratio` (bool) and `stop_ratio`
(float), and `torrentmanager` checks `options['stop_at_ratio'] and
get_ratio() >= options['stop_ratio']`. We were putting the indexer's
float into `stop_at_ratio`, which only switched stopping on and left the
daemon's global ratio (default 2.0) as the one actually enforced. A
`ratio_limit` of 0 turned stopping off entirely. `stop_at_ratio_enabled`
is not a Deluge option at all.

Deluge has no per-torrent seeding time limit, `seed_time_limit` is a
global core preference, so the value is logged as unapplied instead of
sent as a key the daemon drops.

qBittorrent and Transmission already honour both indexer limits, so this
removes a silent difference between clients.

## Verification

- `tests/prowlarr/test_deluge_client.py`: the ratio arrives as
`stop_ratio` with `stop_at_ratio` set, and no key Deluge does not define
is sent. Both fail on current main and pass here.
- Full suite (3165), ruff, ruff format, basedpyright, vulture green.
2026-09-20 23:06:14 -04:00

307 lines
12 KiB
Python

"""Unit tests for the Deluge client."""
from unittest.mock import MagicMock, patch
from shelfmark.download.clients import DownloadStatus
from shelfmark.download.clients.torrent_utils import TorrentInfo
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 test_set_category_uses_label_plugin(monkeypatch):
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient.__new__(DelugeClient)
monkeypatch.setattr(client, "_ensure_connected", MagicMock())
monkeypatch.setattr(client, "_try_set_label", MagicMock(return_value=True))
assert client.set_category("abc123", "imported") is True
client._try_set_label.assert_called_once_with("abc123", "imported")
class TestDelugeClientAddDownload:
"""Tests for DelugeClient.add_download()."""
def test_add_download_uses_configured_download_dir(self, monkeypatch):
"""Add torrent should pass configured download location option."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
"DELUGE_CATEGORY": "books",
"DELUGE_DOWNLOAD_DIR": "/downloads/books",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12")
monkeypatch.setattr(client, "_rpc_call", mock_rpc_call)
mock_try_set_label = MagicMock()
monkeypatch.setattr(client, "_try_set_label", mock_try_set_label)
magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test"
with patch(
"shelfmark.download.clients.deluge.extract_torrent_info", autospec=True
) as mock_extract:
mock_extract.return_value = TorrentInfo(
info_hash="abcdef1234567890abcdef1234567890abcdef12",
torrent_data=None,
is_magnet=True,
magnet_url=magnet,
)
result = client.add_download(magnet, "Test")
assert result == "abcdef1234567890abcdef1234567890abcdef12"
mock_rpc_call.assert_called_once_with(
"core.add_torrent_magnet",
magnet,
{"download_location": "/downloads/books"},
)
mock_try_set_label.assert_called_once_with(
"abcdef1234567890abcdef1234567890abcdef12",
"books",
)
def test_add_download_uses_empty_options_without_download_dir(self, monkeypatch):
"""Add torrent should keep options empty when no directory is configured."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
"DELUGE_CATEGORY": "books",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12")
monkeypatch.setattr(client, "_rpc_call", mock_rpc_call)
monkeypatch.setattr(client, "_try_set_label", MagicMock())
magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test"
with patch(
"shelfmark.download.clients.deluge.extract_torrent_info", autospec=True
) as mock_extract:
mock_extract.return_value = TorrentInfo(
info_hash="abcdef1234567890abcdef1234567890abcdef12",
torrent_data=None,
is_magnet=True,
magnet_url=magnet,
)
client.add_download(magnet, "Test")
mock_rpc_call.assert_called_once_with(
"core.add_torrent_magnet",
magnet,
{},
)
def test_add_download_sends_ratio_limit_as_stop_ratio(self, monkeypatch):
"""Ratio limits should use Deluge's stop_ratio float plus the stop_at_ratio switch."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
"DELUGE_CATEGORY": "books",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12")
monkeypatch.setattr(client, "_rpc_call", mock_rpc_call)
monkeypatch.setattr(client, "_try_set_label", MagicMock())
magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test"
with patch(
"shelfmark.download.clients.deluge.extract_torrent_info", autospec=True
) as mock_extract:
mock_extract.return_value = TorrentInfo(
info_hash="abcdef1234567890abcdef1234567890abcdef12",
torrent_data=None,
is_magnet=True,
magnet_url=magnet,
)
client.add_download(magnet, "Test", ratio_limit=1.5)
mock_rpc_call.assert_called_once_with(
"core.add_torrent_magnet",
magnet,
{"stop_ratio": 1.5, "stop_at_ratio": True},
)
def test_add_download_skips_options_deluge_does_not_define(self, monkeypatch):
"""Deluge has no per-torrent seeding time limit, so no unknown keys are sent."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
"DELUGE_CATEGORY": "books",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12")
monkeypatch.setattr(client, "_rpc_call", mock_rpc_call)
monkeypatch.setattr(client, "_try_set_label", MagicMock())
magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test"
with patch(
"shelfmark.download.clients.deluge.extract_torrent_info", autospec=True
) as mock_extract:
mock_extract.return_value = TorrentInfo(
info_hash="abcdef1234567890abcdef1234567890abcdef12",
torrent_data=None,
is_magnet=True,
magnet_url=magnet,
)
client.add_download(magnet, "Test", ratio_limit=1.5, seeding_time_limit=120)
options = mock_rpc_call.call_args.args[2]
assert "stop_at_ratio_enabled" not in options
assert "seed_time_limit" not in options
class TestDelugeClientErrors:
"""Tests for Deluge error handling fallbacks."""
def test_test_connection_failure_returns_false(self, monkeypatch):
"""Operational client errors should return a failure tuple."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(
client, "_ensure_connected", MagicMock(side_effect=RuntimeError("offline"))
)
success, message = client.test_connection()
assert success is False
assert "offline" in message
assert client._authenticated is False
assert client._connected is False
def test_get_status_failure_returns_error_status(self, monkeypatch):
"""Status lookup failures should degrade to DownloadStatus.error()."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
monkeypatch.setattr(
client, "_rpc_call", MagicMock(side_effect=RuntimeError("status failed"))
)
status = client.get_status("torrent-id")
assert isinstance(status, DownloadStatus)
assert status.state_value == "error"
assert "status failed" in status.message
def test_remove_failure_returns_false(self, monkeypatch):
"""Removal failures should return False rather than raising."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
monkeypatch.setattr(
client, "_rpc_call", MagicMock(side_effect=RuntimeError("remove failed"))
)
assert client.remove("torrent-id") is False
def test_find_existing_failure_returns_none_and_resets_connection(self, monkeypatch):
"""Lookup failures should clear client state and return no match."""
config_values = {
"DELUGE_HOST": "http://localhost",
"DELUGE_PORT": "8112",
"DELUGE_PASSWORD": "password",
}
monkeypatch.setattr(
"shelfmark.download.clients.deluge.config.get",
make_config_getter(config_values),
)
from shelfmark.download.clients.deluge import DelugeClient
client = DelugeClient()
client._authenticated = True
client._connected = True
monkeypatch.setattr(client, "_ensure_connected", lambda: None)
monkeypatch.setattr(
client, "_rpc_call", MagicMock(side_effect=RuntimeError("lookup failed"))
)
magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test"
with patch(
"shelfmark.download.clients.deluge.extract_torrent_info", autospec=True
) as mock_extract:
mock_extract.return_value = TorrentInfo(
info_hash="abcdef1234567890abcdef1234567890abcdef12",
torrent_data=None,
is_magnet=True,
magnet_url=magnet,
)
assert client.find_existing(magnet) is None
assert client._authenticated is False
assert client._connected is False