from __future__ import annotations from unittest.mock import MagicMock import pytest from yuxi.channel.security.registry import ( SecurityCheckerRegistry, SecurityContext, ) class _DummyChecker: name = "dummy" default_priority = 100 async def check(self, ctx: SecurityContext) -> object | None: return None class _AlphaChecker: name = "alpha" default_priority = 200 async def check(self, ctx: SecurityContext) -> object | None: return None class _BetaChecker: name = "beta" default_priority = 100 async def check(self, ctx: SecurityContext) -> object | None: return None @pytest.fixture def registry() -> SecurityCheckerRegistry: return SecurityCheckerRegistry() @pytest.mark.unit class TestSecurityCheckerRegistry: def test_register_and_unregister(self, registry: SecurityCheckerRegistry) -> None: checker = _DummyChecker() registry.register(checker) assert registry.get("dummy") is checker removed = registry.unregister("dummy") assert removed is checker assert registry.get("dummy") is None def test_register_requires_name(self, registry: SecurityCheckerRegistry) -> None: class NoName: name = "" async def check(self, ctx: SecurityContext) -> None: return None with pytest.raises(ValueError): registry.register(NoName()) def test_default_chain_sorted_by_priority(self, registry: SecurityCheckerRegistry) -> None: alpha = _AlphaChecker() beta = _BetaChecker() registry.register(alpha) registry.register(beta) chain = registry.resolve_chain({}) assert [c.name for c in chain] == ["beta", "alpha"] def test_configured_chain_enabled_and_sorted(self, registry: SecurityCheckerRegistry) -> None: alpha = _AlphaChecker() beta = _BetaChecker() registry.register(alpha) registry.register(beta) config = { "channel_type": "feishu", "account_id": "a1", "security_checkers": [ {"name": "alpha", "enabled": True, "priority": 50}, {"name": "beta", "enabled": True, "priority": 150}, ], } chain = registry.resolve_chain(config) assert [c.name for c in chain] == ["alpha", "beta"] def test_configured_chain_uses_default_priority_when_missing(self, registry: SecurityCheckerRegistry) -> None: alpha = _AlphaChecker() beta = _BetaChecker() registry.register(alpha) registry.register(beta) config = { "security_checkers": [ {"name": "alpha", "enabled": True}, ], } chain = registry.resolve_chain(config) assert [c.name for c in chain] == ["alpha"] def test_configured_chain_disabled_ignored(self, registry: SecurityCheckerRegistry) -> None: alpha = _AlphaChecker() beta = _BetaChecker() registry.register(alpha) registry.register(beta) config = { "security_checkers": [ {"name": "alpha", "enabled": False, "priority": 1}, {"name": "beta", "enabled": True, "priority": 2}, ], } chain = registry.resolve_chain(config) assert [c.name for c in chain] == ["beta"] def test_configured_chain_unknown_name_warns_and_ignored( self, registry: SecurityCheckerRegistry, monkeypatch: pytest.MonkeyPatch ) -> None: registry.register(_AlphaChecker()) warn_mock = MagicMock() monkeypatch.setattr("yuxi.channel.security.registry.logger", type("L", (), {"warning": warn_mock})()) config = { "security_checkers": [ {"name": "unknown", "enabled": True}, {"name": "alpha", "enabled": True}, ], } chain = registry.resolve_chain(config) assert [c.name for c in chain] == ["alpha"] warn_mock.assert_called_once() def test_resolve_chain_caches_by_config_hash(self, registry: SecurityCheckerRegistry) -> None: checker = _AlphaChecker() registry.register(checker) config = { "channel_type": "feishu", "account_id": "a1", "security_checkers": [ {"name": "alpha", "enabled": True, "priority": 50}, ], } chain1 = registry.resolve_chain(config) chain2 = registry.resolve_chain(config) assert chain1 is chain2 def test_invalidate_removes_cache_for_account(self, registry: SecurityCheckerRegistry) -> None: checker = _AlphaChecker() registry.register(checker) config1 = { "channel_type": "feishu", "account_id": "a1", "security_checkers": [{"name": "alpha", "enabled": True}], } config2 = { "channel_type": "feishu", "account_id": "a2", "security_checkers": [{"name": "alpha", "enabled": True}], } chain1 = registry.resolve_chain(config1) chain2 = registry.resolve_chain(config2) registry.invalidate("feishu", "a1") assert registry.resolve_chain(config1) is not chain1 assert registry.resolve_chain(config2) is chain2 async def test_start_all_and_stop_all(self, registry: SecurityCheckerRegistry) -> None: start = MagicMock() stop = MagicMock() class _LifecycleChecker: name = "lifecycle" default_priority = 0 async def check(self, ctx: SecurityContext) -> None: return None async def start(self) -> None: start() async def stop(self) -> None: stop() registry.register(_LifecycleChecker()) await registry.start_all() await registry.stop_all() start.assert_called_once() stop.assert_called_once()