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