193 lines
5.9 KiB
Python
193 lines
5.9 KiB
Python
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()
|