Files
shelfmark/tests/metadata/test_googlebooks_parse.py
T
CaliBrain c269230ef3 fix: review regressions in metadata, notifications, activity redaction, search options and genDebug (#1510)
- **Open Library (#1451): keep the work unless the search matched a
translation.** Open Library shows an English edition whenever one
matches the query. So "Mistborn" returned the 2023 "Mistborn" reissue
(2023) instead of "The Final Empire" (2001), and "Children of Dune"
returned "Children of Dune (Dune Chronicles, Book 3)" (1987). The
matched edition's title, ISBNs, cover, language and year now win only
when the edition is in another language and the query doesn't name the
work's own title. Checked against live Open Library: "Children of Dune"
and "Mistborn" give the v1.4.0 results again, and "Der Weg der Könige"
and "Kinder des Nebels" still return the German editions.
- **Google Books (#1434): the ISBN cache decorator is back on
`search_by_isbn`.** It had moved to the new `search_paginated`. Failed
searches were cached as empty pages, and ISBN lookups lost their cache.
- **Notifications: Test notification shows the plugin's failure reason
again.** Apprise 2 delivers on its own worker threads, even for a single
service when that service has a deadline. `_AppriseLogCapture` filtered
records by thread id and dropped all of them, so the UI only said "json:
delivery failed". Apprise runs those workers in a copy of the caller's
context, so the capture now filters on a context variable. That still
keeps concurrent dispatches apart. `AppriseAsset(async_mode=False)`
would not fix this, because the sequential path also uses the worker
pool when a service has a deadline.
- **Activity: `activity_update` no longer sends the raw task id to
non-admins.** The owner's `user_<id>` room got the raw task id. For a
request-linked download that id can hold a private tracker's passkey. It
now goes through `viewer_download_id`, the same mapping `/api/status`
and `download_progress` use. The admins room still gets the raw id.
- **SearchBar (#1476): Escape and an outside click close the homepage's
inline options panel again,** as they did in v1.4.0.
- **genDebug.sh: packages are listed through `importlib.metadata`,**
since #1404 removed pip from the image. The script also now redacts:
  - `PROWLARR_MAM_ID`
- `user:pass@` credentials in URLs (proxies), in both the environment
and the config files
- notification route URLs, which carry their service's token (Telegram,
Discord and others)

- `make python-checks`, `make python-test`, `make frontend-checks` and
`make frontend-test` pass on top of current `main`.
- New tests cover:
  - Open Library edition selection
  - Google Books caching
- log capture against a real Apprise 2 delivery to a local server that
answers 500, plus a concurrent dispatch on another thread
  - the `activity_update` payload sent to each room
  - genDebug redaction and the package listing

  Each one fails on the old code.
- I checked the SearchBar fix in the running app, because the frontend
has no DOM test setup: Escape closes the panel, an outside click closes
it, and a click inside keeps it open.
- I ran the genDebug redaction under GNU sed (Debian image) and BSD sed.
The package listing ran in the pip-less image.
2026-10-21 01:58:41 -04:00

209 lines
6.5 KiB
Python

import requests
from shelfmark.core.cache import get_metadata_cache
from shelfmark.metadata_providers import MetadataSearchOptions
from shelfmark.metadata_providers.googlebooks import GoogleBooksProvider
class _GoogleBooksResponse:
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
return None
def json(self):
return self._payload
class _FlakyGoogleBooksSession:
def __init__(self):
self.calls = 0
def get(self, *args, **kwargs):
self.calls += 1
if self.calls == 1:
raise requests.Timeout
return _GoogleBooksResponse(
{
"items": [
{
"id": "volume-1",
"volumeInfo": {
"title": "Recovered Book",
"authors": ["Alice Author"],
},
}
]
}
)
class _RecordingGoogleBooksSession:
def __init__(self):
self.params = []
def get(self, *args, **kwargs):
self.params.append(dict(kwargs["params"]))
return _GoogleBooksResponse({"items": []})
def test_googlebooks_search_does_not_cache_request_failures():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
session = _FlakyGoogleBooksSession()
provider.session = session
options = MetadataSearchOptions(query="Recovered Book")
assert provider.search(options) == []
result = provider.search(options)
assert session.calls == 2
assert [book.title for book in result] == ["Recovered Book"]
def test_googlebooks_paginated_search_does_not_cache_request_failures():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
session = _FlakyGoogleBooksSession()
provider.session = session
options = MetadataSearchOptions(query="Recovered Book")
assert provider.search_paginated(options).books == []
result = provider.search_paginated(options)
assert session.calls == 2
assert [book.title for book in result.books] == ["Recovered Book"]
def test_googlebooks_isbn_lookup_is_cached():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
session = _FlakyGoogleBooksSession()
session.calls = 1 # skip the simulated timeout
provider.session = session
first = provider.search_by_isbn("978-0-00-000000-2")
second = provider.search_by_isbn("978-0-00-000000-2")
assert first is not None
assert second == first
assert session.calls == 2
def test_googlebooks_pages_tile_when_limit_exceeds_api_maximum():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
session = _RecordingGoogleBooksSession()
provider.session = session
for page in (1, 2):
provider.search(MetadataSearchOptions(query="Dune", limit=50, page=page))
first, second = session.params
assert first["maxResults"] == 40
assert first["startIndex"] == 0
assert second["startIndex"] == first["startIndex"] + first["maxResults"]
def test_googlebooks_page_stride_follows_limit_below_api_maximum():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
session = _RecordingGoogleBooksSession()
provider.session = session
provider.search(MetadataSearchOptions(query="Dune", limit=25, page=3))
assert session.params[0]["maxResults"] == 25
assert session.params[0]["startIndex"] == 50
class TestGoogleBooksParseVolume:
def test_parse_volume_returns_metadata_for_valid_payload(self):
provider = GoogleBooksProvider(api_key="test-key")
result = provider._parse_volume(
{
"id": "volume-1",
"volumeInfo": {
"title": "Test Book",
"authors": ["Alice Author"],
"industryIdentifiers": [
{"type": "ISBN_10", "identifier": "1234567890"},
{"type": "ISBN_13", "identifier": "9781234567897"},
],
"imageLinks": {
"thumbnail": "http://example.com/cover.jpg&edge=curl",
},
"publisher": "Test Publisher",
"publishedDate": "2024-03-01",
"language": "en",
"categories": ["Fiction", "Fantasy"],
"description": "A book.",
"infoLink": "https://example.com/books/volume-1",
"averageRating": 4.2,
"ratingsCount": 1200,
},
}
)
assert result is not None
assert result.provider_id == "volume-1"
assert result.title == "Test Book"
assert result.authors == ["Alice Author"]
assert result.isbn_10 == "1234567890"
assert result.isbn_13 == "9781234567897"
assert result.cover_url == "https://example.com/cover.jpg"
assert result.publish_year == 2024
assert result.display_fields[0].value == "4.2 (1,200)"
def test_parse_volume_returns_none_for_malformed_rating_payload(self):
provider = GoogleBooksProvider(api_key="test-key")
result = provider._parse_volume(
{
"id": "volume-2",
"volumeInfo": {
"title": "Broken Book",
"averageRating": "not-a-number",
},
}
)
assert result is None
class _FullPageSession:
def __init__(self, count):
self.count = count
def get(self, *args, **kwargs):
items = [
{"id": f"v{i}", "volumeInfo": {"title": f"Book {i}", "authors": ["Alice Author"]}}
for i in range(self.count)
]
return _GoogleBooksResponse({"items": items})
def test_googlebooks_reports_more_pages_when_limit_exceeds_api_maximum():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
provider.session = _FullPageSession(40)
result = provider.search_paginated(MetadataSearchOptions(query="Dune", limit=100))
assert len(result.books) == 40
assert result.has_more is True
def test_googlebooks_reports_no_more_pages_after_a_short_page():
get_metadata_cache().clear()
provider = GoogleBooksProvider(api_key="test-key")
provider.session = _FullPageSession(12)
result = provider.search_paginated(MetadataSearchOptions(query="Dune", limit=100))
assert result.has_more is False