mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-14 13:38:38 +00:00
Same idea as #1558, for three more places that hand `str(e)` to whoever is logged in. These are code scanning alerts 106, 142 and 143. `/api/status` and `/api/config` returned the raw exception text of an OSError, RuntimeError and so on with the 500. They now return "Could not read the download status" and "Could not load the configuration". The full error is still logged with its trace. For `/api/releases`, a source failing with an unexpected error put `source: <exception text>` into the error list that goes back to the caller. `search_source_releases` takes a new `include_error_detail` flag. The route passes True for admins (and when auth is off, where everyone is treated as an admin) and False for everyone else, who get `source: search failed`. The message of a `SourceUnavailableError` is written for the user, so it is kept either way, and the background auto-download caller keeps the default and sees the full text as before. Left alone on purpose: the `reason` in the release inspect response, since a test covers that it carries the error text and it looks intentional. Tests: two for the routes and two for the helper. All four fail without the change. Full suite passes (5295), ruff and basedpyright are clean.
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
"""Tests for the per-source release search helper extracted from ``/api/releases``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core import release_search
|
|
from shelfmark.metadata_providers import BookMetadata
|
|
from shelfmark.release_sources import SourceUnavailableError
|
|
|
|
|
|
def _book(**overrides: Any) -> BookMetadata:
|
|
fields: dict[str, Any] = {
|
|
"provider": "manual",
|
|
"provider_id": "1",
|
|
"title": "Dungeon Crawler Carl",
|
|
"authors": ["Matt Dinniman"],
|
|
"isbn_13": "9780593820247",
|
|
}
|
|
fields.update(overrides)
|
|
return BookMetadata(**fields)
|
|
|
|
|
|
class _Source:
|
|
def __init__(self, result: list[str] | None = None, error: Exception | None = None) -> None:
|
|
self.result = result if result is not None else []
|
|
self.error = error
|
|
self.calls: list[tuple[Any, Any, bool, str]] = []
|
|
|
|
def search(
|
|
self, book: BookMetadata, plan: Any, *, expand_search: bool, content_type: str
|
|
) -> list[str]:
|
|
self.calls.append((book, plan, expand_search, content_type))
|
|
if self.error is not None:
|
|
raise self.error
|
|
return list(self.result)
|
|
|
|
|
|
def test_unknown_source_returns_error_tuple(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def _raise(name: str) -> Any:
|
|
msg = f"Unknown source: {name}"
|
|
raise ValueError(msg)
|
|
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", _raise)
|
|
|
|
source, releases, error = release_search.search_source_releases("nope", _book())
|
|
|
|
assert source is None
|
|
assert releases == []
|
|
assert error == "Unknown source: nope"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"exc",
|
|
[SourceUnavailableError("down"), OSError("boom"), RuntimeError("bad"), ValueError("odd")],
|
|
ids=["unavailable", "oserror", "runtime", "valueerror"],
|
|
)
|
|
def test_source_failures_become_messages(monkeypatch: pytest.MonkeyPatch, exc: Exception) -> None:
|
|
src = _Source(error=exc)
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", lambda _name: src)
|
|
|
|
source, releases, error = release_search.search_source_releases("s", _book())
|
|
|
|
assert source is None
|
|
assert releases == []
|
|
assert error == f"s: {exc!s}"
|
|
|
|
|
|
def test_error_detail_can_be_withheld(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
src = _Source(error=OSError("connect to http://10.0.0.5:9117 refused"))
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", lambda _name: src)
|
|
|
|
_source, _releases, error = release_search.search_source_releases(
|
|
"s", _book(), include_error_detail=False
|
|
)
|
|
|
|
assert error == "s: search failed"
|
|
|
|
|
|
def test_unavailable_message_is_kept_when_detail_is_withheld(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
src = _Source(error=SourceUnavailableError("Anna's Archive is not reachable"))
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", lambda _name: src)
|
|
|
|
_source, _releases, error = release_search.search_source_releases(
|
|
"s", _book(), include_error_detail=False
|
|
)
|
|
|
|
assert error == "s: Anna's Archive is not reachable"
|
|
|
|
|
|
def test_success_returns_source_and_forwards_search_kwargs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
src = _Source(result=["r1", "r2"])
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", lambda _name: src)
|
|
|
|
source, releases, error = release_search.search_source_releases(
|
|
"s", _book(), expand_search=True, content_type="audiobook"
|
|
)
|
|
|
|
assert source is src
|
|
assert releases == ["r1", "r2"]
|
|
assert error is None
|
|
book, plan, expand_search, content_type = src.calls[0]
|
|
assert book.title == "Dungeon Crawler Carl"
|
|
assert "dungeon crawler carl" in plan.primary_query.lower()
|
|
assert plan.isbn_candidates == ["9780593820247"]
|
|
assert expand_search is True
|
|
assert content_type == "audiobook"
|
|
|
|
|
|
def test_plan_receives_filters_and_user_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, Any] = {}
|
|
real_build = release_search.build_release_search_plan
|
|
|
|
def _build(book: BookMetadata, **kwargs: Any) -> Any:
|
|
captured.update(kwargs)
|
|
return real_build(book, **kwargs)
|
|
|
|
monkeypatch.setattr(release_search, "build_release_search_plan", _build)
|
|
src = _Source()
|
|
monkeypatch.setattr("shelfmark.release_sources.get_source", lambda _name: src)
|
|
|
|
release_search.search_source_releases(
|
|
"s",
|
|
_book(),
|
|
languages=["en"],
|
|
manual_query="dcc",
|
|
indexers=["idx"],
|
|
user_id=42,
|
|
)
|
|
|
|
assert captured == {
|
|
"languages": ["en"],
|
|
"manual_query": "dcc",
|
|
"indexers": ["idx"],
|
|
"source_filters": None,
|
|
"user_id": 42,
|
|
}
|