"""Tests for the scraper-based eBay adapter. Uses a minimal HTML fixture that mirrors eBay's search results structure. No HTTP requests are made — all tests operate on the pure parsing functions. """ import pytest from datetime import timedelta from app.platforms.ebay.scraper import ( scrape_listings, scrape_sellers, _parse_price, _parse_seller, _parse_time_left, ) # --------------------------------------------------------------------------- # Minimal eBay search results HTML fixture # --------------------------------------------------------------------------- _EBAY_HTML = """
""" # --------------------------------------------------------------------------- # Unit tests: pure parsing functions # --------------------------------------------------------------------------- class TestParsePrice: def test_simple_price(self): assert _parse_price("$950.00") == 950.0 def test_price_range_takes_lower_bound(self): assert _parse_price("$900.00 to $1,050.00") == 900.0 def test_price_with_commas(self): assert _parse_price("$1,100.00") == 1100.0 def test_empty_returns_zero(self): assert _parse_price("") == 0.0 class TestParseSeller: def test_standard_format(self): username, count, ratio = _parse_seller("techguy (1,234) 99.1% positive feedback") assert username == "techguy" assert count == 1234 assert ratio == pytest.approx(0.991, abs=0.001) def test_low_count(self): username, count, ratio = _parse_seller("new_user_2024 (2) 100.0% positive feedback") assert username == "new_user_2024" assert count == 2 assert ratio == pytest.approx(1.0, abs=0.001) def test_fallback_on_malformed(self): username, count, ratio = _parse_seller("weirdformat") assert username == "weirdformat" assert count == 0 assert ratio == 0.0 # --------------------------------------------------------------------------- # Integration tests: HTML fixture → domain objects # --------------------------------------------------------------------------- class TestScrapeListings: def test_skips_shop_on_ebay_ghost(self): listings = scrape_listings(_EBAY_HTML) titles = [l.title for l in listings] assert all("Shop on eBay" not in t for t in titles) def test_parses_three_real_listings(self): listings = scrape_listings(_EBAY_HTML) assert len(listings) == 3 def test_extracts_platform_listing_id_from_url(self): listings = scrape_listings(_EBAY_HTML) assert listings[0].platform_listing_id == "123456789" assert listings[1].platform_listing_id == "987654321" def test_price_range_takes_lower(self): listings = scrape_listings(_EBAY_HTML) assert listings[1].price == 1100.0 def test_condition_lowercased(self): listings = scrape_listings(_EBAY_HTML) assert listings[0].condition == "used" assert listings[1].condition == "new" def test_photo_prefers_data_src(self): listings = scrape_listings(_EBAY_HTML) # Listing 2 has data-src set, src empty assert listings[1].photo_urls == ["https://i.ebayimg.com/thumbs/2.jpg"] def test_seller_platform_id_set(self): listings = scrape_listings(_EBAY_HTML) assert listings[0].seller_platform_id == "techguy" assert listings[2].seller_platform_id == "new_user_2024" class TestScrapeSellers: def test_extracts_three_sellers(self): sellers = scrape_sellers(_EBAY_HTML) assert len(sellers) == 3 def test_feedback_count_and_ratio(self): sellers = scrape_sellers(_EBAY_HTML) assert sellers["techguy"].feedback_count == 1234 assert sellers["techguy"].feedback_ratio == pytest.approx(0.991, abs=0.001) def test_account_age_is_zero(self): """account_age_days is always 0 from scraper — signals partial score.""" sellers = scrape_sellers(_EBAY_HTML) assert all(s.account_age_days == 0 for s in sellers.values()) def test_category_history_is_empty(self): """category_history_json is always '{}' from scraper — signals partial score.""" sellers = scrape_sellers(_EBAY_HTML) assert all(s.category_history_json == "{}" for s in sellers.values()) # --------------------------------------------------------------------------- # _parse_time_left # --------------------------------------------------------------------------- class TestParseTimeLeft: def test_days_hours(self): td = _parse_time_left("3d 14h left") assert td == timedelta(days=3, hours=14) def test_hours_minutes(self): td = _parse_time_left("14h 23m left") assert td == timedelta(hours=14, minutes=23) def test_minutes_seconds(self): td = _parse_time_left("23m 45s left") assert td == timedelta(minutes=23, seconds=45) def test_days_only(self): td = _parse_time_left("2d left") assert td == timedelta(days=2) def test_no_match_returns_none(self): assert _parse_time_left("Buy It Now") is None def test_empty_string_returns_none(self): assert _parse_time_left("") is None def test_all_zeros_returns_none(self): # Regex can match "0d 0h 0m 0s left" — should treat as no time left = None assert _parse_time_left("0d 0h 0m 0s left") is None def test_auction_listing_sets_ends_at(self): """scrape_listings should set ends_at for an auction item.""" auction_html = """ """ listings = scrape_listings(auction_html) assert len(listings) == 1 assert listings[0].buying_format == "auction" assert listings[0].ends_at is not None def test_fixed_price_listing_no_ends_at(self): """scrape_listings should leave ends_at=None for fixed-price items.""" listings = scrape_listings(_EBAY_HTML) fixed = [l for l in listings if l.buying_format == "fixed_price"] assert len(fixed) > 0 assert all(l.ends_at is None for l in fixed)