"""inbound.py DTO 单元测试。 覆盖 ``InboundMessageCmd`` / ``InboundResult`` / ``ReceiveInboundCmd`` / ``SignatureVerifyResult`` 的字段赋值、默认值、不可变语义与 ``__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.command import CommandResponse from yuxi.channels.contract.dtos.common import RawEvent from yuxi.channels.contract.dtos.inbound import ( InboundMessageCmd, InboundResult, ReceiveInboundCmd, SignatureVerifyResult, ) from yuxi.channels.contract.errors import ValidationError pytestmark = pytest.mark.unit def _make_raw_event() -> RawEvent: return RawEvent( source="webhook", payload={"msg": "hello"}, headers={"x-signature": "abc"}, received_at=datetime(2026, 1, 1, 12, 0, 0), ) @pytest.mark.unit class TestInboundMessageCmd: """入站消息命令 DTO 测试。""" def test_fields_are_assigned(self): # Arrange raw_event = _make_raw_event() # Act cmd = InboundMessageCmd( channel_type=ChannelType("feishu"), account_id="acc-1", raw_event=raw_event, ) # Assert assert cmd.channel_type == ChannelType("feishu") assert cmd.account_id == "acc-1" assert cmd.raw_event is raw_event assert cmd.trace_id is None def test_with_trace_id(self): # Arrange raw_event = _make_raw_event() # Act cmd = InboundMessageCmd( channel_type=ChannelType("feishu"), account_id="acc-1", raw_event=raw_event, trace_id="trace-1", ) # Assert assert cmd.trace_id == "trace-1" def test_is_frozen(self): # Arrange raw_event = _make_raw_event() cmd = InboundMessageCmd( channel_type=ChannelType("feishu"), account_id="acc-1", raw_event=raw_event, ) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): cmd.account_id = "other" # type: ignore[misc] def test_empty_account_id_raises_validation_error(self): # Arrange raw_event = _make_raw_event() # Act / Assert with pytest.raises(ValidationError) as exc_info: InboundMessageCmd( channel_type=ChannelType("feishu"), account_id="", raw_event=raw_event, ) assert exc_info.value.field == "account_id" @pytest.mark.unit class TestInboundResult: """入站结果 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange # Act result = InboundResult(ack_decision="ack") # Assert assert result.ack_decision == "ack" def test_defaults_are_correct(self): # Arrange # Act result = InboundResult(ack_decision="ack") # Assert assert result.agent_run_id is None assert result.is_silent is False assert result.error is None assert result.command_response is None assert result.error_code == "" assert result.trace_id == "" assert result.raw_response is None def test_nack_with_error(self): # Arrange # Act result = InboundResult( ack_decision="nack", error="processing failed", error_code="INTERNAL", trace_id="trace-1", ) # Assert assert result.ack_decision == "nack" assert result.error == "processing failed" assert result.error_code == "INTERNAL" def test_with_command_response(self): # Arrange resp = CommandResponse(content="done") # Act result = InboundResult(ack_decision="ack", command_response=resp) # Assert assert result.command_response is resp def test_with_raw_response(self): # Arrange # Act result = InboundResult( ack_decision="ack", raw_response={"challenge": "xyz"}, ) # Assert assert result.raw_response == {"challenge": "xyz"} def test_is_frozen(self): # Arrange result = InboundResult(ack_decision="ack") # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): result.ack_decision = "nack" # type: ignore[misc] @pytest.mark.unit class TestReceiveInboundCmd: """Webhook 入站接收命令 DTO 测试。""" def test_required_fields_are_assigned(self): # Arrange # Act cmd = ReceiveInboundCmd( channel_type=ChannelType("feishu"), raw_event='{"msg":"hello"}', ) # Assert assert cmd.channel_type == ChannelType("feishu") assert cmd.raw_event == '{"msg":"hello"}' assert cmd.headers == {} assert cmd.trace_id is None def test_with_headers_and_trace_id(self): # Arrange # Act cmd = ReceiveInboundCmd( channel_type=ChannelType("feishu"), raw_event='{"msg":"hello"}', headers={"x-signature": "abc"}, trace_id="trace-1", ) # Assert assert cmd.headers == {"x-signature": "abc"} assert cmd.trace_id == "trace-1" def test_is_frozen(self): # Arrange cmd = ReceiveInboundCmd(channel_type=ChannelType("feishu"), raw_event="{}") # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): cmd.raw_event = "other" # type: ignore[misc] def test_none_channel_type_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ReceiveInboundCmd(channel_type=None, raw_event="{}") # type: ignore[arg-type] assert exc_info.value.field == "channel_type" def test_empty_raw_event_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: ReceiveInboundCmd(channel_type=ChannelType("feishu"), raw_event="") assert exc_info.value.field == "raw_event" @pytest.mark.unit class TestSignatureVerifyResult: """Webhook 签名校验结果 DTO 测试。""" def test_valid_result(self): # Arrange # Act result = SignatureVerifyResult(valid=True) # Assert assert result.valid is True assert result.reason is None def test_invalid_result_with_reason(self): # Arrange # Act result = SignatureVerifyResult(valid=False, reason="signature mismatch") # Assert assert result.valid is False assert result.reason == "signature mismatch" def test_is_frozen(self): # Arrange result = SignatureVerifyResult(valid=True) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): result.valid = False # type: ignore[misc]