mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 20:51:10 +01:00
288 lines
9.1 KiB
Python
288 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from threading import Event
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core.models import DownloadTask, QueueStatus
|
|
from shelfmark.core.queue import BookQueue
|
|
|
|
|
|
class _AvailableSource:
|
|
display_name = "Test Source"
|
|
|
|
def is_available(self):
|
|
return True
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def source_available_by_default(monkeypatch):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
monkeypatch.setattr(orchestrator, "get_source", lambda _source: _AvailableSource())
|
|
|
|
|
|
def test_retry_download_requeues_error_task(monkeypatch):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-1",
|
|
source="direct_download",
|
|
title="Retryable",
|
|
last_error_message="Timeout",
|
|
last_error_type="TimeoutError",
|
|
)
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
mock_queue.get_task_status.return_value = QueueStatus.ERROR
|
|
mock_queue.enqueue_existing.return_value = True
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
monkeypatch.setattr(orchestrator, "ws_manager", None)
|
|
|
|
ok, error = orchestrator.retry_download("task-1")
|
|
|
|
assert ok is True
|
|
assert error is None
|
|
assert task.last_error_message is None
|
|
assert task.last_error_type is None
|
|
assert task.priority == -10
|
|
mock_queue.enqueue_existing.assert_called_once_with("task-1", priority=-10)
|
|
mock_queue.update_status_message.assert_called_once_with("task-1", "Retrying now")
|
|
|
|
|
|
def test_retry_download_rejects_request_linked_tasks(monkeypatch):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-2",
|
|
source="prowlarr",
|
|
title="Request linked",
|
|
request_id=123,
|
|
)
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
mock_queue.get_task_status.return_value = QueueStatus.ERROR
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
monkeypatch.setattr(orchestrator, "ws_manager", None)
|
|
|
|
ok, error = orchestrator.retry_download("task-2")
|
|
|
|
assert ok is False
|
|
assert error == "Request-linked downloads must be retried from requests"
|
|
mock_queue.enqueue_existing.assert_not_called()
|
|
|
|
|
|
def test_can_retry_download_task_allows_request_postprocess_retry_when_staged_file_exists(tmp_path):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
staged_file = tmp_path / "requested-staged.epub"
|
|
staged_file.write_text("staged")
|
|
task = DownloadTask(
|
|
task_id="task-request-staged-1",
|
|
source="prowlarr",
|
|
title="Requested Retryable",
|
|
request_id=123,
|
|
staged_path=str(staged_file),
|
|
)
|
|
|
|
assert orchestrator.can_retry_download_task(task, QueueStatus.ERROR) is True
|
|
|
|
|
|
def test_can_retry_download_task_blocks_request_error_retry_without_staged_file():
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-request-staged-2",
|
|
source="prowlarr",
|
|
title="Requested Not Retryable",
|
|
request_id=123,
|
|
staged_path="/tmp/does-not-exist.epub",
|
|
)
|
|
|
|
assert orchestrator.can_retry_download_task(task, QueueStatus.ERROR) is False
|
|
|
|
|
|
def test_finalize_download_failure_sets_terminal_error(monkeypatch):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-3",
|
|
source="direct_download",
|
|
title="Terminal failure",
|
|
last_error_message="Download timed out",
|
|
last_error_type="TimeoutError",
|
|
)
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
|
|
orchestrator._finalize_download_failure("task-3")
|
|
|
|
mock_queue.update_status_message.assert_called_once_with("task-3", "Download timed out")
|
|
mock_queue.update_status.assert_called_once_with("task-3", QueueStatus.ERROR)
|
|
|
|
|
|
def test_finalize_download_failure_uses_fallback_message(monkeypatch):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-4",
|
|
source="direct_download",
|
|
title="No message",
|
|
last_error_type="TimeoutError",
|
|
)
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
|
|
orchestrator._finalize_download_failure("task-4")
|
|
|
|
mock_queue.update_status_message.assert_called_once_with(
|
|
"task-4", "Download failed: TimeoutError"
|
|
)
|
|
mock_queue.update_status.assert_called_once_with("task-4", QueueStatus.ERROR)
|
|
|
|
|
|
def test_callback_error_results_in_terminal_error(monkeypatch):
|
|
"""When a handler signals error via status_callback, it should result in terminal ERROR."""
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
task = DownloadTask(
|
|
task_id="task-callback-1",
|
|
source="direct_download",
|
|
title="Callback Error",
|
|
)
|
|
|
|
queue = BookQueue()
|
|
queue.add(task)
|
|
terminal_calls: list[tuple[str, QueueStatus]] = []
|
|
queue.set_terminal_status_hook(
|
|
lambda task_id, status, _task: terminal_calls.append((task_id, status))
|
|
)
|
|
|
|
handler = MagicMock()
|
|
|
|
def _download(_task, _cancel_flag, _progress_callback, status_callback):
|
|
status_callback("error", "Download timed out")
|
|
return None
|
|
|
|
handler.download.side_effect = _download
|
|
handler.post_process_cleanup = MagicMock()
|
|
|
|
monkeypatch.setattr(orchestrator, "book_queue", queue)
|
|
monkeypatch.setattr(orchestrator, "get_handler", lambda _source: handler)
|
|
monkeypatch.setattr(orchestrator, "ws_manager", None)
|
|
|
|
orchestrator._process_single_download(task.task_id, Event())
|
|
|
|
assert task.last_error_message == "Download timed out"
|
|
assert task.last_error_type == "StatusCallbackError"
|
|
assert queue.get_task_status(task.task_id) == QueueStatus.ERROR
|
|
assert len(terminal_calls) == 1
|
|
assert terminal_calls[0] == (task.task_id, QueueStatus.ERROR)
|
|
|
|
|
|
def test_output_stage_retry_skips_redownload_when_staged_file_exists(monkeypatch, tmp_path):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
staged_file = tmp_path / "staged.epub"
|
|
staged_file.write_text("staged")
|
|
|
|
task = DownloadTask(
|
|
task_id="task-staged-1",
|
|
source="direct_download",
|
|
title="Reuse Staged File",
|
|
staged_path=str(staged_file),
|
|
)
|
|
|
|
handler = MagicMock()
|
|
handler.download = MagicMock(return_value=str(tmp_path / "should-not-download.epub"))
|
|
handler.post_process_cleanup = MagicMock()
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
monkeypatch.setattr(orchestrator, "get_handler", lambda _source: handler)
|
|
|
|
seen_temp_files: list[Path] = []
|
|
|
|
def _post_process(temp_file, *_args, **_kwargs):
|
|
seen_temp_files.append(temp_file)
|
|
return "folder://done"
|
|
|
|
monkeypatch.setattr(orchestrator, "post_process_download", _post_process)
|
|
|
|
result = orchestrator._download_task(task.task_id, Event())
|
|
|
|
assert result == "folder://done"
|
|
handler.download.assert_not_called()
|
|
assert seen_temp_files == [staged_file]
|
|
assert task.staged_path is None
|
|
|
|
|
|
def test_output_stage_retry_falls_back_to_download_when_staged_file_missing(monkeypatch, tmp_path):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
missing_staged_file = tmp_path / "missing.epub"
|
|
downloaded_file = tmp_path / "downloaded.epub"
|
|
downloaded_file.write_text("downloaded")
|
|
|
|
task = DownloadTask(
|
|
task_id="task-staged-2",
|
|
source="direct_download",
|
|
title="Fallback Download",
|
|
staged_path=str(missing_staged_file),
|
|
)
|
|
|
|
handler = MagicMock()
|
|
handler.download = MagicMock(return_value=str(downloaded_file))
|
|
handler.post_process_cleanup = MagicMock()
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
monkeypatch.setattr(orchestrator, "get_handler", lambda _source: handler)
|
|
|
|
seen_temp_files: list[Path] = []
|
|
|
|
def _post_process(temp_file, *_args, **_kwargs):
|
|
seen_temp_files.append(temp_file)
|
|
return "folder://done"
|
|
|
|
monkeypatch.setattr(orchestrator, "post_process_download", _post_process)
|
|
|
|
result = orchestrator._download_task(task.task_id, Event())
|
|
|
|
assert result == "folder://done"
|
|
handler.download.assert_called_once()
|
|
assert seen_temp_files == [downloaded_file]
|
|
assert task.staged_path is None
|
|
|
|
|
|
def test_get_book_data_clears_download_path_when_file_read_fails(monkeypatch, tmp_path):
|
|
import shelfmark.download.orchestrator as orchestrator
|
|
|
|
missing_file = tmp_path / "missing.epub"
|
|
task = DownloadTask(
|
|
task_id="task-book-data-1",
|
|
source="direct_download",
|
|
title="Missing File",
|
|
download_path=str(missing_file),
|
|
)
|
|
|
|
mock_queue = MagicMock()
|
|
mock_queue.get_task.return_value = task
|
|
monkeypatch.setattr(orchestrator, "book_queue", mock_queue)
|
|
|
|
file_data, returned_task = orchestrator.get_book_data(task.task_id)
|
|
|
|
assert file_data is None
|
|
assert returned_task is task
|
|
assert task.download_path is None
|