chore: sync project files
This commit is contained in:
331
tests/core/test_schema.py
Executable file
331
tests/core/test_schema.py
Executable file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Tests pour pricewatch.app.core.schema
|
||||
|
||||
Vérifie la validation Pydantic, la serialization JSON,
|
||||
et les méthodes helper de ProductSnapshot.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from pricewatch.app.core.schema import (
|
||||
DebugInfo,
|
||||
DebugStatus,
|
||||
FetchMethod,
|
||||
ProductSnapshot,
|
||||
StockStatus,
|
||||
)
|
||||
|
||||
|
||||
class TestEnums:
|
||||
"""Tests des enums."""
|
||||
|
||||
def test_stock_status_values(self):
|
||||
"""Vérifie les valeurs de StockStatus."""
|
||||
assert StockStatus.IN_STOCK.value == "in_stock"
|
||||
assert StockStatus.OUT_OF_STOCK.value == "out_of_stock"
|
||||
assert StockStatus.UNKNOWN.value == "unknown"
|
||||
|
||||
def test_fetch_method_values(self):
|
||||
"""Vérifie les valeurs de FetchMethod."""
|
||||
assert FetchMethod.HTTP.value == "http"
|
||||
assert FetchMethod.PLAYWRIGHT.value == "playwright"
|
||||
|
||||
def test_debug_status_values(self):
|
||||
"""Vérifie les valeurs de DebugStatus."""
|
||||
assert DebugStatus.SUCCESS.value == "success"
|
||||
assert DebugStatus.PARTIAL.value == "partial"
|
||||
assert DebugStatus.FAILED.value == "failed"
|
||||
|
||||
|
||||
class TestDebugInfo:
|
||||
"""Tests du modèle DebugInfo."""
|
||||
|
||||
def test_debug_info_creation(self):
|
||||
"""Crée un DebugInfo valide."""
|
||||
debug = DebugInfo(
|
||||
method=FetchMethod.HTTP,
|
||||
status=DebugStatus.SUCCESS,
|
||||
duration_ms=1500,
|
||||
html_size_bytes=120000,
|
||||
)
|
||||
assert debug.method == FetchMethod.HTTP
|
||||
assert debug.status == DebugStatus.SUCCESS
|
||||
assert debug.duration_ms == 1500
|
||||
assert debug.html_size_bytes == 120000
|
||||
assert debug.errors == []
|
||||
assert debug.notes == []
|
||||
|
||||
def test_debug_info_with_errors(self):
|
||||
"""Crée un DebugInfo avec des erreurs."""
|
||||
debug = DebugInfo(
|
||||
method=FetchMethod.PLAYWRIGHT,
|
||||
status=DebugStatus.FAILED,
|
||||
errors=["403 Forbidden", "Captcha detected"],
|
||||
notes=["Fallback to Playwright triggered"],
|
||||
)
|
||||
assert len(debug.errors) == 2
|
||||
assert "403 Forbidden" in debug.errors
|
||||
assert len(debug.notes) == 1
|
||||
|
||||
def test_debug_info_defaults(self):
|
||||
"""Vérifie les valeurs par défaut."""
|
||||
debug = DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS)
|
||||
assert debug.errors == []
|
||||
assert debug.notes == []
|
||||
assert debug.duration_ms is None
|
||||
assert debug.html_size_bytes is None
|
||||
|
||||
|
||||
class TestProductSnapshot:
|
||||
"""Tests du modèle ProductSnapshot."""
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_snapshot(self) -> ProductSnapshot:
|
||||
"""Fixture: ProductSnapshot minimal valide."""
|
||||
return ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://www.amazon.fr/dp/B08N5WRWNW",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def complete_snapshot(self) -> ProductSnapshot:
|
||||
"""Fixture: ProductSnapshot complet."""
|
||||
return ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://www.amazon.fr/dp/B08N5WRWNW",
|
||||
fetched_at=datetime(2026, 1, 13, 10, 30, 0),
|
||||
title="PlayStation 5",
|
||||
price=499.99,
|
||||
currency="EUR",
|
||||
shipping_cost=0.0,
|
||||
stock_status=StockStatus.IN_STOCK,
|
||||
reference="B08N5WRWNW",
|
||||
category="Jeux vidéo",
|
||||
images=[
|
||||
"https://example.com/image1.jpg",
|
||||
"https://example.com/image2.jpg",
|
||||
],
|
||||
specs={
|
||||
"Marque": "Sony",
|
||||
"Couleur": "Blanc",
|
||||
"Poids": "4.5 kg",
|
||||
},
|
||||
debug=DebugInfo(
|
||||
method=FetchMethod.HTTP,
|
||||
status=DebugStatus.SUCCESS,
|
||||
duration_ms=1200,
|
||||
html_size_bytes=145000,
|
||||
),
|
||||
)
|
||||
|
||||
def test_create_minimal_snapshot(self, minimal_snapshot):
|
||||
"""Crée un ProductSnapshot minimal."""
|
||||
assert minimal_snapshot.source == "amazon"
|
||||
assert minimal_snapshot.url == "https://www.amazon.fr/dp/B08N5WRWNW"
|
||||
assert minimal_snapshot.title is None
|
||||
assert minimal_snapshot.price is None
|
||||
assert minimal_snapshot.currency == "EUR" # Default
|
||||
assert minimal_snapshot.stock_status == StockStatus.UNKNOWN # Default
|
||||
|
||||
def test_create_complete_snapshot(self, complete_snapshot):
|
||||
"""Crée un ProductSnapshot complet."""
|
||||
assert complete_snapshot.source == "amazon"
|
||||
assert complete_snapshot.title == "PlayStation 5"
|
||||
assert complete_snapshot.price == 499.99
|
||||
assert complete_snapshot.reference == "B08N5WRWNW"
|
||||
assert len(complete_snapshot.images) == 2
|
||||
assert len(complete_snapshot.specs) == 3
|
||||
|
||||
def test_url_validation_empty(self):
|
||||
"""URL vide doit échouer."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ProductSnapshot(
|
||||
source="amazon",
|
||||
url="",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert "URL cannot be empty" in str(exc_info.value)
|
||||
|
||||
def test_url_validation_whitespace(self):
|
||||
"""URL avec seulement des espaces doit échouer."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ProductSnapshot(
|
||||
source="amazon",
|
||||
url=" ",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert "URL cannot be empty" in str(exc_info.value)
|
||||
|
||||
def test_source_validation_empty(self):
|
||||
"""Source vide doit échouer."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ProductSnapshot(
|
||||
source="",
|
||||
url="https://example.com",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert "Source cannot be empty" in str(exc_info.value)
|
||||
|
||||
def test_source_normalization(self):
|
||||
"""Source doit être normalisée en lowercase."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="AMAZON",
|
||||
url="https://example.com",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert snapshot.source == "amazon"
|
||||
|
||||
def test_price_negative(self):
|
||||
"""Prix négatif doit échouer."""
|
||||
with pytest.raises(ValidationError):
|
||||
ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
price=-10.0,
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
|
||||
def test_shipping_cost_negative(self):
|
||||
"""Frais de port négatifs doivent échouer."""
|
||||
with pytest.raises(ValidationError):
|
||||
ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
shipping_cost=-5.0,
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
|
||||
def test_images_validation(self):
|
||||
"""Les URLs d'images vides doivent être filtrées."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
images=["https://img1.jpg", "", " ", "https://img2.jpg"],
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert len(snapshot.images) == 2
|
||||
assert "https://img1.jpg" in snapshot.images
|
||||
assert "https://img2.jpg" in snapshot.images
|
||||
|
||||
def test_is_complete_with_title_and_price(self, complete_snapshot):
|
||||
"""Un snapshot avec titre et prix est complet."""
|
||||
assert complete_snapshot.is_complete() is True
|
||||
|
||||
def test_is_complete_without_price(self, minimal_snapshot):
|
||||
"""Un snapshot sans prix n'est pas complet."""
|
||||
minimal_snapshot.title = "Test Product"
|
||||
assert minimal_snapshot.is_complete() is False
|
||||
|
||||
def test_is_complete_without_title(self, minimal_snapshot):
|
||||
"""Un snapshot sans titre n'est pas complet."""
|
||||
minimal_snapshot.price = 99.99
|
||||
assert minimal_snapshot.is_complete() is False
|
||||
|
||||
def test_is_complete_minimal(self, minimal_snapshot):
|
||||
"""Un snapshot minimal n'est pas complet."""
|
||||
assert minimal_snapshot.is_complete() is False
|
||||
|
||||
def test_add_error(self, minimal_snapshot):
|
||||
"""Ajoute une erreur au debug."""
|
||||
minimal_snapshot.add_error("Test error 1")
|
||||
minimal_snapshot.add_error("Test error 2")
|
||||
assert len(minimal_snapshot.debug.errors) == 2
|
||||
assert "Test error 1" in minimal_snapshot.debug.errors
|
||||
|
||||
def test_add_note(self, minimal_snapshot):
|
||||
"""Ajoute une note au debug."""
|
||||
minimal_snapshot.add_note("Test note 1")
|
||||
minimal_snapshot.add_note("Test note 2")
|
||||
assert len(minimal_snapshot.debug.notes) == 2
|
||||
assert "Test note 1" in minimal_snapshot.debug.notes
|
||||
|
||||
def test_to_dict(self, complete_snapshot):
|
||||
"""Serialization vers dict."""
|
||||
data = complete_snapshot.to_dict()
|
||||
assert isinstance(data, dict)
|
||||
assert data["source"] == "amazon"
|
||||
assert data["title"] == "PlayStation 5"
|
||||
assert data["price"] == 499.99
|
||||
assert isinstance(data["fetched_at"], str) # ISO format
|
||||
assert data["debug"]["method"] == "http"
|
||||
|
||||
def test_to_json(self, complete_snapshot):
|
||||
"""Serialization vers JSON."""
|
||||
json_str = complete_snapshot.to_json()
|
||||
assert isinstance(json_str, str)
|
||||
|
||||
# Vérifie que c'est du JSON valide
|
||||
data = json.loads(json_str)
|
||||
assert data["source"] == "amazon"
|
||||
assert data["title"] == "PlayStation 5"
|
||||
assert data["price"] == 499.99
|
||||
|
||||
def test_from_json(self, complete_snapshot):
|
||||
"""Désérialisation depuis JSON."""
|
||||
# Serialize puis deserialize
|
||||
json_str = complete_snapshot.to_json()
|
||||
restored = ProductSnapshot.from_json(json_str)
|
||||
|
||||
assert restored.source == complete_snapshot.source
|
||||
assert restored.title == complete_snapshot.title
|
||||
assert restored.price == complete_snapshot.price
|
||||
assert restored.reference == complete_snapshot.reference
|
||||
|
||||
def test_to_dict_and_from_json_roundtrip(self, complete_snapshot):
|
||||
"""Roundtrip complet dict → JSON → ProductSnapshot."""
|
||||
# to_dict puis JSON puis from_json
|
||||
json_str = json.dumps(complete_snapshot.to_dict())
|
||||
restored = ProductSnapshot.from_json(json_str)
|
||||
|
||||
assert restored.source == complete_snapshot.source
|
||||
assert restored.title == complete_snapshot.title
|
||||
assert restored.price == complete_snapshot.price
|
||||
|
||||
def test_enum_serialization(self):
|
||||
"""Les enums doivent être sérialisés en string."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
stock_status=StockStatus.IN_STOCK,
|
||||
debug=DebugInfo(method=FetchMethod.PLAYWRIGHT, status=DebugStatus.PARTIAL),
|
||||
)
|
||||
|
||||
data = snapshot.to_dict()
|
||||
assert data["stock_status"] == "in_stock"
|
||||
assert data["debug"]["method"] == "playwright"
|
||||
assert data["debug"]["status"] == "partial"
|
||||
|
||||
def test_fetched_at_default(self):
|
||||
"""fetched_at doit avoir une valeur par défaut."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert snapshot.fetched_at is not None
|
||||
assert isinstance(snapshot.fetched_at, datetime)
|
||||
|
||||
def test_specs_default(self):
|
||||
"""specs doit être un dict vide par défaut."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert snapshot.specs == {}
|
||||
assert isinstance(snapshot.specs, dict)
|
||||
|
||||
def test_images_default(self):
|
||||
"""images doit être une liste vide par défaut."""
|
||||
snapshot = ProductSnapshot(
|
||||
source="amazon",
|
||||
url="https://example.com",
|
||||
debug=DebugInfo(method=FetchMethod.HTTP, status=DebugStatus.SUCCESS),
|
||||
)
|
||||
assert snapshot.images == []
|
||||
assert isinstance(snapshot.images, list)
|
||||
Reference in New Issue
Block a user