1. 清理测试文件中未使用的导入与冗余代码 2. 修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道 3. 调整批量消息撤回的响应语义,对齐其他端点的部分成功契约 4. 修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑 5. 替换anyio为asyncio并行调用,修正时间UTC导入路径 6. 优化前端外部系统概览页的刷新状态提示与缓存逻辑 7. 修复测试用例中的断言与请求方式问题,适配httpx删除请求特性 8. 重构后端路由的依赖注入,移除冗余的数据库会话依赖 9. 调整测试用例的权限校验逻辑,修正强制登出的权限判断 10. 修复语义分块测试的numpy依赖问题,清理冗余导入
214 lines
6.0 KiB
Python
214 lines
6.0 KiB
Python
"""pairing.py DTO 单元测试。
|
|
|
|
覆盖 ``PairingId`` / ``PairingStatus`` / ``DmDecision`` / ``DmPolicy`` /
|
|
``PairingRecord`` / ``PairingQuery`` / ``BotLoopBudget`` /
|
|
``CreatePairingResult`` / ``BatchPairingCmd`` / ``CleanExpiredPairingsCmd`` /
|
|
``PairingStatsQuery`` / ``PairingTrendPoint`` / ``PairingStatsResult`` 的
|
|
字段赋值、默认值、不可变语义与 ``__post_init__`` 校验逻辑。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
from yuxi.channels.contract.dtos.common import Operator, OperatorRole
|
|
from yuxi.channels.contract.dtos.pairing import (
|
|
BatchPairingCmd,
|
|
BotLoopBudget,
|
|
CleanExpiredPairingsCmd,
|
|
DmDecision,
|
|
DmPolicy,
|
|
PairingId,
|
|
PairingQuery,
|
|
PairingRecord,
|
|
PairingStatsQuery,
|
|
PairingStatus,
|
|
)
|
|
from yuxi.channels.contract.errors import ValidationError
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _make_operator() -> Operator:
|
|
return Operator(user_id="admin-1", role=OperatorRole.ADMIN_USER)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPairingId:
|
|
"""配对 ID DTO 测试。"""
|
|
|
|
def test_value_is_assigned(self):
|
|
# Act
|
|
pid = PairingId(value="pair-001")
|
|
# Assert
|
|
assert pid.value == "pair-001"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPairingStatus:
|
|
"""配对状态枚举测试。"""
|
|
|
|
def test_str_values(self):
|
|
assert PairingStatus.PENDING == "pending"
|
|
assert PairingStatus.APPROVED == "approved"
|
|
assert PairingStatus.REJECTED == "rejected"
|
|
assert PairingStatus.EXPIRED == "expired"
|
|
assert PairingStatus.REVOKED == "revoked"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDmDecision:
|
|
"""DM 决策枚举测试。"""
|
|
|
|
def test_str_values(self):
|
|
assert DmDecision.ALLOW == "allow"
|
|
assert DmDecision.DENY == "deny"
|
|
assert DmDecision.PENDING_PAIRING == "pending_pairing"
|
|
assert DmDecision.WHITELIST == "whitelist"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDmPolicy:
|
|
"""DM 策略枚举测试。"""
|
|
|
|
def test_str_values(self):
|
|
assert DmPolicy.ALLOW == "allow"
|
|
assert DmPolicy.DENY == "deny"
|
|
assert DmPolicy.PAIRING_REQUIRED == "pairing_required"
|
|
assert DmPolicy.WHITELIST == "whitelist"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPairingRecord:
|
|
"""配对记录 DTO 测试。"""
|
|
|
|
def test_fields_are_assigned_with_defaults(self):
|
|
# Act
|
|
record = PairingRecord(
|
|
pairing_id="pair-1",
|
|
channel_account_id="acc-1",
|
|
peer_id="peer-1",
|
|
status=PairingStatus.PENDING,
|
|
)
|
|
# Assert
|
|
assert record.channel_type is None
|
|
assert record.approver_id is None
|
|
assert record.version == 1
|
|
|
|
def test_is_frozen(self):
|
|
# Arrange
|
|
record = PairingRecord(
|
|
pairing_id="p1",
|
|
channel_account_id="a1",
|
|
peer_id="peer",
|
|
status=PairingStatus.PENDING,
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
record.status = PairingStatus.APPROVED # type: ignore[misc]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPairingQuery:
|
|
"""配对查询 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
query = PairingQuery()
|
|
# Assert
|
|
assert query.limit == 100
|
|
assert query.offset == 0
|
|
assert query.status is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestBotLoopBudget:
|
|
"""机器人循环预算 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
budget = BotLoopBudget(max_replies_per_hour=100, cooldown_seconds=60)
|
|
# Assert
|
|
assert budget.current_count == 0
|
|
assert budget.last_reply_at is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestBatchPairingCmd:
|
|
"""批量配对命令 DTO 测试。"""
|
|
|
|
def test_valid_construction(self):
|
|
# Act
|
|
cmd = BatchPairingCmd(
|
|
pairing_ids=("pair-1", "pair-2"),
|
|
operator=_make_operator(),
|
|
)
|
|
# Assert
|
|
assert cmd.reason is None
|
|
|
|
def test_empty_pairing_ids_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BatchPairingCmd(pairing_ids=(), operator=_make_operator())
|
|
assert exc_info.value.field == "pairing_ids"
|
|
|
|
def test_too_many_pairing_ids_raises(self):
|
|
# Arrange
|
|
ids = tuple(f"pair-{i}" for i in range(501))
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BatchPairingCmd(pairing_ids=ids, operator=_make_operator())
|
|
assert exc_info.value.field == "pairing_ids"
|
|
|
|
def test_reason_too_long_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
BatchPairingCmd(
|
|
pairing_ids=("pair-1",),
|
|
operator=_make_operator(),
|
|
reason="x" * 513,
|
|
)
|
|
assert exc_info.value.field == "reason"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCleanExpiredPairingsCmd:
|
|
"""清理过期配对命令 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
cmd = CleanExpiredPairingsCmd(operator=_make_operator())
|
|
# Assert
|
|
assert cmd.max_count == 500
|
|
assert cmd.channel_type is None
|
|
|
|
def test_max_count_below_min_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CleanExpiredPairingsCmd(operator=_make_operator(), max_count=0)
|
|
assert exc_info.value.field == "max_count"
|
|
|
|
def test_max_count_above_max_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CleanExpiredPairingsCmd(operator=_make_operator(), max_count=1001)
|
|
assert exc_info.value.field == "max_count"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPairingStatsQuery:
|
|
"""配对统计查询 DTO 测试。"""
|
|
|
|
def test_defaults(self):
|
|
# Act
|
|
query = PairingStatsQuery()
|
|
# Assert
|
|
assert query.granularity == "day"
|
|
|
|
def test_invalid_granularity_raises(self):
|
|
# Act / Assert
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PairingStatsQuery(granularity="minute")
|
|
assert exc_info.value.field == "granularity"
|