Files
shelfmark/tests/core/test_release_search.py
T
splitsec2andInfiniteAvenger 44f4e13cce refactor: extract the per-source release search out of /api/releases (#1355)
The `/api/releases` route carries an inner `_search_source_releases`
helper that builds the search plan for one source, logs the planned
query type, runs the search and turns
`SourceUnavailableError`/operational errors into an error message
instead of raising. Anything outside the route that wants to search one
source with exactly those semantics has to go through Flask today.

This moves that helper into `shelfmark/core/release_search.py` as
`search_source_releases()` and has the route delegate to it. Behaviour
is unchanged: same plan construction (including the caller's `user_id`,
so per-user default languages still apply), same logging, same
error-to-message handling.

It is the refactor half of #1047 by @InfiniteAvenger, split out on its
own as you asked for other PRs (#1318). Their authorship is preserved on
the commit; I rebased it onto current `main` and added tests.

## Verification

- `tests/core/test_release_search.py`: unknown source → `"Unknown
source: …"`, `SourceUnavailableError` and operational errors →
`"<source>: <error>"`, success path forwards `expand_search` /
`content_type` and returns the source instance, the plan receives
languages / manual query / indexers / `user_id`. (These tests are
type-annotated; happy to strip the annotations if you prefer the suite's
bare style.)
- Full suite, ruff, ruff format, basedpyright, vulture green; the
existing `/api/releases` route tests are unchanged and pass.

Co-authored-by: InfiniteAvenger <calebewest02@gmail.com>
2026-09-19 23:16:53 -04:00

120 lines
3.7 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")],
ids=["unavailable", "oserror", "runtime"],
)
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_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,
}