Arag / tests /unit /test_platform_presence.py
AuthorBot
Improve per-platform price fetch for all verified listings.
b32dd30
Raw
History Blame Contribute Delete
17.5 kB
"""Unit tests for Platform Presence Scanner."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from app.services.platform_presence import (
SCORED_PLATFORMS,
CatalogContext,
PlatformListing,
build_recommendations,
compute_score,
confidence_to_status,
parse_goodreads_book_links,
platform_presence_from_dict,
platform_presence_to_dict,
scan_platform_presence,
search_title,
title_match_score,
)
from app.repositories.platform_listing_repo import PlatformListingRepository
class TestTitleMatchScore:
def test_exact_title_match(self):
score = title_match_score("Regime Change", "Regime Change", "Patrick Deneen", "Patrick Deneen")
assert score >= 0.85
def test_series_suffix_stripped_for_match(self):
score = title_match_score(
"Her Deadly Homecoming (Carolina McKay, #1)",
"Her Deadly Homecoming",
)
assert score >= 0.85
def test_partial_title_no_match(self):
score = title_match_score("Unique Obscure Title XYZ", "Completely Different Book")
assert score < 0.5
def test_empty_title_returns_zero(self):
assert title_match_score("", "Some Title") == 0.0
class TestSearchTitle:
def test_strips_series_parenthetical(self):
assert search_title("Her Deadly Homecoming (Carolina McKay, #1)") == "Her Deadly Homecoming"
def test_strips_bracket_series(self):
assert search_title("Dark Matter [The Trilogy #2]") == "Dark Matter"
def test_strips_a_novel_suffix(self):
assert search_title("Winter Counts (A Novel)") == "Winter Counts"
def test_plain_title_unchanged(self):
assert search_title("Regime Change") == "Regime Change"
def test_strips_amazon_marketing_subtitle(self):
long_title = (
"The Girl Behind the Gates: The gripping, heartbreaking historical "
"bestseller based on a true story"
)
assert search_title(long_title) == "The Girl Behind the Gates"
def test_search_query_uses_core_title_without_quotes(self):
from app.services.platform_presence import _search_query
q = _search_query("Her Deadly Homecoming (Carolina McKay Crime Thriller Book 1)", "Jane Doe", None)
assert q == "Her Deadly Homecoming Jane Doe"
assert '"' not in q
class TestGoodreadsBookLinks:
SAMPLE = (
'{"__typename":"BookLink","name":"Barnes \\u0026 Noble",'
'"url":"https://www.barnesandnoble.com/s/Her+Deadly+Homecoming","ref":null},'
'{"__typename":"BookLink","name":"Kobo","url":"https://www.kobo.com/us/en/search?query=x","ref":null},'
'{"__typename":"BookLink","name":"Thriftbooks","url":"https://www.thriftbooks.com/w/x","ref":null}'
)
def test_parses_retailer_links(self):
links = parse_goodreads_book_links(self.SAMPLE)
assert "barnes_noble" in links
assert "kobo" in links
assert "thriftbooks" in links
assert "barnesandnoble.com" in links["barnes_noble"]
def test_empty_html_returns_empty(self):
assert parse_goodreads_book_links("") == {}
class TestNormalizeRetailerUrl:
def test_unwraps_synergy_redirect(self):
from app.services.isbn_catalog import normalize_retailer_url
raw = (
"https://click.linksynergy.com/fs-bin/click?id=x"
"&RD_PARM1=https%3A%2F%2Fwww.kobo.com%2Fus%2Fen%2Fsearch%3FQuery%3Dtest"
)
assert "kobo.com" in normalize_retailer_url(raw)
def test_unwraps_partnerize_destination(self):
from app.services.isbn_catalog import normalize_retailer_url
raw = (
"https://prf.hn/click/camref:1101ljNE7/pubref:x/"
"destination:https://www.thriftbooks.com/browse/?b.search=Test"
)
assert normalize_retailer_url(raw) == "https://www.thriftbooks.com/browse/?b.search=Test"
def test_bookshop_from_isbn(self):
from app.services.isbn_catalog import bookshop_url_from_isbn
url = bookshop_url_from_isbn("9780306406157", "Test")
assert url == "https://bookshop.org/search?keywords=9780306406157"
class TestRetailerUrlClassification:
def test_amazon_product_url(self):
from app.services.isbn_catalog import classify_retailer_url, sanitize_platform_url
url = "http://www.amazon.com/gp/product/B0DHWS3P6T/ref=affiliate"
clean = sanitize_platform_url(url, "amazon")
assert clean == "https://www.amazon.com/dp/B0DHWS3P6T"
assert classify_retailer_url(clean, "amazon") == "product"
def test_search_url_classified(self):
from app.services.isbn_catalog import classify_retailer_url
url = "https://www.barnesandnoble.com/s/Lethal+Lines"
assert classify_retailer_url(url, "barnes_noble") == "search"
def test_goodreads_product_url(self):
from app.services.isbn_catalog import classify_retailer_url
url = "https://www.goodreads.com/book/show/219584318-lethal-lines"
assert classify_retailer_url(url, "goodreads") == "product"
def test_broken_url_invalid(self):
from app.services.isbn_catalog import sanitize_platform_url
assert sanitize_platform_url("https:", "thriftbooks") is None
class TestScoring:
def _listing(self, pid: str, status: str, weight: float) -> PlatformListing:
return PlatformListing(
platform_id=pid,
display_name=pid,
status=status,
confidence=0.9 if status == "verified" else 0.75,
listing_url="https://example.com",
benefit="test",
weight=weight,
)
def test_compute_score_counts_verified_platforms(self):
platforms = [
self._listing("amazon", "verified", 2.0),
self._listing("kobo", "likely", 1.0),
self._listing("goodreads", "verified", 1.0),
self._listing("walmart", "not_found", 0.8),
]
assert compute_score(platforms) == 2.0
def test_confidence_to_status_binary(self):
assert confidence_to_status(0.90) == "verified"
assert confidence_to_status(0.75) == "not_found"
assert confidence_to_status(0.50) == "not_found"
assert confidence_to_status(0.80, isbn_match=True) == "verified"
def test_recommendations_sorted_by_weight(self):
platforms = [
self._listing("amazon", "not_found", 2.0),
self._listing("abebooks", "not_found", 0.5),
self._listing("apple_books", "verified", 1.2),
]
recs = build_recommendations(platforms)
assert recs[0]["platform_id"] == "amazon"
assert recs[1]["platform_id"] == "abebooks"
class TestSerialization:
def test_round_trip_dict(self):
listing = PlatformListing(
platform_id="amazon",
display_name="Amazon",
status="verified",
confidence=1.0,
listing_url="https://amazon.com/dp/123",
benefit="Largest channel",
weight=2.0,
icon="🛍",
)
from app.services.platform_presence import PlatformPresenceResult
result = PlatformPresenceResult(
score=2.0,
listed_count=1,
scanned_at="2026-01-01T00:00:00+00:00",
source_isbn="9781234567890",
platforms=[listing],
recommendations=[],
)
restored = platform_presence_from_dict(platform_presence_to_dict(result))
assert restored.score == 2.0
assert restored.platforms[0].platform_id == "amazon"
@pytest.mark.asyncio
async def test_scan_source_platform_marked_verified():
"""Source URL platform should be verified without HTTP calls."""
from app.services.catalog_hub import CatalogSnapshot
snap = CatalogSnapshot(title="Test Book", author="Jane Author", isbn13="9781234567890")
with patch(
"app.services.platform_presence._load_goodreads_hub",
new=AsyncMock(return_value=("Jane Author", None, {})),
), patch(
"app.services.catalog_hub.build_catalog_snapshot",
new=AsyncMock(return_value=snap),
), patch(
"app.services.platform_presence._enrich_listing_prices",
new=AsyncMock(),
):
scan = await scan_platform_presence(
title="Test Book",
author="Jane Author",
isbn="9781234567890",
source_platform="amazon",
source_url="https://www.amazon.com/dp/B01234567",
buy_url="https://www.amazon.com/dp/B01234567",
)
amazon = next(p for p in scan.platforms if p.platform_id == "amazon")
assert amazon.status == "verified"
assert amazon.confidence == 1.0
assert amazon.listing_url == "https://www.amazon.com/dp/B01234567"
assert scan.listed_count >= 1
@pytest.mark.asyncio
async def test_scan_uses_goodreads_catalog_links():
"""Goodreads product buy links verify retailers without HTML scraping."""
from app.services.catalog_hub import CatalogSnapshot
gr_links = {
"goodreads": "https://www.goodreads.com/book/show/55046753",
"thriftbooks": "https://www.thriftbooks.com/w/x",
}
snap = CatalogSnapshot(
title="Her Deadly Homecoming",
author="Tony Urban",
goodreads_url=gr_links["goodreads"],
goodreads_confidence=0.95,
retailer_links=gr_links,
)
with patch(
"app.services.platform_presence._load_goodreads_hub",
new=AsyncMock(return_value=("Tony Urban", gr_links["goodreads"], gr_links)),
), patch(
"app.services.catalog_hub.build_catalog_snapshot",
new=AsyncMock(return_value=snap),
):
scan = await scan_platform_presence(
title="Her Deadly Homecoming (Carolina McKay, #1)",
author="",
)
verified = {p.platform_id for p in scan.platforms if p.status == "verified"}
assert {"goodreads", "thriftbooks"}.issubset(verified)
@pytest.mark.asyncio
async def test_build_catalog_uses_slug_fetch_and_bookshop_isbn():
"""Catalog should prefer slug page fetch and add bookshop when ISBN known."""
from app.services.platform_presence import _build_catalog_context
html = (
'{"__typename":"BookLink","name":"Amazon","url":"https://www.amazon.com/dp/B08GFXGTJZ","ref":null}'
)
mock_client = MagicMock()
with patch(
"app.services.platform_presence._goodreads_autocomplete_best",
new=AsyncMock(return_value={
"book_id": "55046753",
"book_path": "/book/show/55046753-her-deadly-homecoming",
"author_name": "Tony Urban",
}),
), patch(
"app.services.platform_presence._fetch_goodreads_book_page",
new=AsyncMock(return_value=html),
) as fetch_mock:
ctx = await _build_catalog_context(
mock_client,
"Her Deadly Homecoming",
"",
isbn="9780306406157",
)
fetch_mock.assert_called_once_with(
mock_client, "55046753", "/book/show/55046753-her-deadly-homecoming",
)
assert ctx.resolved_author == "Tony Urban"
assert "amazon" in ctx.retailer_links
assert "bookshop" in ctx.retailer_links
assert "9780306406157" in ctx.retailer_links["bookshop"]
@pytest.mark.asyncio
async def test_scan_cache_key_busted_for_v10():
from app.services.platform_presence import scan_cache_key
assert scan_cache_key("https://example.com/book").startswith("platform_scan:v10:")
@pytest.mark.asyncio
async def test_scan_empty_title_skips():
scan = await scan_platform_presence(title="", author="")
assert scan.score == 0.0
assert all(p.status == "skipped" for p in scan.platforms)
@pytest.mark.asyncio
async def test_scan_mocks_catalog_hub():
"""Scan uses catalog hub APIs — mock snapshot for deterministic output."""
from app.services.catalog_hub import CatalogSnapshot
snap = CatalogSnapshot(
title="Test Book",
author="Jane Author",
isbn13="9780306406157",
gb_volume_id="vol1",
gb_confidence=0.92,
ol_confidence=0.90,
ol_key="/works/OL1W",
goodreads_url="https://www.goodreads.com/book/show/1",
goodreads_confidence=0.95,
)
with patch(
"app.services.platform_presence._load_goodreads_hub",
new=AsyncMock(return_value=("Jane Author", snap.goodreads_url, {})),
), patch(
"app.services.catalog_hub.build_catalog_snapshot",
new=AsyncMock(return_value=snap),
), patch(
"app.services.platform_presence._enrich_listing_prices",
new=AsyncMock(),
):
scan = await scan_platform_presence(
title="Test Book",
author="Jane Author",
isbn="9780306406157",
)
assert len(SCORED_PLATFORMS) == 10
verified_ids = {p.platform_id for p in scan.platforms if p.status == "verified"}
assert "google_books" in verified_ids
assert "goodreads" in verified_ids
assert "open_library" in verified_ids
def test_replace_from_scan_defaults_visible_in_widget_false():
"""Repo must never expose listings to widget by default."""
repo = PlatformListingRepository.__new__(PlatformListingRepository)
from app.services.platform_presence import PlatformPresenceResult
scan = PlatformPresenceResult(
score=2.0,
listed_count=1,
scanned_at="2026-01-01T00:00:00+00:00",
source_isbn=None,
platforms=[
PlatformListing(
platform_id="amazon",
display_name="Amazon",
status="verified",
confidence=1.0,
listing_url="https://amazon.com/x",
benefit="",
weight=2.0,
),
],
recommendations=[],
)
# Verify create payload would set visible_in_widget=False
for p in scan.platforms:
assert p.listing_url # saved internally
def test_widget_helpers_ignore_platform_listings():
"""get_book_links must not import platform listing repo."""
import inspect
from app.services.pipeline import helpers
source = inspect.getsource(helpers.get_book_links)
assert "platform_listing" not in source.lower()
assert "BookPlatformListing" not in source
def test_infer_distribution_note_kindle_only():
from app.services.platform_presence import infer_distribution_note
platforms = [
PlatformListing(
platform_id="amazon", display_name="Amazon", status="verified",
confidence=0.98, listing_url="https://amazon.com/dp/B0X", benefit="", weight=2.0,
),
PlatformListing(
platform_id="goodreads", display_name="Goodreads", status="verified",
confidence=0.95, listing_url="https://goodreads.com/book/show/1", benefit="", weight=1.0,
),
PlatformListing(
platform_id="kobo", display_name="Kobo", status="not_found",
confidence=0.0, listing_url=None, benefit="", weight=1.0,
),
]
note = infer_distribution_note(platforms)
assert note is not None
assert "Kindle" in note
def test_infer_distribution_note_suppressed_with_isbn():
from app.services.platform_presence import infer_distribution_note
platforms = [
PlatformListing(
platform_id="amazon", display_name="Amazon", status="verified",
confidence=0.98, listing_url="https://amazon.com/dp/B0X", benefit="", weight=2.0,
),
]
assert infer_distribution_note(platforms, source_isbn="9781529374544") is None
def test_parse_goodreads_book_ref():
from app.services.platform_presence import parse_goodreads_book_ref
ref = parse_goodreads_book_ref(
"https://www.goodreads.com/book/show/219584318-lethal-lines?q=1"
)
assert ref == ("219584318", "/book/show/219584318-lethal-lines")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_lethal_lines_kindle_only_scan():
"""Lethal Lines is Amazon Kindle + Goodreads only — not wide retail."""
scan = await scan_platform_presence(
title="Lethal Lines",
author="Paige Black",
source_url="https://www.goodreads.com/book/show/219584318-lethal-lines",
source_platform="goodreads",
)
verified = [p for p in scan.platforms if p.status == "verified"]
verified_ids = {p.platform_id for p in verified}
assert verified_ids == {"amazon", "goodreads"}
assert scan.listed_count == 2
assert scan.distribution_note
amazon = next(p for p in verified if p.platform_id == "amazon")
assert "B0DHWS3P6T" in (amazon.listing_url or "")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_girl_behind_gates_wide_distribution_scan():
"""Trade-published title should resolve Goodreads + multi-retailer coverage."""
scan = await scan_platform_presence(
title=(
"The Girl Behind the Gates: The gripping, heartbreaking historical "
"bestseller based on a true story"
),
author="Brenda Davies",
isbn="9781529374551",
source_platform="amazon",
source_url="https://www.amazon.com/dp/B0847KDV8W",
buy_url="https://www.amazon.com/dp/B0847KDV8W",
)
verified = {p.platform_id for p in scan.platforms if p.status == "verified"}
assert "amazon" in verified
assert "goodreads" in verified
assert scan.listed_count >= 5
assert scan.distribution_note is None