Spaces:
Running
Running
| """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" | |
| 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 | |
| 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) | |
| 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"] | |
| 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:") | |
| 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) | |
| 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") | |
| 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 "") | |
| 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 | |