1. 清理测试文件中未使用的导入与冗余代码 2. 修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道 3. 调整批量消息撤回的响应语义,对齐其他端点的部分成功契约 4. 修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑 5. 替换anyio为asyncio并行调用,修正时间UTC导入路径 6. 优化前端外部系统概览页的刷新状态提示与缓存逻辑 7. 修复测试用例中的断言与请求方式问题,适配httpx删除请求特性 8. 重构后端路由的依赖注入,移除冗余的数据库会话依赖 9. 调整测试用例的权限校验逻辑,修正强制登出的权限判断 10. 修复语义分块测试的numpy依赖问题,清理冗余导入
218 lines
6.6 KiB
Python
218 lines
6.6 KiB
Python
"""route.py DTO 单元测试。
|
|
|
|
覆盖 ``MatchSource`` / ``ConfigMatchSource`` / ``RouteBinding`` /
|
|
``MatchTier`` / ``BindingContext`` / ``ConfigFallbackChain`` /
|
|
``NestedWhitelistDecision`` / ``RouteBindingRule`` / ``SaveRouteBindingCmd`` /
|
|
``UpdateRouteBindingCmd`` / ``RouteBindingFilter`` 的字段赋值、默认值、
|
|
不可变语义与 ``__post_init__`` 校验逻辑。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
from yuxi.channels.contract.dtos.route import (
|
|
BindingContext,
|
|
ConfigFallbackChain,
|
|
ConfigMatchSource,
|
|
MatchSource,
|
|
MatchTier,
|
|
NestedWhitelistDecision,
|
|
RouteBinding,
|
|
)
|
|
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
|