1. 删除了 test/unit/external_systems/framework/ 下的废弃空测试目录 2. 修复多处测试断言逻辑、参数传递与测试数据构造 3. 新增日志级别、缓存令牌、流事件等DTO单元测试 4. 补充路由绑定、会话仓储、outbox仓储的测试覆盖 5. 更新测试用例中的异常类型、参数校验与业务逻辑断言
223 lines
6.7 KiB
Python
223 lines
6.7 KiB
Python
"""route.py DTO 单元测试。
|
|
|
|
覆盖 ``MatchSource`` / ``ConfigMatchSource`` / ``RouteBinding`` /
|
|
``MatchTier`` / ``BindingContext`` / ``ConfigFallbackChain`` /
|
|
``NestedWhitelistDecision`` / ``RouteBindingRule`` / ``SaveRouteBindingCmd`` /
|
|
``UpdateRouteBindingCmd`` / ``RouteBindingFilter`` 的字段赋值、默认值、
|
|
不可变语义与 ``__post_init__`` 校验逻辑。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
from yuxi.channels.contract.dtos.route import (
|
|
BindingContext,
|
|
ConfigFallbackChain,
|
|
ConfigMatchSource,
|
|
MatchSource,
|
|
MatchTier,
|
|
NestedWhitelistDecision,
|
|
RouteBinding,
|
|
RouteBindingFilter,
|
|
RouteBindingRule,
|
|
SaveRouteBindingCmd,
|
|
UpdateRouteBindingCmd,
|
|
)
|
|
from yuxi.channels.contract.errors import ValidationError
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMatchSource:
|
|
"""匹配来源枚举测试。"""
|
|
|
|
def test_str_values(self):
|
|
assert MatchSource.EXPLICIT == "explicit"
|
|
assert MatchSource.SESSION == "session"
|
|
assert MatchSource.IDENTITY == "identity"
|
|
assert MatchSource.DEFAULT == "default"
|
|
assert MatchSource.CHAT_TYPE == "chat_type"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestConfigMatchSource:
|
|
"""配置匹配来源枚举测试。"""
|
|
|
|
def test_str_values(self):
|
|
assert ConfigMatchSource.DIRECT == "direct"
|
|
assert ConfigMatchSource.PARENT == "parent"
|
|
assert ConfigMatchSource.WILDCARD == "wildcard"
|
|
assert ConfigMatchSource.CHANNEL_BINDING == "channel_binding"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestRouteBinding:
|
|
"""路由绑定 DTO 测试。"""
|
|
|
|
def test_fields_are_assigned_with_defaults(self):
|
|
# Act
|
|
binding = RouteBinding(
|
|
channel_account_id="acc-1",
|
|
agent_binding="agent-slug",
|
|
match_source=MatchSource.EXPLICIT,
|
|
)
|
|
# Assert
|
|
assert binding.channel_session_id is None
|
|
assert binding.matched_layer is None
|
|
assert binding.config_match_source is None
|
|
|
|
def test_is_frozen(self):
|
|
# Arrange
|
|
binding = RouteBinding(
|
|
channel_account_id="a1",
|
|
agent_binding="slug",
|
|
match_source=MatchSource.DEFAULT,
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
binding.agent_binding = "other" # type: ignore[misc]
|
|
|
|
def test_empty_channel_account_id_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RouteBinding(
|
|
channel_account_id="",
|
|
agent_binding="slug",
|
|
match_source=MatchSource.DEFAULT,
|
|
)
|
|
assert exc_info.value.field == "channel_account_id"
|
|
|
|
def test_empty_agent_binding_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RouteBinding(
|
|
channel_account_id="a1",
|
|
agent_binding="",
|
|
match_source=MatchSource.DEFAULT,
|
|
)
|
|
assert exc_info.value.field == "agent_binding"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMatchTier:
|
|
"""匹配层级 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
tier = MatchTier(name="explicit", priority=0, match_method="exact")
|
|
# Assert
|
|
assert tier.enabled is True
|
|
|
|
def test_empty_name_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MatchTier(name="", priority=0, match_method="exact")
|
|
assert exc_info.value.field == "name"
|
|
|
|
def test_negative_priority_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MatchTier(name="t", priority=-1, match_method="exact")
|
|
assert exc_info.value.field == "priority"
|
|
|
|
def test_invalid_match_method_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MatchTier(name="t", priority=0, match_method="invalid") # type: ignore[arg-type]
|
|
assert exc_info.value.field == "match_method"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestBindingContext:
|
|
"""绑定上下文 DTO 测试。"""
|
|
|
|
def test_valid_construction(self):
|
|
# Act
|
|
ctx = BindingContext(
|
|
session_key="session-key",
|
|
channel_type=ChannelType("wechat"),
|
|
account_id="acc-1",
|
|
chat_type="p2p",
|
|
peer_id="peer-1",
|
|
)
|
|
# Assert
|
|
assert ctx.conversation_id is None
|
|
assert ctx.unified_identity_id is None
|
|
|
|
def test_empty_session_key_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BindingContext(
|
|
session_key="",
|
|
channel_type=ChannelType("wechat"),
|
|
account_id="a1",
|
|
chat_type="p2p",
|
|
peer_id="p1",
|
|
)
|
|
assert exc_info.value.field == "session_key"
|
|
|
|
def test_empty_account_id_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BindingContext(
|
|
session_key="sk",
|
|
channel_type=ChannelType("wechat"),
|
|
account_id="",
|
|
chat_type="p2p",
|
|
peer_id="p1",
|
|
)
|
|
assert exc_info.value.field == "account_id"
|
|
|
|
def test_invalid_chat_type_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BindingContext(
|
|
session_key="sk",
|
|
channel_type=ChannelType("wechat"),
|
|
account_id="a1",
|
|
chat_type="invalid", # type: ignore[arg-type]
|
|
peer_id="p1",
|
|
)
|
|
assert exc_info.value.field == "chat_type"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestConfigFallbackChain:
|
|
"""配置回退链 DTO 测试。"""
|
|
|
|
def test_fields_are_assigned(self):
|
|
# Act
|
|
chain = ConfigFallbackChain(
|
|
match_key="wechat:*",
|
|
match_source=ConfigMatchSource.WILDCARD,
|
|
)
|
|
# Assert
|
|
assert chain.match_value is None
|
|
|
|
def test_empty_match_key_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConfigFallbackChain(
|
|
match_key="",
|
|
match_source=ConfigMatchSource.DIRECT,
|
|
)
|
|
assert exc_info.value.field == "match_key"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestNestedWhitelistDecision:
|
|
"""嵌套白名单决策 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
decision = NestedWhitelistDecision(
|
|
outer_configured=True, outer_matched=False
|
|
)
|
|
# Assert
|
|
assert decision.inner_decision is None
|