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

293 lines
10 KiB
Python
Executable File

"""
Tests pour pricewatch.app.core.registry
Vérifie l'enregistrement des stores, la détection automatique,
et les fonctions helper du registry.
"""
import pytest
from pricewatch.app.core.registry import StoreRegistry
from pricewatch.app.stores.base import BaseStore
from pricewatch.app.core.schema import ProductSnapshot
class MockStore(BaseStore):
"""Mock store pour les tests."""
def __init__(self, store_id: str, match_patterns: dict[str, float]):
"""
Args:
store_id: ID du store
match_patterns: Dict {substring: score} pour simuler match()
"""
super().__init__(store_id=store_id, selectors_path=None)
self.match_patterns = match_patterns
def match(self, url: str) -> float:
"""Retourne un score basé sur les patterns configurés."""
if not url:
return 0.0
url_lower = url.lower()
for pattern, score in self.match_patterns.items():
if pattern in url_lower:
return score
return 0.0
def canonicalize(self, url: str) -> str:
"""Mock canonicalize."""
return url
def extract_reference(self, url: str) -> str | None:
"""Mock extract_reference."""
return "TEST_REF"
def parse(self, html: str, url: str, **kwargs) -> ProductSnapshot:
"""Mock parse - pas utilisé dans les tests du registry."""
raise NotImplementedError("Mock parse not implemented")
class TestStoreRegistry:
"""Tests du StoreRegistry."""
@pytest.fixture
def registry(self) -> StoreRegistry:
"""Fixture: Registry vide."""
return StoreRegistry()
@pytest.fixture
def mock_amazon(self) -> MockStore:
"""Fixture: Mock Amazon store."""
return MockStore(
store_id="amazon",
match_patterns={"amazon.fr": 0.9, "amazon.com": 0.8},
)
@pytest.fixture
def mock_cdiscount(self) -> MockStore:
"""Fixture: Mock Cdiscount store."""
return MockStore(
store_id="cdiscount",
match_patterns={"cdiscount.com": 0.9},
)
def test_registry_init_empty(self, registry):
"""Un registry vide ne contient aucun store."""
assert len(registry) == 0
assert registry.list_stores() == []
def test_register_single_store(self, registry, mock_amazon):
"""Enregistre un seul store."""
registry.register(mock_amazon)
assert len(registry) == 1
assert "amazon" in registry.list_stores()
def test_register_multiple_stores(self, registry, mock_amazon, mock_cdiscount):
"""Enregistre plusieurs stores."""
registry.register(mock_amazon)
registry.register(mock_cdiscount)
assert len(registry) == 2
assert set(registry.list_stores()) == {"amazon", "cdiscount"}
def test_register_invalid_type(self, registry):
"""Enregistrer un objet non-BaseStore doit échouer."""
with pytest.raises(TypeError) as exc_info:
registry.register("not a store")
assert "Expected BaseStore" in str(exc_info.value)
def test_register_duplicate_replaces(self, registry, mock_amazon):
"""Enregistrer deux fois le même store_id remplace le premier."""
registry.register(mock_amazon)
assert len(registry) == 1
# Créer un autre mock avec le même ID
duplicate = MockStore(store_id="amazon", match_patterns={"amazon.es": 0.7})
registry.register(duplicate)
# Doit toujours avoir un seul store
assert len(registry) == 1
assert "amazon" in registry.list_stores()
# Doit avoir le nouveau store
store = registry.get_store("amazon")
assert store is duplicate
def test_get_store_existing(self, registry, mock_amazon):
"""Récupère un store existant."""
registry.register(mock_amazon)
store = registry.get_store("amazon")
assert store is mock_amazon
def test_get_store_non_existing(self, registry):
"""Récupère un store inexistant retourne None."""
store = registry.get_store("nonexistent")
assert store is None
def test_unregister_existing(self, registry, mock_amazon):
"""Désenregistre un store existant."""
registry.register(mock_amazon)
assert len(registry) == 1
removed = registry.unregister("amazon")
assert removed is True
assert len(registry) == 0
assert "amazon" not in registry.list_stores()
def test_unregister_non_existing(self, registry):
"""Désenregistre un store inexistant retourne False."""
removed = registry.unregister("nonexistent")
assert removed is False
def test_detect_store_empty_url(self, registry, mock_amazon):
"""URL vide retourne None."""
registry.register(mock_amazon)
store = registry.detect_store("")
assert store is None
def test_detect_store_whitespace_url(self, registry, mock_amazon):
"""URL avec espaces retourne None."""
registry.register(mock_amazon)
store = registry.detect_store(" ")
assert store is None
def test_detect_store_empty_registry(self, registry):
"""Registry vide retourne None."""
store = registry.detect_store("https://example.com")
assert store is None
def test_detect_store_single_match(self, registry, mock_amazon):
"""Détecte un store avec un seul match."""
registry.register(mock_amazon)
store = registry.detect_store("https://www.amazon.fr/dp/B08N5WRWNW")
assert store is mock_amazon
def test_detect_store_no_match(self, registry, mock_amazon):
"""Aucun store ne match retourne None."""
registry.register(mock_amazon)
store = registry.detect_store("https://www.ebay.com/item/123")
assert store is None
def test_detect_store_multiple_matches_best_score(
self, registry, mock_amazon, mock_cdiscount
):
"""Avec plusieurs matches, retourne le meilleur score."""
registry.register(mock_amazon)
registry.register(mock_cdiscount)
# Test Amazon
store = registry.detect_store("https://www.amazon.fr/dp/B08N5WRWNW")
assert store is mock_amazon
# Test Cdiscount
store = registry.detect_store("https://www.cdiscount.com/product/123")
assert store is mock_cdiscount
def test_detect_store_ambiguous_url_best_score(self, registry):
"""URL ambiguë: retourne le store avec le meilleur score."""
# Créer deux stores avec des scores différents pour la même URL
store_a = MockStore(store_id="store_a", match_patterns={"example.com": 0.7})
store_b = MockStore(store_id="store_b", match_patterns={"example.com": 0.9})
registry.register(store_a)
registry.register(store_b)
store = registry.detect_store("https://www.example.com")
assert store is store_b # Meilleur score (0.9 vs 0.7)
def test_detect_store_exception_in_match(self, registry, mock_amazon):
"""Si un store.match() lève une exception, continue avec les autres."""
# Créer un store qui crash
class BrokenStore(MockStore):
def match(self, url: str) -> float:
raise RuntimeError("Simulated crash")
broken = BrokenStore(store_id="broken", match_patterns={})
registry.register(broken)
registry.register(mock_amazon)
# Doit quand même détecter Amazon malgré le crash du broken store
store = registry.detect_store("https://www.amazon.fr/dp/B08N5WRWNW")
assert store is mock_amazon
def test_list_stores_empty(self, registry):
"""Liste des stores vide."""
assert registry.list_stores() == []
def test_list_stores_multiple(self, registry, mock_amazon, mock_cdiscount):
"""Liste des stores avec plusieurs enregistrés."""
registry.register(mock_amazon)
registry.register(mock_cdiscount)
stores = registry.list_stores()
assert len(stores) == 2
assert "amazon" in stores
assert "cdiscount" in stores
def test_len_operator(self, registry, mock_amazon, mock_cdiscount):
"""Opérateur len() retourne le nombre de stores."""
assert len(registry) == 0
registry.register(mock_amazon)
assert len(registry) == 1
registry.register(mock_cdiscount)
assert len(registry) == 2
registry.unregister("amazon")
assert len(registry) == 1
def test_repr(self, registry, mock_amazon, mock_cdiscount):
"""Représentation string du registry."""
registry.register(mock_amazon)
registry.register(mock_cdiscount)
repr_str = repr(registry)
assert "StoreRegistry" in repr_str
assert "amazon" in repr_str
assert "cdiscount" in repr_str
class TestRegistryGlobalFunctions:
"""Tests des fonctions globales du module registry."""
def test_get_registry_singleton(self):
"""get_registry() retourne toujours la même instance."""
from pricewatch.app.core.registry import get_registry
registry1 = get_registry()
registry2 = get_registry()
assert registry1 is registry2
def test_register_store_global(self):
"""register_store() enregistre dans le registry global."""
from pricewatch.app.core.registry import get_registry, register_store
# Nettoyer le registry global pour le test
registry = get_registry()
initial_count = len(registry)
mock = MockStore(store_id="test_global", match_patterns={})
register_store(mock)
assert len(registry) == initial_count + 1
assert "test_global" in registry.list_stores()
# Cleanup
registry.unregister("test_global")
def test_detect_store_global(self):
"""detect_store() utilise le registry global."""
from pricewatch.app.core.registry import detect_store, get_registry, register_store
# Nettoyer le registry global pour le test
registry = get_registry()
mock = MockStore(store_id="test_detect", match_patterns={"testsite.com": 0.9})
register_store(mock)
store = detect_store("https://www.testsite.com/product")
assert store is not None
assert store.store_id == "test_detect"
# Cleanup
registry.unregister("test_detect")