mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-04-29 18:12:57 -04:00
- URL normalization (WIP) for external clients / prowlarr / booklore URLs used. - More robust handling of Windows path directories in mapping - UI tweaks - Compose clean-ups
379 lines
14 KiB
Python
379 lines
14 KiB
Python
"""Tests for remote path mappings.
|
|
|
|
This focuses on integration of mapping logic into the Prowlarr handler.
|
|
"""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from threading import Event
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from shelfmark.core.models import DownloadTask
|
|
from shelfmark.release_sources.prowlarr.clients import DownloadState, DownloadStatus
|
|
from shelfmark.release_sources.prowlarr.handler import ProwlarrHandler
|
|
|
|
|
|
class ProgressRecorder:
|
|
def __init__(self):
|
|
self.progress_values = []
|
|
self.status_updates = []
|
|
|
|
def progress_callback(self, progress: float):
|
|
self.progress_values.append(progress)
|
|
|
|
def status_callback(self, status: str, message: str | None):
|
|
self.status_updates.append((status, message))
|
|
|
|
|
|
def test_remaps_completed_path_when_remote_path_missing():
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
local_file = Path(tmp_dir) / "local" / "book.epub"
|
|
local_file.parent.mkdir(parents=True)
|
|
local_file.write_text("test content")
|
|
|
|
remote_path = "/remote/downloads/book.epub"
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.name = "qbittorrent"
|
|
mock_client.find_existing.return_value = None
|
|
mock_client.add_download.return_value = "download_id"
|
|
mock_client.get_status.return_value = DownloadStatus(
|
|
progress=100,
|
|
state=DownloadState.COMPLETE,
|
|
message="Complete",
|
|
complete=True,
|
|
file_path=remote_path,
|
|
)
|
|
mock_client.get_download_path.return_value = remote_path
|
|
|
|
def config_get(key: str, default=""):
|
|
if key == "PROWLARR_REMOTE_PATH_MAPPINGS":
|
|
return [
|
|
{
|
|
"host": "qbittorrent",
|
|
"remotePath": "/remote/downloads",
|
|
"localPath": str(local_file.parent),
|
|
}
|
|
]
|
|
return default
|
|
|
|
with patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_release",
|
|
return_value={
|
|
"protocol": "torrent",
|
|
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
|
},
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_client",
|
|
return_value=mock_client,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.remove_release",
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.config.get",
|
|
side_effect=config_get,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL",
|
|
0.01,
|
|
):
|
|
handler = ProwlarrHandler()
|
|
task = DownloadTask(task_id="poll-mapping-test", 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 == str(local_file)
|
|
assert task.original_download_path == str(local_file)
|
|
|
|
|
|
def test_remap_prefers_mapping_when_original_exists():
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
remote_dir = Path(tmp_dir) / "remote" / "downloads"
|
|
remote_dir.mkdir(parents=True)
|
|
remote_file = remote_dir / "book.epub"
|
|
remote_file.write_text("remote content")
|
|
|
|
local_dir = Path(tmp_dir) / "local" / "downloads"
|
|
local_dir.mkdir(parents=True)
|
|
local_file = local_dir / "book.epub"
|
|
local_file.write_text("local content")
|
|
|
|
remote_path = str(remote_file)
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.name = "qbittorrent"
|
|
mock_client.find_existing.return_value = None
|
|
mock_client.add_download.return_value = "download_id"
|
|
mock_client.get_status.return_value = DownloadStatus(
|
|
progress=100,
|
|
state=DownloadState.COMPLETE,
|
|
message="Complete",
|
|
complete=True,
|
|
file_path=remote_path,
|
|
)
|
|
mock_client.get_download_path.return_value = remote_path
|
|
|
|
def config_get(key: str, default=""):
|
|
if key == "PROWLARR_REMOTE_PATH_MAPPINGS":
|
|
return [
|
|
{
|
|
"host": "qbittorrent",
|
|
"remotePath": str(remote_dir),
|
|
"localPath": str(local_dir),
|
|
}
|
|
]
|
|
return default
|
|
|
|
with patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_release",
|
|
return_value={
|
|
"protocol": "torrent",
|
|
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
|
},
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_client",
|
|
return_value=mock_client,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.remove_release",
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.config.get",
|
|
side_effect=config_get,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL",
|
|
0.01,
|
|
):
|
|
handler = ProwlarrHandler()
|
|
task = DownloadTask(task_id="poll-mapping-prefer", 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 == str(local_file)
|
|
assert task.original_download_path == str(local_file)
|
|
|
|
|
|
def test_remap_fails_when_mapping_exists_but_path_missing():
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
remote_dir = Path(tmp_dir) / "remote" / "downloads"
|
|
remote_dir.mkdir(parents=True)
|
|
remote_file = remote_dir / "book.epub"
|
|
remote_file.write_text("remote content")
|
|
|
|
local_dir = Path(tmp_dir) / "local" / "downloads"
|
|
local_dir.mkdir(parents=True)
|
|
local_file = local_dir / "book.epub"
|
|
remote_path = str(remote_file)
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.name = "qbittorrent"
|
|
mock_client.find_existing.return_value = None
|
|
mock_client.add_download.return_value = "download_id"
|
|
mock_client.get_status.return_value = DownloadStatus(
|
|
progress=100,
|
|
state=DownloadState.COMPLETE,
|
|
message="Complete",
|
|
complete=True,
|
|
file_path=remote_path,
|
|
)
|
|
mock_client.get_download_path.return_value = remote_path
|
|
|
|
def config_get(key: str, default=""):
|
|
if key == "PROWLARR_REMOTE_PATH_MAPPINGS":
|
|
return [
|
|
{
|
|
"host": "qbittorrent",
|
|
"remotePath": str(remote_dir),
|
|
"localPath": str(local_dir),
|
|
}
|
|
]
|
|
return default
|
|
|
|
with patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_release",
|
|
return_value={
|
|
"protocol": "torrent",
|
|
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
|
},
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_client",
|
|
return_value=mock_client,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.remove_release",
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.config.get",
|
|
side_effect=config_get,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL",
|
|
0.01,
|
|
):
|
|
handler = ProwlarrHandler()
|
|
task = DownloadTask(task_id="poll-mapping-missing", 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 any(status == "error" for status, _ in recorder.status_updates)
|
|
assert not local_file.exists()
|
|
|
|
|
|
def test_remaps_windows_path_to_linux():
|
|
"""Test that Windows paths from external download clients are correctly remapped."""
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
# Create a local file that represents the mounted path
|
|
local_file = Path(tmp_dir) / "downloads" / "Le Fay" / "book.epub"
|
|
local_file.parent.mkdir(parents=True)
|
|
local_file.write_text("test content")
|
|
|
|
# Windows path as reported by qBittorrent running on Windows
|
|
windows_path = r"D:\Torrents\Le Fay\book.epub"
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.name = "qbittorrent"
|
|
mock_client.find_existing.return_value = None
|
|
mock_client.add_download.return_value = "download_id"
|
|
mock_client.get_status.return_value = DownloadStatus(
|
|
progress=100,
|
|
state=DownloadState.COMPLETE,
|
|
message="Complete",
|
|
complete=True,
|
|
file_path=windows_path,
|
|
)
|
|
mock_client.get_download_path.return_value = windows_path
|
|
|
|
def config_get(key: str, default=""):
|
|
if key == "PROWLARR_REMOTE_PATH_MAPPINGS":
|
|
return [
|
|
{
|
|
"host": "qbittorrent",
|
|
# User enters Windows path in settings (with backslashes)
|
|
"remotePath": r"D:\Torrents",
|
|
"localPath": str(Path(tmp_dir) / "downloads"),
|
|
}
|
|
]
|
|
return default
|
|
|
|
with patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_release",
|
|
return_value={
|
|
"protocol": "torrent",
|
|
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
|
},
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_client",
|
|
return_value=mock_client,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.remove_release",
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.config.get",
|
|
side_effect=config_get,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL",
|
|
0.01,
|
|
):
|
|
handler = ProwlarrHandler()
|
|
task = DownloadTask(task_id="windows-path-test", 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 == str(local_file)
|
|
assert task.original_download_path == str(local_file)
|
|
|
|
|
|
def test_windows_path_case_insensitive_matching():
|
|
"""Test that Windows path matching is case-insensitive.
|
|
|
|
Users may enter paths in different case than what the download client reports.
|
|
For example, user enters 'd:\\torrents' but qBittorrent reports 'D:\\Torrents'.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
# Create a local file that represents the mounted path
|
|
local_file = Path(tmp_dir) / "downloads" / "Le Fay" / "book.epub"
|
|
local_file.parent.mkdir(parents=True)
|
|
local_file.write_text("test content")
|
|
|
|
# qBittorrent reports path with different case than user's setting
|
|
windows_path = r"D:\Torrents\Le Fay\book.epub" # Mixed case
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.name = "qbittorrent"
|
|
mock_client.find_existing.return_value = None
|
|
mock_client.add_download.return_value = "download_id"
|
|
mock_client.get_status.return_value = DownloadStatus(
|
|
progress=100,
|
|
state=DownloadState.COMPLETE,
|
|
message="Complete",
|
|
complete=True,
|
|
file_path=windows_path,
|
|
)
|
|
mock_client.get_download_path.return_value = windows_path
|
|
|
|
def config_get(key: str, default=""):
|
|
if key == "PROWLARR_REMOTE_PATH_MAPPINGS":
|
|
return [
|
|
{
|
|
"host": "qbittorrent",
|
|
# User enters lowercase (as shown in UI screenshot)
|
|
"remotePath": r"d:\torrents",
|
|
"localPath": str(Path(tmp_dir) / "downloads"),
|
|
}
|
|
]
|
|
return default
|
|
|
|
with patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_release",
|
|
return_value={
|
|
"protocol": "torrent",
|
|
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
|
},
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.get_client",
|
|
return_value=mock_client,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.remove_release",
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.config.get",
|
|
side_effect=config_get,
|
|
), patch(
|
|
"shelfmark.release_sources.prowlarr.handler.POLL_INTERVAL",
|
|
0.01,
|
|
):
|
|
handler = ProwlarrHandler()
|
|
task = DownloadTask(task_id="case-insensitive-test", 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 == str(local_file)
|
|
assert task.original_download_path == str(local_file)
|