Files
shelfmark/tests/metadata/test_googlebooks_parse.py
T
splitsec2 7c8e89c567 fix(googlebooks): page by the capped size, not the raw limit (#1370)
The Google Books search builds `maxResults` as `min(limit, 40)` because
the API caps a page at 40 volumes, but advances `startIndex` by the full
`limit`. The pages then stop tiling. With `limit=50`, page 1 covers
items 0 to 39 and page 2 starts at 50, so items 40 to 49 are never
returned and every later page drops another 10.

This computes the page size once and uses it for both `maxResults` and
`startIndex`. The shipped frontend asks for 40 and is unaffected.
`/api/metadata/search` clamps `limit` to 100, so an API caller passing
41 to 100 was hitting it.

One thing I left alone. The provider uses the base `search_paginated`
heuristic, `has_more = len(books) >= options.limit`, which still reports
`has_more: false` for a limit above 40 since Google can never return
that many. That was already the behaviour before this change, and fixing
it means either touching the shared heuristic or adding a provider
override, so I kept this patch to the stride. Happy to follow up if you
want it.

## Verification

- `tests/metadata/test_googlebooks_parse.py`: pages 1 and 2 at
`limit=50` must tile exactly, plus a guard that `limit=25` still strides
by 25. The first fails on current main and passes here.
- Full suite (3165), ruff, ruff format, basedpyright, vulture green.
2026-09-20 23:05:48 -04:00

146 lines
4.6 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_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