"""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