1. 删除了 test/unit/external_systems/framework/ 下的废弃空测试目录 2. 修复多处测试断言逻辑、参数传递与测试数据构造 3. 新增日志级别、缓存令牌、流事件等DTO单元测试 4. 补充路由绑定、会话仓储、outbox仓储的测试覆盖 5. 更新测试用例中的异常类型、参数校验与业务逻辑断言
236 lines
7.0 KiB
Python
236 lines
7.0 KiB
Python
"""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]
|