mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-04-20 05:51:21 -04:00
- Adds `uv`, `ruff`, `pyright`, `vulture` and `pytest-xdist` - Move project, lockfile, docker build etc to uv - Align python tooling on 3.14 - Huge bulk of ruff linter fixes applied. Still in progress but all the core types are now enforced - Update CI and test helpers
284 lines
8.9 KiB
Python
284 lines
8.9 KiB
Python
"""
|
|
Integration tests for the Prowlarr download handler.
|
|
|
|
These tests verify the end-to-end download flow through the Prowlarr handler.
|
|
|
|
Run with: docker compose -f docker-compose.test-clients.yml exec shelfmark uv run pytest /app/tests/prowlarr/test_integration_handler.py -v -m integration
|
|
"""
|
|
|
|
import time
|
|
from threading import Event
|
|
from typing import List, Optional, Tuple
|
|
import pytest
|
|
|
|
from shelfmark.core.config import config
|
|
from shelfmark.core.settings_registry import save_config_file
|
|
from shelfmark.core.models import DownloadTask
|
|
from shelfmark.release_sources.prowlarr.handler import ProwlarrHandler
|
|
from shelfmark.release_sources.prowlarr.utils import get_protocol
|
|
from shelfmark.release_sources.prowlarr.cache import cache_release, get_release, remove_release, _cache
|
|
|
|
|
|
# Test magnet link
|
|
TEST_MAGNET = "magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu-22.04.3-live-server-amd64.iso"
|
|
|
|
|
|
def _setup_transmission_config():
|
|
"""Set up Transmission configuration via config files and refresh config."""
|
|
save_config_file("prowlarr_clients", {
|
|
"PROWLARR_TORRENT_CLIENT": "transmission",
|
|
"TRANSMISSION_URL": "http://transmission:9091",
|
|
"TRANSMISSION_USERNAME": "admin",
|
|
"TRANSMISSION_PASSWORD": "admin",
|
|
"TRANSMISSION_CATEGORY": "test",
|
|
})
|
|
config.refresh()
|
|
|
|
|
|
def _is_transmission_available():
|
|
"""Check if Transmission is available."""
|
|
_setup_transmission_config()
|
|
try:
|
|
from shelfmark.download.clients.transmission import TransmissionClient
|
|
client = TransmissionClient()
|
|
success, _ = client.test_connection()
|
|
return success
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
class ProgressRecorder:
|
|
"""Records progress and status updates during download."""
|
|
|
|
def __init__(self):
|
|
self.progress_values: List[float] = []
|
|
self.status_updates: List[Tuple[str, Optional[str]]] = []
|
|
|
|
def progress_callback(self, progress: float):
|
|
self.progress_values.append(progress)
|
|
|
|
def status_callback(self, status: str, message: Optional[str]):
|
|
self.status_updates.append((status, message))
|
|
|
|
@property
|
|
def last_status(self) -> Optional[str]:
|
|
return self.status_updates[-1][0] if self.status_updates else None
|
|
|
|
@property
|
|
def last_message(self) -> Optional[str]:
|
|
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]
|
|
|
|
|
|
class TestGetProtocol:
|
|
"""Tests for the get_protocol function."""
|
|
|
|
def test_get_protocol_torrent(self):
|
|
"""Test detecting torrent protocol."""
|
|
result = {"protocol": "torrent"}
|
|
assert get_protocol(result) == "torrent"
|
|
|
|
def test_get_protocol_usenet(self):
|
|
"""Test detecting usenet protocol."""
|
|
result = {"protocol": "usenet"}
|
|
assert get_protocol(result) == "usenet"
|
|
|
|
def test_get_protocol_unknown(self):
|
|
"""Test unknown protocol."""
|
|
result = {"protocol": "ftp"}
|
|
assert get_protocol(result) == "unknown"
|
|
|
|
def test_get_protocol_empty(self):
|
|
"""Test empty protocol."""
|
|
result = {}
|
|
assert get_protocol(result) == "unknown"
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestHandlerCacheOperations:
|
|
"""Tests for handler cache-related behavior."""
|
|
|
|
def test_download_fails_without_cached_release(self):
|
|
"""Test that download fails when release is not in cache."""
|
|
_setup_transmission_config()
|
|
handler = ProwlarrHandler()
|
|
|
|
task = DownloadTask(
|
|
task_id="non-existent-id-12345",
|
|
source="prowlarr",
|
|
title="Test Book",
|
|
)
|
|
cancel_flag = Event()
|
|
recorder = ProgressRecorder()
|
|
|
|
result = handler.download(
|
|
task=task,
|
|
cancel_flag=cancel_flag,
|
|
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.lower()
|
|
|
|
def test_download_fails_without_download_url(self):
|
|
"""Test that download fails when release has no download URL."""
|
|
_setup_transmission_config()
|
|
handler = ProwlarrHandler()
|
|
|
|
task_id = "no-url-release-test"
|
|
cache_release(task_id, {
|
|
"protocol": "torrent",
|
|
"title": "Test Release",
|
|
# No downloadUrl or magnetUrl
|
|
})
|
|
|
|
try:
|
|
task = DownloadTask(
|
|
task_id=task_id,
|
|
source="prowlarr",
|
|
title="Test Book",
|
|
)
|
|
cancel_flag = Event()
|
|
recorder = ProgressRecorder()
|
|
|
|
result = handler.download(
|
|
task=task,
|
|
cancel_flag=cancel_flag,
|
|
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.lower()
|
|
finally:
|
|
remove_release(task_id)
|
|
|
|
def test_cancel_removes_from_cache(self):
|
|
"""Test that cancel removes release from cache."""
|
|
handler = ProwlarrHandler()
|
|
|
|
task_id = "cancel-test-id-unique"
|
|
cache_release(task_id, {"title": "Test"})
|
|
|
|
assert get_release(task_id) is not None
|
|
|
|
result = handler.cancel(task_id)
|
|
|
|
assert result is True
|
|
assert get_release(task_id) is None
|
|
|
|
def test_cancel_handles_missing_task(self):
|
|
"""Test that cancel handles non-existent task gracefully."""
|
|
handler = ProwlarrHandler()
|
|
|
|
result = handler.cancel("definitely-non-existent-task-id")
|
|
|
|
assert result is True # Should still return True
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def transmission_available():
|
|
"""Check if Transmission is available, skip if not."""
|
|
if not _is_transmission_available():
|
|
pytest.skip("Transmission not available - ensure docker-compose.test-clients.yml is running")
|
|
return True
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestProwlarrHandlerWithTransmission:
|
|
"""Integration tests for ProwlarrHandler with Transmission."""
|
|
|
|
def test_download_starts_and_can_be_cancelled(self, transmission_available):
|
|
"""Test that download starts and can be cancelled."""
|
|
_setup_transmission_config()
|
|
handler = ProwlarrHandler()
|
|
|
|
# Cache a valid release
|
|
task_id = f"test-cancel-release-{time.time()}"
|
|
cache_release(task_id, {
|
|
"protocol": "torrent",
|
|
"title": "Ubuntu Test ISO",
|
|
"magnetUrl": TEST_MAGNET,
|
|
})
|
|
|
|
task = DownloadTask(
|
|
task_id=task_id,
|
|
source="prowlarr",
|
|
title="Ubuntu Test ISO",
|
|
)
|
|
cancel_flag = Event()
|
|
recorder = ProgressRecorder()
|
|
|
|
# Start download in a thread and cancel after a short delay
|
|
import threading
|
|
|
|
def cancel_after_delay():
|
|
time.sleep(4) # Let it start
|
|
cancel_flag.set()
|
|
|
|
cancel_thread = threading.Thread(target=cancel_after_delay)
|
|
cancel_thread.start()
|
|
|
|
result = handler.download(
|
|
task=task,
|
|
cancel_flag=cancel_flag,
|
|
progress_callback=recorder.progress_callback,
|
|
status_callback=recorder.status_callback,
|
|
)
|
|
|
|
cancel_thread.join()
|
|
|
|
# Download was cancelled
|
|
assert result is None
|
|
# Should have some status updates
|
|
assert len(recorder.status_updates) > 0
|
|
# Should see resolving or downloading status (not just error)
|
|
assert "resolving" in recorder.statuses or "downloading" in recorder.statuses or "cancelled" in recorder.statuses
|
|
|
|
def test_handler_sends_to_transmission(self, transmission_available):
|
|
"""Test that handler properly sends downloads to Transmission."""
|
|
_setup_transmission_config()
|
|
handler = ProwlarrHandler()
|
|
|
|
task_id = f"transmission-test-{time.time()}"
|
|
cache_release(task_id, {
|
|
"protocol": "torrent",
|
|
"title": "Integration Test Torrent",
|
|
"magnetUrl": TEST_MAGNET,
|
|
})
|
|
|
|
task = DownloadTask(
|
|
task_id=task_id,
|
|
source="prowlarr",
|
|
title="Integration Test Torrent",
|
|
)
|
|
cancel_flag = Event()
|
|
recorder = ProgressRecorder()
|
|
|
|
import threading
|
|
|
|
def cancel_soon():
|
|
time.sleep(5)
|
|
cancel_flag.set()
|
|
|
|
t = threading.Thread(target=cancel_soon)
|
|
t.start()
|
|
|
|
handler.download(
|
|
task=task,
|
|
cancel_flag=cancel_flag,
|
|
progress_callback=recorder.progress_callback,
|
|
status_callback=recorder.status_callback,
|
|
)
|
|
|
|
t.join()
|
|
|
|
# Should have seen resolving status (means it tried to send to client)
|
|
assert "resolving" in recorder.statuses or "downloading" in recorder.statuses
|