Files
wecom_it_smart_desk/backend/tests/test_invite_status.py
T

496 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 会话邀请参与者状态校验 — 单元测试
# =============================================================================
# 测试覆盖:
# 1. ai_handling 状态 → 邀请成功(本次修复核心)
# 2. serving 状态 → 邀请成功(原有功能不回归)
# 3. resolved 状态 → 邀请失败
# 4. queued 状态 → 邀请失败
# 5. pending_close 状态 → 邀请失败
# 6. 权限校验:非参与者邀请被拒绝(3030 错误码)
# 7. 重复邀请校验(3032 错误码)
#
# 设计原则:
# - mock SessionService 的内部辅助方法和 DB,聚焦 invite_participants 业务逻辑
# - 参考 backend/tests/test_org_tree.py 的测试风格
# =============================================================================
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.services.session_service import SessionService
from app.utils.response import AppException
# =============================================================================
# 辅助:构造 mock 会话对象与 SessionService 实例
# =============================================================================
def _mock_conversation(
status: str = "serving",
assigned_agent_id: str = "agent_001",
employee_id: str = "emp_001",
participants: list | None = None,
conv_id: str = "conv-001",
):
"""构造一个模拟会话对象,用于测试 invite_participants。
Args:
status: 会话状态
assigned_agent_id: 主责坐席ID
employee_id: 员工ID(会话发起人)
participants: 已有参与者列表
conv_id: 会话ID
"""
conv = MagicMock()
conv.id = conv_id
conv.status = status
conv.assigned_agent_id = assigned_agent_id
conv.employee_id = employee_id
conv.participants = participants if participants is not None else []
conv.updated_at = MagicMock()
return conv
def _new_participants():
"""构造被邀请人列表(1 个新员工)。"""
return [
{"id": "emp_002", "name": "张三", "department": "技术部", "type": "employee"},
]
def _make_service():
"""构造 SessionService 实例,所有外部依赖已 mock。
返回的 service 对象已 mock 以下方法/属性:
- _get_conversation: AsyncMock(需在测试中设置 return_value
- _get_employee_avatar: AsyncMock,返回 None
- _create_system_message: AsyncMock
- _broadcast_participant_change: AsyncMock
- db: MagicMockadd/flush 已 mock
- wecom_service: None(跳过通知发送)
"""
db = MagicMock()
db.add = MagicMock()
db.flush = AsyncMock()
service = SessionService(db=db, wecom_service=None)
service._get_conversation = AsyncMock()
service._get_employee_avatar = AsyncMock(return_value=None)
service._create_system_message = AsyncMock()
service._broadcast_participant_change = AsyncMock()
return service
# =============================================================================
# 一、状态校验:各状态下的邀请行为
# =============================================================================
class TestInviteStatusCheck:
"""invite_participants 状态校验测试。
修复前:仅 serving 状态允许邀请,ai_handling 被错误拒绝。
修复后:serving 和 ai_handling 均允许邀请。
"""
# ----- 1.1 ai_handling 状态 → 邀请成功(本次修复核心) -----
@pytest.mark.asyncio
async def test_ai_handling_status_invite_succeeds(self):
"""ai_handling 状态下邀请参与者应成功(本次修复核心)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="ai_handling")
service._get_conversation.return_value = conv
# Act
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
# Assert
assert result is conv
# 参与者列表已更新,包含新邀请人
assert len(conv.participants) == 1
assert conv.participants[0]["id"] == "emp_002"
# DB 写入被调用
service.db.add.assert_called_once_with(conv)
service.db.flush.assert_awaited_once()
# ----- 1.2 serving 状态 → 邀请成功(原有功能不回归) -----
@pytest.mark.asyncio
async def test_serving_status_invite_succeeds(self):
"""serving 状态下邀请参与者应成功(原有功能不回归)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="serving")
service._get_conversation.return_value = conv
# Act
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
# Assert
assert result is conv
assert len(conv.participants) == 1
assert conv.participants[0]["id"] == "emp_002"
service.db.add.assert_called_once_with(conv)
service.db.flush.assert_awaited_once()
# ----- 1.3 resolved 状态 → 邀请失败 -----
@pytest.mark.asyncio
async def test_resolved_status_invite_fails(self):
"""resolved 状态下邀请参与者应失败(已结单不应允许邀请)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="resolved")
service._get_conversation.return_value = conv
# Act & Assert
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
assert exc_info.value.code == 3031
# 不应写 DB
service.db.add.assert_not_called()
service.db.flush.assert_not_awaited()
# ----- 1.4 queued 状态 → 邀请失败 -----
@pytest.mark.asyncio
async def test_queued_status_invite_fails(self):
"""queued 状态下邀请参与者应失败(排队中不应允许邀请)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="queued")
service._get_conversation.return_value = conv
# Act & Assert
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
assert exc_info.value.code == 3031
service.db.add.assert_not_called()
service.db.flush.assert_not_awaited()
# ----- 1.5 pending_close 状态 → 邀请失败 -----
@pytest.mark.asyncio
async def test_pending_close_status_invite_fails(self):
"""pending_close 状态下邀请参与者应失败(待关单不应允许邀请)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="pending_close")
service._get_conversation.return_value = conv
# Act & Assert
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
assert exc_info.value.code == 3031
service.db.add.assert_not_called()
service.db.flush.assert_not_awaited()
# ----- 1.6 错误消息包含当前状态信息 -----
@pytest.mark.asyncio
async def test_error_message_contains_current_status(self):
"""状态校验失败时,错误消息应包含当前状态值。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="resolved")
service._get_conversation.return_value = conv
# Act & Assert
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
assert "resolved" in exc_info.value.message
# =============================================================================
# 二、权限校验:非参与者邀请被拒绝
# =============================================================================
class TestInvitePermissionCheck:
"""invite_participants 权限校验测试。
权限规则:只有主责坐席、会话发起人、或已在 participants 中的参与者可以邀请。
"""
# ----- 2.1 非参与者邀请 → 被拒绝(3030) -----
@pytest.mark.asyncio
async def test_non_participant_invite_rejected(self):
"""非主责坐席、非发起人、非参与者的用户邀请应被拒绝(3030)。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="serving",
assigned_agent_id="agent_001",
employee_id="emp_001",
participants=[], # 没有已有参与者
)
service._get_conversation.return_value = conv
# Act & Assert — inviter 不是 agent_001 也不是 emp_001
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="outsider_999",
participants=_new_participants(),
)
assert exc_info.value.code == 3030
service.db.add.assert_not_called()
# ----- 2.2 主责坐席邀请 → 通过权限校验 -----
@pytest.mark.asyncio
async def test_primary_agent_can_invite(self):
"""主责坐席可以发起邀请。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="serving",
assigned_agent_id="agent_001",
employee_id="emp_001",
)
service._get_conversation.return_value = conv
# Act
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
# Assert
assert result is conv
assert len(conv.participants) == 1
# ----- 2.3 会话发起人邀请 → 通过权限校验 -----
@pytest.mark.asyncio
async def test_creator_can_invite(self):
"""会话发起人(employee_id)可以发起邀请。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="ai_handling",
assigned_agent_id="agent_001",
employee_id="emp_001",
)
service._get_conversation.return_value = conv
# Act
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="emp_001",
participants=_new_participants(),
)
# Assert
assert result is conv
assert len(conv.participants) == 1
# ----- 2.4 已有参与者邀请 → 通过权限校验 -----
@pytest.mark.asyncio
async def test_existing_participant_can_invite(self):
"""已在 participants 列表中的参与者可以邀请其他人。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="serving",
assigned_agent_id="agent_001",
employee_id="emp_001",
participants=[
{"id": "emp_005", "name": "王五", "type": "employee"},
],
)
service._get_conversation.return_value = conv
# Act — emp_005 是已有参与者,邀请新的人
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="emp_005",
participants=_new_participants(),
)
# Assert
assert result is conv
# 参与者列表包含原有的 + 新邀请的
assert len(conv.participants) == 2
participant_ids = {p["id"] for p in conv.participants}
assert "emp_005" in participant_ids
assert "emp_002" in participant_ids
# =============================================================================
# 三、重复邀请校验
# =============================================================================
class TestInviteDuplicateCheck:
"""invite_participants 重复邀请校验测试。"""
# ----- 3.1 所有被邀请人已在会话中 → 3032 -----
@pytest.mark.asyncio
async def test_all_invitees_already_in_conversation(self):
"""所有被邀请人已在会话中时,应抛出 3032。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="serving",
participants=[
{"id": "emp_002", "name": "张三", "type": "employee"},
],
)
service._get_conversation.return_value = conv
# Act & Assert — 邀请已在会话中的 emp_002
with pytest.raises(AppException) as exc_info:
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=[
{"id": "emp_002", "name": "张三", "type": "employee"},
],
)
assert exc_info.value.code == 3032
# ----- 3.2 部分新部分旧 → 新的被加入 -----
@pytest.mark.asyncio
async def test_partial_new_invitees_added(self):
"""部分被邀请人已存在时,只添加新的人。"""
# Arrange
service = _make_service()
conv = _mock_conversation(
status="ai_handling",
participants=[
{"id": "emp_002", "name": "张三", "type": "employee"},
],
)
service._get_conversation.return_value = conv
# Act — 邀请 emp_002(已有)和 emp_003(新)
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=[
{"id": "emp_002", "name": "张三", "type": "employee"},
{"id": "emp_003", "name": "李四", "type": "employee"},
],
)
# Assert
assert result is conv
assert len(conv.participants) == 2
participant_ids = {p["id"] for p in conv.participants}
assert "emp_002" in participant_ids
assert "emp_003" in participant_ids
# =============================================================================
# 四、成功邀请的副作用验证
# =============================================================================
class TestInviteSideEffects:
"""invite_participants 成功时的副作用验证。"""
# ----- 4.1 创建系统消息广播 -----
@pytest.mark.asyncio
async def test_system_message_created_on_success(self):
"""成功邀请后应创建系统消息广播。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="ai_handling")
service._get_conversation.return_value = conv
# Act
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
# Assert
service._create_system_message.assert_awaited_once()
call_kwargs = service._create_system_message.call_args.kwargs
assert call_kwargs["conversation_id"] == "conv-001"
assert "张三" in call_kwargs["content"]
# ----- 4.2 WebSocket 广播参与者变更 -----
@pytest.mark.asyncio
async def test_broadcast_participant_change_on_success(self):
"""成功邀请后应广播参与者变更通知。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="serving")
service._get_conversation.return_value = conv
# Act
await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=_new_participants(),
)
# Assert
service._broadcast_participant_change.assert_awaited_once()
call_args = service._broadcast_participant_change.call_args
assert call_args.args[0] is conv # 第一个参数是 conversation
assert call_args.args[1] == "participant_invited" # 事件类型
# ----- 4.3 多人邀请全部添加 -----
@pytest.mark.asyncio
async def test_multiple_invitees_all_added(self):
"""一次邀请多人时,所有人都应被添加。"""
# Arrange
service = _make_service()
conv = _mock_conversation(status="ai_handling", participants=[])
service._get_conversation.return_value = conv
participants = [
{"id": "emp_010", "name": "赵六", "type": "employee"},
{"id": "emp_011", "name": "孙七", "type": "employee"},
{"id": "emp_012", "name": "周八", "type": "employee"},
]
# Act
result = await service.invite_participants(
conversation_id="conv-001",
inviter_agent_id="agent_001",
participants=participants,
)
# Assert
assert result is conv
assert len(conv.participants) == 3
added_ids = {p["id"] for p in conv.participants}
assert added_ids == {"emp_010", "emp_011", "emp_012"}