""" Tests pour pricewatch.app.stores.amazon.store Vérifie match(), canonicalize(), extract_reference() et parse() pour le store Amazon. """ import pytest from pricewatch.app.stores.amazon.store import AmazonStore from pricewatch.app.core.schema import DebugStatus, StockStatus class TestAmazonMatch: """Tests de la méthode match() pour Amazon.""" @pytest.fixture def store(self) -> AmazonStore: """Fixture: AmazonStore instance.""" return AmazonStore() def test_match_amazon_fr(self, store): """amazon.fr doit retourner 0.9.""" score = store.match("https://www.amazon.fr/dp/B08N5WRWNW") assert score == 0.9 def test_match_amazon_com(self, store): """amazon.com doit retourner 0.8.""" score = store.match("https://www.amazon.com/dp/B08N5WRWNW") assert score == 0.8 def test_match_amazon_co_uk(self, store): """amazon.co.uk doit retourner 0.8.""" score = store.match("https://www.amazon.co.uk/dp/B08N5WRWNW") assert score == 0.8 def test_match_amazon_de(self, store): """amazon.de doit retourner 0.7.""" score = store.match("https://www.amazon.de/dp/B08N5WRWNW") assert score == 0.7 def test_match_non_amazon(self, store): """URL non-Amazon doit retourner 0.0.""" score = store.match("https://www.cdiscount.com/product/123") assert score == 0.0 def test_match_empty_url(self, store): """URL vide doit retourner 0.0.""" score = store.match("") assert score == 0.0 def test_match_case_insensitive(self, store): """Match doit être insensible à la casse.""" score = store.match("https://www.AMAZON.FR/dp/B08N5WRWNW") assert score == 0.9 class TestAmazonCanonicalize: """Tests de la méthode canonicalize() pour Amazon.""" @pytest.fixture def store(self) -> AmazonStore: """Fixture: AmazonStore instance.""" return AmazonStore() def test_canonicalize_with_product_name(self, store): """URL avec nom de produit doit être normalisée.""" url = "https://www.amazon.fr/Product-Name-Here/dp/B08N5WRWNW/ref=sr_1_1" canonical = store.canonicalize(url) assert canonical == "https://www.amazon.fr/dp/B08N5WRWNW" def test_canonicalize_already_canonical(self, store): """URL déjà canonique ne change pas.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" canonical = store.canonicalize(url) assert canonical == "https://www.amazon.fr/dp/B08N5WRWNW" def test_canonicalize_with_query_params(self, store): """URL avec query params doit être normalisée.""" url = "https://www.amazon.fr/dp/B08N5WRWNW?ref=abc&tag=xyz" canonical = store.canonicalize(url) assert canonical == "https://www.amazon.fr/dp/B08N5WRWNW" def test_canonicalize_gp_product(self, store): """URL avec /gp/product/ doit être normalisée.""" url = "https://www.amazon.fr/gp/product/B08N5WRWNW" canonical = store.canonicalize(url) assert canonical == "https://www.amazon.fr/dp/B08N5WRWNW" def test_canonicalize_no_asin(self, store): """URL sans ASIN retourne l'URL nettoyée.""" url = "https://www.amazon.fr/some-page?ref=abc" canonical = store.canonicalize(url) assert canonical == "https://www.amazon.fr/some-page" assert "?" not in canonical def test_canonicalize_empty_url(self, store): """URL vide retourne URL vide.""" canonical = store.canonicalize("") assert canonical == "" def test_canonicalize_preserves_domain(self, store): """Le domaine doit être préservé.""" url_fr = "https://www.amazon.fr/dp/B08N5WRWNW/ref=123" url_com = "https://www.amazon.com/dp/B08N5WRWNW/ref=123" assert store.canonicalize(url_fr) == "https://www.amazon.fr/dp/B08N5WRWNW" assert store.canonicalize(url_com) == "https://www.amazon.com/dp/B08N5WRWNW" class TestAmazonExtractReference: """Tests de la méthode extract_reference() pour Amazon.""" @pytest.fixture def store(self) -> AmazonStore: """Fixture: AmazonStore instance.""" return AmazonStore() def test_extract_reference_dp(self, store): """Extraction d'ASIN depuis /dp/.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" asin = store.extract_reference(url) assert asin == "B08N5WRWNW" def test_extract_reference_dp_with_path(self, store): """Extraction d'ASIN depuis /dp/ avec chemin.""" url = "https://www.amazon.fr/Product-Name/dp/B08N5WRWNW/ref=sr_1_1" asin = store.extract_reference(url) assert asin == "B08N5WRWNW" def test_extract_reference_gp_product(self, store): """Extraction d'ASIN depuis /gp/product/.""" url = "https://www.amazon.fr/gp/product/B08N5WRWNW" asin = store.extract_reference(url) assert asin == "B08N5WRWNW" def test_extract_reference_invalid_url(self, store): """URL sans ASIN retourne None.""" url = "https://www.amazon.fr/some-page" asin = store.extract_reference(url) assert asin is None def test_extract_reference_empty_url(self, store): """URL vide retourne None.""" asin = store.extract_reference("") assert asin is None def test_extract_reference_asin_format(self, store): """L'ASIN doit avoir exactement 10 caractères alphanumériques.""" # ASIN valide: 10 caractères url_valid = "https://www.amazon.fr/dp/B08N5WRWNW" assert store.extract_reference(url_valid) == "B08N5WRWNW" # ASIN invalide: trop court url_short = "https://www.amazon.fr/dp/B08N5" assert store.extract_reference(url_short) is None # ASIN invalide: trop long url_long = "https://www.amazon.fr/dp/B08N5WRWNW123" assert store.extract_reference(url_long) is None class TestAmazonParse: """Tests de la méthode parse() pour Amazon.""" @pytest.fixture def store(self) -> AmazonStore: """Fixture: AmazonStore instance.""" return AmazonStore() @pytest.fixture def minimal_html(self) -> str: """Fixture: HTML Amazon minimal avec titre et prix.""" return """ Test Product Test Amazon Product 299,99 €
En stock
""" @pytest.fixture def complete_html(self) -> str: """Fixture: HTML Amazon complet.""" return """ Test Product PlayStation 5 Console 499,99 €
En stock
Marque Sony
Couleur Blanc
""" @pytest.fixture def captcha_html(self) -> str: """Fixture: HTML avec captcha.""" return """

Sorry, we just need to make sure you're not a robot.

""" @pytest.fixture def out_of_stock_html(self) -> str: """Fixture: HTML produit en rupture de stock.""" return """ Out of Stock Product 199,99 €
Actuellement indisponible
""" def test_parse_minimal_html(self, store, minimal_html): """Parse un HTML minimal avec titre et prix.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(minimal_html, url) assert snapshot.source == "amazon" assert snapshot.url == "https://www.amazon.fr/dp/B08N5WRWNW" assert snapshot.title == "Test Amazon Product" assert snapshot.price == 299.99 assert snapshot.currency == "EUR" assert snapshot.stock_status == StockStatus.IN_STOCK assert snapshot.is_complete() is True def test_parse_complete_html(self, store, complete_html): """Parse un HTML complet avec toutes les données.""" url = "https://www.amazon.fr/ps5/dp/B08N5WRWNW" snapshot = store.parse(complete_html, url) assert snapshot.title == "PlayStation 5 Console" assert snapshot.price == 499.99 assert snapshot.reference == "B08N5WRWNW" assert snapshot.stock_status == StockStatus.IN_STOCK assert snapshot.category == "Jeux vidéo" assert len(snapshot.images) >= 2 assert "Marque" in snapshot.specs assert snapshot.specs["Marque"] == "Sony" assert snapshot.is_complete() is True assert snapshot.debug.status == DebugStatus.SUCCESS def test_parse_captcha_html(self, store, captcha_html): """Parse un HTML avec captcha doit signaler l'erreur.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(captcha_html, url) assert snapshot.debug.status == DebugStatus.FAILED assert any("captcha" in err.lower() for err in snapshot.debug.errors) assert snapshot.is_complete() is False def test_parse_out_of_stock(self, store, out_of_stock_html): """Parse un produit en rupture de stock.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(out_of_stock_html, url) assert snapshot.title == "Out of Stock Product" assert snapshot.price == 199.99 assert snapshot.stock_status == StockStatus.OUT_OF_STOCK def test_parse_empty_html(self, store): """Parse un HTML vide doit retourner un snapshot partiel.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse("", url) assert snapshot.source == "amazon" assert snapshot.title is None assert snapshot.price is None assert snapshot.is_complete() is False assert snapshot.debug.status == DebugStatus.PARTIAL def test_parse_canonicalizes_url(self, store, minimal_html): """Parse doit canonicaliser l'URL.""" url = "https://www.amazon.fr/Product-Name/dp/B08N5WRWNW/ref=sr_1_1?tag=xyz" snapshot = store.parse(minimal_html, url) assert snapshot.url == "https://www.amazon.fr/dp/B08N5WRWNW" def test_parse_extracts_reference_from_url(self, store, minimal_html): """Parse doit extraire l'ASIN depuis l'URL.""" url = "https://www.amazon.fr/Product-Name/dp/B08N5WRWNW" snapshot = store.parse(minimal_html, url) assert snapshot.reference == "B08N5WRWNW" def test_parse_sets_fetched_at(self, store, minimal_html): """Parse doit définir fetched_at.""" url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(minimal_html, url) assert snapshot.fetched_at is not None def test_parse_partial_status_without_title(self, store): """Parse sans titre doit avoir status PARTIAL.""" html = """ 299 99 """ url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(html, url) assert snapshot.debug.status == DebugStatus.PARTIAL assert snapshot.title is None assert snapshot.price == 299.99 def test_parse_partial_status_without_price(self, store): """Parse sans prix doit avoir status PARTIAL.""" html = """ Test Product """ url = "https://www.amazon.fr/dp/B08N5WRWNW" snapshot = store.parse(html, url) assert snapshot.debug.status == DebugStatus.PARTIAL assert snapshot.title == "Test Product" assert snapshot.price is None class TestAmazonStoreInit: """Tests de l'initialisation du store Amazon.""" def test_store_id(self): """Le store_id doit être 'amazon'.""" store = AmazonStore() assert store.store_id == "amazon" def test_selectors_loaded(self): """Les sélecteurs doivent être chargés depuis selectors.yml.""" store = AmazonStore() # Vérifie que des sélecteurs ont été chargés assert isinstance(store.selectors, dict) # Devrait avoir au moins quelques sélecteurs assert len(store.selectors) > 0 def test_repr(self): """Test de la représentation string.""" store = AmazonStore() repr_str = repr(store) assert "AmazonStore" in repr_str assert "amazon" in repr_str