"""pairing.py DTO 单元测试。 覆盖 ``PairingId`` / ``PairingStatus`` / ``DmDecision`` / ``DmPolicy`` / ``PairingRecord`` / ``PairingQuery`` / ``BotLoopBudget`` / ``CreatePairingResult`` / ``BatchPairingCmd`` / ``CleanExpiredPairingsCmd`` / ``PairingStatsQuery`` / ``PairingTrendPoint`` / ``PairingStatsResult`` 的 字段赋值、默认值、不可变语义与 ``__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.common import Operator, OperatorRole from yuxi.channels.contract.dtos.pairing import ( BatchPairingCmd, BotLoopBudget, CleanExpiredPairingsCmd, CreatePairingResult, DmDecision, DmPolicy, PairingId, PairingQuery, PairingRecord, PairingStatsQuery, PairingStatsResult, PairingStatus, PairingTrendPoint, ) 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"