chore: sync project files
This commit is contained in:
386
tests/stores/test_amazon.py
Executable file
386
tests/stores/test_amazon.py
Executable file
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
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 """
|
||||
<html>
|
||||
<head><title>Test Product</title></head>
|
||||
<body>
|
||||
<span id="productTitle">Test Amazon Product</span>
|
||||
<span class="a-price-whole">299,99 €</span>
|
||||
<span class="a-price-symbol">€</span>
|
||||
<div id="availability">
|
||||
<span class="a-size-medium a-color-success">En stock</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def complete_html(self) -> str:
|
||||
"""Fixture: HTML Amazon complet."""
|
||||
return """
|
||||
<html>
|
||||
<head><title>Test Product</title></head>
|
||||
<body>
|
||||
<span id="productTitle">PlayStation 5 Console</span>
|
||||
<span class="a-price-whole">499,99 €</span>
|
||||
<span class="a-price-symbol">€</span>
|
||||
<div id="availability">
|
||||
<span class="a-size-medium a-color-success">En stock</span>
|
||||
</div>
|
||||
<input type="hidden" name="ASIN" value="B08N5WRWNW" />
|
||||
<div id="wayfinding-breadcrumbs_feature_div">
|
||||
<ul>
|
||||
<li><a>Accueil</a></li>
|
||||
<li><a>High-Tech</a></li>
|
||||
<li><a>Jeux vidéo</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<img src="https://m.media-amazon.com/images/I/image1.jpg" />
|
||||
<img src="https://m.media-amazon.com/images/I/image2.jpg" />
|
||||
<table id="productDetails_techSpec_section_1">
|
||||
<tr>
|
||||
<th>Marque</th>
|
||||
<td>Sony</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Couleur</th>
|
||||
<td>Blanc</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def captcha_html(self) -> str:
|
||||
"""Fixture: HTML avec captcha."""
|
||||
return """
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<p>Sorry, we just need to make sure you're not a robot.</p>
|
||||
<form action="/captcha">
|
||||
<input type="text" name="captcha" />
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def out_of_stock_html(self) -> str:
|
||||
"""Fixture: HTML produit en rupture de stock."""
|
||||
return """
|
||||
<html>
|
||||
<body>
|
||||
<span id="productTitle">Out of Stock Product</span>
|
||||
<span class="a-price-whole">199,99 €</span>
|
||||
<div id="availability">
|
||||
<span class="a-size-medium a-color-price">Actuellement indisponible</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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("<html><body></body></html>", 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 = """
|
||||
<html><body>
|
||||
<span class="a-price-whole">299</span>
|
||||
<span class="a-price-fraction">99</span>
|
||||
</body></html>
|
||||
"""
|
||||
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 = """
|
||||
<html><body>
|
||||
<span id="productTitle">Test Product</span>
|
||||
</body></html>
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user