import pytest from proxy_pool.plugins.protocols import CheckContext, CheckResult, DiscoveredProxy, Event from proxy_pool.plugins.registry import PluginConflictError, PluginNotFoundError, PluginRegistry, PluginValidationError class FakeParser: name = "fake" def supports(self, url: str) -> bool: return "fake" in url async def parse(self, raw, source_url, source_id, default_protocol): return [DiscoveredProxy(ip="1.2.3.4", port=8080, protocol="http")] def default_schedule(self): return None class FakeChecker: name = "fake_check" stage = 1 priority = 0 timeout = 5.0 async def check(self, proxy_ip, proxy_port, proxy_protocol, context): return CheckResult(passed=True, detail="ok") def should_skip(self, proxy_protocol): return False class FakeNotifier: name = "fake_notify" subscribes_to = ["test.*"] notified_events: list[Event] def __init__(self): self.notified_events = [] async def notify(self, event): self.notified_events.append(event) async def health_check(self): return True class TestParserRegistration: def test_register_and_retrieve(self): registry = PluginRegistry() parser = FakeParser() registry.register_parser(parser) assert registry.get_parser("fake") is parser def test_duplicate_name_raises_conflict(self): registry = PluginRegistry() registry.register_parser(FakeParser()) with pytest.raises(PluginConflictError): registry.register_parser(FakeParser()) def test_get_missing_parser_raises(self): registry = PluginRegistry() with pytest.raises(PluginNotFoundError): registry.get_parser("nonexistent") def test_get_parser_for_url(self): registry = PluginRegistry() parser = FakeParser() registry.register_parser(parser) assert registry.get_parser_for_url("http://fake.com/list") is parser assert registry.get_parser_for_url("http://other.com/list") is None def test_invalid_plugin_raises_validation_error(self): registry = PluginRegistry() with pytest.raises(PluginValidationError): registry.register_parser("not a plugin") class TestCheckerRegistration: def test_checkers_sorted_by_stage_and_priority(self): registry = PluginRegistry() class Late(FakeChecker): name = "late" stage = 2 priority = 0 class Early(FakeChecker): name = "early" stage = 1 priority = 0 class EarlyLow(FakeChecker): name = "early_low" stage = 1 priority = 10 registry.register_checker(Late()) registry.register_checker(EarlyLow()) registry.register_checker(Early()) pipeline = registry.get_checker_pipeline() names = [c.name for c in pipeline] assert names == ["early", "early_low", "late"] class TestEventBus: async def test_emit_notifies_matching_subscribers(self): registry = PluginRegistry() notifier = FakeNotifier() registry.register_notifier(notifier) event = Event(type="test.something", payload={"key": "value"}) await registry.emit(event) # Give the fire-and-forget task a moment to run import asyncio await asyncio.sleep(0.05) assert len(notifier.notified_events) == 1 assert notifier.notified_events[0].type == "test.something" async def test_emit_skips_non_matching(self): registry = PluginRegistry() notifier = FakeNotifier() notifier.subscribes_to = ["other.*"] registry.register_notifier(notifier) await registry.emit(Event(type="test.something", payload={})) import asyncio await asyncio.sleep(0.05) assert len(notifier.notified_events) == 0