Files
scrap/tests/core/test_schema.py
2026-01-13 19:49:04 +01:00

332 lines
12 KiB
Python
Executable File

"""
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)