添加了channels限界上下文的大量单元测试文件,包括: 1. 各层级通用与专用的conftest夹具 2. 核心领域模型、事件、服务测试 3. 应用层流水线、扩展处理器测试 4. 适配器与插件层测试 5. 修复并补充了pool manager测试用例
404 lines
11 KiB
Python
404 lines
11 KiB
Python
"""yuxi.channels.adapters.mappers 单元测试。
|
|
|
|
覆盖 ORM ↔ dataclass 映射函数,包括 ``orm_to_channel_account`` /
|
|
``channel_account_for_write`` / ``orm_to_channel_session`` /
|
|
``orm_to_pairing`` / ``orm_to_audit_log`` / ``orm_to_outbox_entry`` /
|
|
``orm_to_user_identity`` / ``orm_to_channel_report``,使用 ``patch`` 替换
|
|
加解密函数,不依赖真实密钥配置。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters import mappers
|
|
from yuxi.channels.contract.dtos.audit import AuditEntry, AuditOperationType
|
|
from yuxi.channels.contract.dtos.channel import (
|
|
AccountStatus,
|
|
ChannelAccount,
|
|
ChannelSession,
|
|
ChannelType,
|
|
UserIdentity,
|
|
)
|
|
from yuxi.channels.contract.dtos.outbox import (
|
|
MessageDurabilityPolicy,
|
|
OutboxEntry,
|
|
OutboxStatus,
|
|
)
|
|
from yuxi.channels.contract.dtos.pairing import PairingRecord, PairingStatus
|
|
from yuxi.channels.contract.dtos.report import Report
|
|
from yuxi.channels.contract.errors import DependencyError
|
|
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _make_account_orm() -> MagicMock:
|
|
"""构造 ChannelAccount ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.channel_type = "feishu"
|
|
orm.account_id = "acc-1"
|
|
orm.display_name = "Account 1"
|
|
orm.config = {"token": "enc"}
|
|
orm.enabled = True
|
|
orm.status = "active"
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.updated_at = datetime(2026, 1, 1)
|
|
orm.transport_cursor = "cursor-1"
|
|
orm.last_rotated_at = None
|
|
return orm
|
|
|
|
|
|
def _make_session_orm() -> MagicMock:
|
|
"""构造 ChannelSession ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.session_id = "sess-1"
|
|
orm.channel_type = "feishu"
|
|
orm.account_id = 100
|
|
orm.peer_id = "peer-1"
|
|
orm.chat_type = "p2p"
|
|
orm.conversation_id = 200
|
|
orm.unified_identity_id = "uid-1"
|
|
orm.owner_peer_id = None
|
|
orm.is_temporary = False
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.updated_at = datetime(2026, 1, 1)
|
|
orm.deleted_at = None
|
|
orm.closed_at = None
|
|
orm.last_message_at = None
|
|
return orm
|
|
|
|
|
|
def _make_pairing_orm() -> MagicMock:
|
|
"""构造 ChannelPairing ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.pairing_id = "pair-1"
|
|
orm.account_id = 100
|
|
orm.peer_id = "peer-1"
|
|
orm.status = "pending"
|
|
orm.peer_name = "Peer One"
|
|
orm.approver_id = None
|
|
orm.approved_at = None
|
|
orm.rejected_at = None
|
|
orm.revoked_at = None
|
|
orm.reason = None
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.updated_at = datetime(2026, 1, 1)
|
|
orm.expires_at = None
|
|
orm.requested_at = None
|
|
orm.version = 1
|
|
return orm
|
|
|
|
|
|
def _make_audit_orm() -> MagicMock:
|
|
"""构造 ChannelAuditLog ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.operator = "admin-1"
|
|
orm.operation = "account_created"
|
|
orm.target_channel = "feishu"
|
|
orm.result = "success"
|
|
orm.timestamp = datetime(2026, 1, 1)
|
|
orm.params_summary = {"key": "value"}
|
|
orm.trace_id = "trace-1"
|
|
orm.source_ip = "127.0.0.1"
|
|
orm.request_id = "req-1"
|
|
orm.message_id = None
|
|
orm.content_summary = None
|
|
orm.target_account = "acc-1"
|
|
return orm
|
|
|
|
|
|
def _make_outbox_orm() -> MagicMock:
|
|
"""构造 ChannelOutboxEntry ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.outbox_id = 1
|
|
orm.message_id = 500
|
|
orm.account_id = 100
|
|
orm.status = "pending"
|
|
orm.durability_policy = "required"
|
|
orm.retry_count = 0
|
|
orm.max_retry = 3
|
|
orm.next_retry_at = None
|
|
orm.last_error = None
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.updated_at = datetime(2026, 1, 1)
|
|
orm.expires_at = None
|
|
orm.channel_msg_id = None
|
|
orm.version = 1
|
|
orm.channel_session_id = 200
|
|
orm.latency_ms = None
|
|
orm.funnel_node = None
|
|
return orm
|
|
|
|
|
|
def _make_identity_orm() -> MagicMock:
|
|
"""构造 ChannelUserIdentity ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.identity_id = "uid-1"
|
|
orm.user_id = 300
|
|
orm.identity_type = "email"
|
|
orm.identity_value = "user@example.com"
|
|
orm.channel_type = "feishu"
|
|
orm.channel_sender_id = "sender-1"
|
|
orm.source = "explicit_mapping"
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.updated_at = datetime(2026, 1, 1)
|
|
return orm
|
|
|
|
|
|
def _make_report_orm() -> MagicMock:
|
|
"""构造 ChannelReport ORM 桩。"""
|
|
orm = MagicMock()
|
|
orm.report_id = "rpt-1"
|
|
orm.task_id = "task-1"
|
|
orm.report_type = "message_stats"
|
|
orm.status = "ready"
|
|
orm.params = {"range": "7d"}
|
|
orm.content = {"data": 1}
|
|
orm.error_message = None
|
|
orm.created_at = datetime(2026, 1, 1)
|
|
orm.ready_at = datetime(2026, 1, 2)
|
|
orm.created_by = "user-1"
|
|
orm.retried_from = None
|
|
orm.retried_at = None
|
|
return orm
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToChannelAccount:
|
|
def test_returns_channel_account_with_decrypted_config(self):
|
|
# Arrange
|
|
orm = _make_account_orm()
|
|
|
|
# Act
|
|
with patch(
|
|
"yuxi.channels.adapters.mappers.decrypt_sensitive_fields",
|
|
return_value={"token": "plain"},
|
|
):
|
|
result = mappers.orm_to_channel_account(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, ChannelAccount)
|
|
assert result.account_id == "acc-1"
|
|
assert result.config == {"token": "plain"}
|
|
assert result.channel_type == ChannelType.FEISHU
|
|
assert result.status == AccountStatus.ACTIVE
|
|
|
|
def test_raises_dependency_error_when_decryption_fails(self):
|
|
# Arrange
|
|
orm = _make_account_orm()
|
|
|
|
# Act / Assert
|
|
with patch(
|
|
"yuxi.channels.adapters.mappers.decrypt_sensitive_fields",
|
|
side_effect=ValueError("bad key"),
|
|
):
|
|
with pytest.raises(DependencyError):
|
|
mappers.orm_to_channel_account(orm)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestChannelAccountForWrite:
|
|
def test_encrypts_config_field(self):
|
|
# Arrange
|
|
data = {"account_id": "acc-1", "config": {"token": "plain"}}
|
|
|
|
# Act
|
|
with patch(
|
|
"yuxi.channels.adapters.mappers.encrypt_sensitive_fields",
|
|
return_value={"token": "enc"},
|
|
) as enc_mock:
|
|
result = mappers.channel_account_for_write(data)
|
|
|
|
# Assert
|
|
assert result["config"] == {"token": "enc"}
|
|
enc_mock.assert_called_once_with({"token": "plain"})
|
|
|
|
def test_returns_new_dict_without_mutating_input(self):
|
|
# Arrange
|
|
data = {"account_id": "acc-1", "config": {"token": "plain"}}
|
|
|
|
# Act
|
|
with patch(
|
|
"yuxi.channels.adapters.mappers.encrypt_sensitive_fields",
|
|
return_value={"token": "enc"},
|
|
):
|
|
result = mappers.channel_account_for_write(data)
|
|
|
|
# Assert
|
|
assert data["config"] == {"token": "plain"}
|
|
assert result is not data
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToChannelSession:
|
|
def test_returns_channel_session_with_str_ids(self):
|
|
# Arrange
|
|
orm = _make_session_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_channel_session(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, ChannelSession)
|
|
assert result.session_id == "sess-1"
|
|
assert result.account_id == "100"
|
|
assert result.conversation_id == "200"
|
|
assert result.channel_type == ChannelType.FEISHU
|
|
|
|
def test_handles_none_conversation_id(self):
|
|
# Arrange
|
|
orm = _make_session_orm()
|
|
orm.conversation_id = None
|
|
|
|
# Act
|
|
result = mappers.orm_to_channel_session(orm)
|
|
|
|
# Assert
|
|
assert result.conversation_id is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToPairing:
|
|
def test_returns_pairing_record_without_account_orm(self):
|
|
# Arrange
|
|
orm = _make_pairing_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_pairing(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, PairingRecord)
|
|
assert result.pairing_id == "pair-1"
|
|
assert result.channel_account_id == "100"
|
|
assert result.channel_type is None
|
|
assert result.status == PairingStatus.PENDING
|
|
|
|
def test_returns_pairing_record_with_account_orm(self):
|
|
# Arrange
|
|
orm = _make_pairing_orm()
|
|
account_orm = _make_account_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_pairing(orm, account_orm)
|
|
|
|
# Assert
|
|
assert result.channel_account_id == "acc-1"
|
|
assert result.channel_type == ChannelType.FEISHU
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToAuditLog:
|
|
def test_returns_audit_entry(self):
|
|
# Arrange
|
|
orm = _make_audit_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_audit_log(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, AuditEntry)
|
|
assert result.operator == "admin-1"
|
|
assert result.operation == AuditOperationType.ACCOUNT_CREATE
|
|
assert result.target == "feishu"
|
|
assert result.target_channel == "feishu"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToOutboxEntry:
|
|
def test_returns_outbox_entry_with_str_ids(self):
|
|
# Arrange
|
|
orm = _make_outbox_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_outbox_entry(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, OutboxEntry)
|
|
assert result.outbox_id == 1
|
|
assert result.message_id == "500"
|
|
assert result.channel_account_id == "100"
|
|
assert result.channel_session_id == "200"
|
|
assert result.status == OutboxStatus.PENDING
|
|
assert result.durability_policy == MessageDurabilityPolicy.REQUIRED
|
|
|
|
def test_handles_none_channel_session_id(self):
|
|
# Arrange
|
|
orm = _make_outbox_orm()
|
|
orm.channel_session_id = None
|
|
|
|
# Act
|
|
result = mappers.orm_to_outbox_entry(orm)
|
|
|
|
# Assert
|
|
assert result.channel_session_id is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToUserIdentity:
|
|
def test_returns_user_identity_with_str_user_id(self):
|
|
# Arrange
|
|
orm = _make_identity_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_user_identity(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, UserIdentity)
|
|
assert result.identity_id == "uid-1"
|
|
assert result.user_id == "300"
|
|
assert result.channel_type == ChannelType.FEISHU
|
|
|
|
def test_handles_none_user_id(self):
|
|
# Arrange
|
|
orm = _make_identity_orm()
|
|
orm.user_id = None
|
|
|
|
# Act
|
|
result = mappers.orm_to_user_identity(orm)
|
|
|
|
# Assert
|
|
assert result.user_id is None
|
|
|
|
def test_handles_none_channel_type(self):
|
|
# Arrange
|
|
orm = _make_identity_orm()
|
|
orm.channel_type = None
|
|
|
|
# Act
|
|
result = mappers.orm_to_user_identity(orm)
|
|
|
|
# Assert
|
|
assert result.channel_type is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOrmToChannelReport:
|
|
def test_returns_report_dto(self):
|
|
# Arrange
|
|
orm = _make_report_orm()
|
|
|
|
# Act
|
|
result = mappers.orm_to_channel_report(orm)
|
|
|
|
# Assert
|
|
assert isinstance(result, Report)
|
|
assert result.report_id == "rpt-1"
|
|
assert result.task_id == "task-1"
|
|
assert result.status == "ready"
|
|
assert result.content == {"data": 1}
|
|
|
|
def test_handles_none_params(self):
|
|
# Arrange
|
|
orm = _make_report_orm()
|
|
orm.params = None
|
|
|
|
# Act
|
|
result = mappers.orm_to_channel_report(orm)
|
|
|
|
# Assert
|
|
assert result.params == {}
|