360 lines
12 KiB
Python
360 lines
12 KiB
Python
"""入站/出站中间件注册表单元测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from yuxi.channel.exceptions import ChannelConfigurationError
|
|
from yuxi.channel.middlewares.protocols import InboundContext, InboundResult, OutboundContext, OutboundResult
|
|
from yuxi.channel.middlewares.registry import InboundMiddlewareRegistry, OutboundMiddlewareRegistry
|
|
|
|
|
|
class _DummyInbound:
|
|
name = "dummy"
|
|
default_order = 100
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _AlphaInbound:
|
|
name = "alpha"
|
|
default_order = 200
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _BetaInbound:
|
|
name = "beta"
|
|
default_order = 100
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _DedupeInbound:
|
|
name = "dedupe"
|
|
default_order = 100
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _TransactionInbound:
|
|
name = "transaction"
|
|
default_order = 300
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _SessionInbound:
|
|
name = "session"
|
|
default_order = 400
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _DummyOutbound:
|
|
name = "dummy"
|
|
default_order = 100
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _AlphaOutbound:
|
|
name = "alpha"
|
|
default_order = 200
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _BetaOutbound:
|
|
name = "beta"
|
|
default_order = 100
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _SendOutbound:
|
|
name = "send"
|
|
default_order = 700
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
class _StatusUpdateOutbound:
|
|
name = "status_update"
|
|
default_order = 800
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
|
|
@pytest.fixture
|
|
def inbound_registry() -> InboundMiddlewareRegistry:
|
|
return InboundMiddlewareRegistry()
|
|
|
|
|
|
@pytest.fixture
|
|
def outbound_registry() -> OutboundMiddlewareRegistry:
|
|
return OutboundMiddlewareRegistry()
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestInboundMiddlewareRegistry:
|
|
def test_register_and_unregister(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
mw = _DummyInbound()
|
|
inbound_registry.register(mw)
|
|
assert inbound_registry.get("dummy") is mw
|
|
|
|
removed = inbound_registry.unregister("dummy")
|
|
assert removed is mw
|
|
assert inbound_registry.get("dummy") is None
|
|
|
|
def test_register_requires_name(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
class NoName:
|
|
name = ""
|
|
default_order = 0
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
with pytest.raises(ValueError):
|
|
inbound_registry.register(NoName())
|
|
|
|
def test_default_chain_sorted_by_order(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
alpha = _AlphaInbound()
|
|
beta = _BetaInbound()
|
|
inbound_registry.register(alpha)
|
|
inbound_registry.register(beta)
|
|
|
|
chain = inbound_registry.resolve_chain({})
|
|
assert [m.name for m in chain] == ["beta", "alpha"]
|
|
|
|
def test_configured_chain_enabled_and_sorted(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
alpha = _AlphaInbound()
|
|
beta = _BetaInbound()
|
|
inbound_registry.register(alpha)
|
|
inbound_registry.register(beta)
|
|
|
|
config = {
|
|
"channel_type": "feishu",
|
|
"account_id": "a1",
|
|
"inbound_middlewares": [
|
|
{"name": "alpha", "enabled": True, "order": 50},
|
|
{"name": "beta", "enabled": True, "order": 150},
|
|
],
|
|
}
|
|
chain = inbound_registry.resolve_chain(config)
|
|
assert [m.name for m in chain] == ["alpha", "beta"]
|
|
|
|
def test_configured_chain_uses_default_order_when_missing(
|
|
self, inbound_registry: InboundMiddlewareRegistry
|
|
) -> None:
|
|
alpha = _AlphaInbound()
|
|
inbound_registry.register(alpha)
|
|
|
|
config = {"inbound_middlewares": [{"name": "alpha", "enabled": True}]}
|
|
chain = inbound_registry.resolve_chain(config)
|
|
assert [m.name for m in chain] == ["alpha"]
|
|
|
|
def test_configured_chain_disabled_ignored(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
alpha = _AlphaInbound()
|
|
beta = _BetaInbound()
|
|
inbound_registry.register(alpha)
|
|
inbound_registry.register(beta)
|
|
|
|
config = {
|
|
"inbound_middlewares": [
|
|
{"name": "alpha", "enabled": False, "order": 1},
|
|
{"name": "beta", "enabled": True, "order": 2},
|
|
],
|
|
}
|
|
chain = inbound_registry.resolve_chain(config)
|
|
assert [m.name for m in chain] == ["beta"]
|
|
|
|
def test_configured_chain_unknown_name_warns_and_ignored(
|
|
self, inbound_registry: InboundMiddlewareRegistry, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
inbound_registry.register(_AlphaInbound())
|
|
|
|
warn_mock = MagicMock()
|
|
monkeypatch.setattr("yuxi.channel.middlewares.registry.logger", type("L", (), {"warning": warn_mock})())
|
|
|
|
config = {
|
|
"inbound_middlewares": [
|
|
{"name": "unknown", "enabled": True},
|
|
{"name": "alpha", "enabled": True},
|
|
],
|
|
}
|
|
chain = inbound_registry.resolve_chain(config)
|
|
assert [m.name for m in chain] == ["alpha"]
|
|
warn_mock.assert_called_once()
|
|
|
|
def test_resolve_chain_caches_by_config_hash(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
mw = _AlphaInbound()
|
|
inbound_registry.register(mw)
|
|
|
|
config = {
|
|
"channel_type": "feishu",
|
|
"account_id": "a1",
|
|
"inbound_middlewares": [{"name": "alpha", "enabled": True, "order": 50}],
|
|
}
|
|
chain1 = inbound_registry.resolve_chain(config)
|
|
chain2 = inbound_registry.resolve_chain(config)
|
|
assert chain1 is chain2
|
|
|
|
def test_invalidate_removes_cache_for_account(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
mw = _AlphaInbound()
|
|
inbound_registry.register(mw)
|
|
|
|
config1 = {
|
|
"channel_type": "feishu",
|
|
"account_id": "a1",
|
|
"inbound_middlewares": [{"name": "alpha", "enabled": True}],
|
|
}
|
|
config2 = {
|
|
"channel_type": "feishu",
|
|
"account_id": "a2",
|
|
"inbound_middlewares": [{"name": "alpha", "enabled": True}],
|
|
}
|
|
chain1 = inbound_registry.resolve_chain(config1)
|
|
chain2 = inbound_registry.resolve_chain(config2)
|
|
|
|
inbound_registry.invalidate("feishu", "a1")
|
|
assert inbound_registry.resolve_chain(config1) is not chain1
|
|
assert inbound_registry.resolve_chain(config2) is chain2
|
|
|
|
def test_dedupe_must_be_first(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
inbound_registry.register(_DedupeInbound())
|
|
inbound_registry.register(_AlphaInbound())
|
|
|
|
config = {
|
|
"inbound_middlewares": [
|
|
{"name": "alpha", "enabled": True, "order": 50},
|
|
{"name": "dedupe", "enabled": True, "order": 100},
|
|
],
|
|
}
|
|
with pytest.raises(ChannelConfigurationError, match="dedupe 必须位于链首"):
|
|
inbound_registry.resolve_chain(config)
|
|
|
|
def test_transaction_must_be_before_session_route_create_run(
|
|
self, inbound_registry: InboundMiddlewareRegistry
|
|
) -> None:
|
|
inbound_registry.register(_TransactionInbound())
|
|
inbound_registry.register(_SessionInbound())
|
|
|
|
config = {
|
|
"inbound_middlewares": [
|
|
{"name": "session", "enabled": True, "order": 100},
|
|
{"name": "transaction", "enabled": True, "order": 200},
|
|
],
|
|
}
|
|
with pytest.raises(ChannelConfigurationError, match="transaction 必须位于 session"):
|
|
inbound_registry.resolve_chain(config)
|
|
|
|
async def test_start_all_and_stop_all(self, inbound_registry: InboundMiddlewareRegistry) -> None:
|
|
start = MagicMock()
|
|
stop = MagicMock()
|
|
|
|
class _LifecycleInbound:
|
|
name = "lifecycle"
|
|
default_order = 0
|
|
|
|
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
|
|
return await next_mw()
|
|
|
|
async def start(self) -> None:
|
|
start()
|
|
|
|
async def stop(self) -> None:
|
|
stop()
|
|
|
|
inbound_registry.register(_LifecycleInbound())
|
|
await inbound_registry.start_all()
|
|
await inbound_registry.stop_all()
|
|
start.assert_called_once()
|
|
stop.assert_called_once()
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOutboundMiddlewareRegistry:
|
|
def test_register_and_unregister(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
|
|
mw = _DummyOutbound()
|
|
outbound_registry.register(mw)
|
|
assert outbound_registry.get("dummy") is mw
|
|
|
|
removed = outbound_registry.unregister("dummy")
|
|
assert removed is mw
|
|
assert outbound_registry.get("dummy") is None
|
|
|
|
def test_default_chain_sorted_by_order(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
|
|
alpha = _AlphaOutbound()
|
|
beta = _BetaOutbound()
|
|
outbound_registry.register(alpha)
|
|
outbound_registry.register(beta)
|
|
|
|
chain = outbound_registry.resolve_chain({})
|
|
assert [m.name for m in chain] == ["beta", "alpha"]
|
|
|
|
def test_configured_chain_enabled_and_sorted(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
|
|
alpha = _AlphaOutbound()
|
|
beta = _BetaOutbound()
|
|
outbound_registry.register(alpha)
|
|
outbound_registry.register(beta)
|
|
|
|
config = {
|
|
"channel_type": "feishu",
|
|
"account_id": "a1",
|
|
"outbound_middlewares": [
|
|
{"name": "alpha", "enabled": True, "order": 50},
|
|
{"name": "beta", "enabled": True, "order": 150},
|
|
],
|
|
}
|
|
chain = outbound_registry.resolve_chain(config)
|
|
assert [m.name for m in chain] == ["alpha", "beta"]
|
|
|
|
def test_send_must_be_before_status_update(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
|
|
outbound_registry.register(_StatusUpdateOutbound())
|
|
outbound_registry.register(_SendOutbound())
|
|
|
|
config = {
|
|
"outbound_middlewares": [
|
|
{"name": "status_update", "enabled": True, "order": 100},
|
|
{"name": "send", "enabled": True, "order": 200},
|
|
],
|
|
}
|
|
with pytest.raises(ChannelConfigurationError, match="send 必须位于 status_update 之前"):
|
|
outbound_registry.resolve_chain(config)
|
|
|
|
async def test_start_all_and_stop_all(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
|
|
start = MagicMock()
|
|
stop = MagicMock()
|
|
|
|
class _LifecycleOutbound:
|
|
name = "lifecycle"
|
|
default_order = 0
|
|
|
|
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
|
|
return await next_mw()
|
|
|
|
async def start(self) -> None:
|
|
start()
|
|
|
|
async def stop(self) -> None:
|
|
stop()
|
|
|
|
outbound_registry.register(_LifecycleOutbound())
|
|
await outbound_registry.start_all()
|
|
await outbound_registry.stop_all()
|
|
start.assert_called_once()
|
|
stop.assert_called_once()
|