238 lines
8.5 KiB
Python
238 lines
8.5 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 会话状态流转测试
|
||
# =============================================================================
|
||
# 测试覆盖:
|
||
# 1. 会话创建默认状态为 queued
|
||
# 2. 坐席接单:queued → serving
|
||
# 3. 结单:serving → resolved
|
||
# 4. 重复接单处理
|
||
# 5. 已结单会话的操作限制
|
||
# 6. 置顶/取消置顶切换
|
||
# 7. 代办/取消代办切换
|
||
# 8. 会话列表过滤
|
||
# =============================================================================
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
import pytest
|
||
import pytest_asyncio
|
||
from httpx import ASGITransport, AsyncClient
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from unittest.mock import patch
|
||
|
||
from app.models.agent import Agent
|
||
from app.models.conversation import Conversation
|
||
from app.services.session_service import SessionService
|
||
from tests.conftest import create_test_conversation, create_test_agent, MockRedis
|
||
|
||
|
||
class TestConversationStateFlow:
|
||
"""测试会话状态流转。"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_new_conversation_default_status_queued(self, db_session):
|
||
"""验证新会话默认状态为 queued。"""
|
||
conv = create_test_conversation()
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
assert conv.status == "queued"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_assign_conversation_to_serving(self, db_session):
|
||
"""验证坐席接单将会话状态改为 serving。"""
|
||
conv = create_test_conversation(status="queued")
|
||
agent = create_test_agent(user_id="agent001", name="坐席小王")
|
||
db_session.add_all([conv, agent])
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.assign_agent(conv.id, "agent001")
|
||
|
||
assert result.status == "serving"
|
||
assert result.assigned_agent_id == "agent001"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_resolve_conversation(self, db_session):
|
||
"""验证结单将会话状态改为 resolved。"""
|
||
conv = create_test_conversation(status="serving")
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.resolve_conversation(conv.id)
|
||
|
||
assert result.status == "resolved"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_resolve_queued_conversation_is_allowed(self, db_session):
|
||
"""验证 queued 状态的会话可以直接结单(员工问题自行解决)。"""
|
||
conv = create_test_conversation(status="queued")
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
# queued → resolved 是合法的状态流转
|
||
result = await session_service.resolve_conversation(conv.id)
|
||
assert result.status == "resolved"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_cannot_resolve_already_resolved_conversation(self, db_session):
|
||
"""验证已结单的会话不能再结单。"""
|
||
conv = create_test_conversation(status="resolved")
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
from app.utils.response import AppException
|
||
with pytest.raises(AppException):
|
||
await session_service.resolve_conversation(conv.id)
|
||
|
||
|
||
class TestConversationToggle:
|
||
"""测试会话标记切换。"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_toggle_pin(self, db_session):
|
||
"""验证置顶切换:未置顶→置顶。"""
|
||
conv = create_test_conversation(is_pinned=False)
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.toggle_pin(conv.id)
|
||
assert result.is_pinned is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_toggle_pin_off(self, db_session):
|
||
"""验证置顶切换:置顶→取消置顶。"""
|
||
conv = create_test_conversation(is_pinned=True)
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.toggle_pin(conv.id)
|
||
assert result.is_pinned is False
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_toggle_todo(self, db_session):
|
||
"""验证代办切换:未代办→代办。"""
|
||
conv = create_test_conversation(is_todo=False)
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.toggle_todo(conv.id)
|
||
assert result.is_todo is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_toggle_todo_off(self, db_session):
|
||
"""验证代办切换:代办→取消代办。"""
|
||
conv = create_test_conversation(is_todo=True)
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result = await session_service.toggle_todo(conv.id)
|
||
assert result.is_todo is False
|
||
|
||
|
||
class TestConversationList:
|
||
"""测试会话列表查询。"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_all_conversations(self, db_session):
|
||
"""验证获取所有会话。"""
|
||
convs = [
|
||
create_test_conversation(employee_id=f"list_user_{i}", status="queued")
|
||
for i in range(3)
|
||
]
|
||
db_session.add_all(convs)
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result, total = await session_service.get_conversations()
|
||
|
||
assert total >= 3
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_conversations_by_status(self, db_session):
|
||
"""验证按状态过滤会话。"""
|
||
db_session.add(create_test_conversation(employee_id="filter_queued", status="queued"))
|
||
db_session.add(create_test_conversation(employee_id="filter_serving", status="serving"))
|
||
await db_session.flush()
|
||
|
||
session_service = SessionService(db_session)
|
||
result, total = await session_service.get_conversations(status="queued")
|
||
|
||
for conv in result:
|
||
assert conv.status == "queued"
|
||
|
||
|
||
class TestConversationAPI:
|
||
"""测试会话管理 API 端点。"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_conversations_endpoint(self, client, db_session, mock_redis):
|
||
"""验证 GET /api/conversations 返回正确格式。"""
|
||
conv = create_test_conversation(employee_id="api_list_user")
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
# 登录坐席获取 token(/api/conversations 需要 get_current_agent 认证)
|
||
login_resp = await client.post(
|
||
"/agents/login",
|
||
json={"user_id": "conv_list_agent", "name": "会话列表坐席"},
|
||
)
|
||
token = login_resp.json()["data"]["token"]
|
||
|
||
response = await client.get(
|
||
"/conversations",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["code"] == 0
|
||
assert "items" in data["data"]
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_resolve_conversation_endpoint(self, client, db_session, mock_redis):
|
||
"""验证 POST /api/conversations/{id}/resolve 结单。
|
||
|
||
权限:只有主责坐席(assigned_agent_id)才能结单。
|
||
"""
|
||
# 先创建坐席
|
||
from app.models.agent import Agent as AgentModel
|
||
agent = AgentModel(
|
||
user_id="resolve_test_agent",
|
||
name="结单测试坐席",
|
||
status="online",
|
||
current_load=0,
|
||
max_load=5,
|
||
)
|
||
db_session.add(agent)
|
||
await db_session.flush()
|
||
|
||
# 创建分配给此坐席的会话
|
||
conv = create_test_conversation(employee_id="api_resolve_user", status="serving")
|
||
conv.assigned_agent_id = "resolve_test_agent"
|
||
db_session.add(conv)
|
||
await db_session.flush()
|
||
|
||
# 登录坐席获取 token
|
||
login_resp = await client.post(
|
||
"/agents/login",
|
||
json={"user_id": "resolve_test_agent", "name": "结单测试坐席"},
|
||
)
|
||
token = login_resp.json()["data"]["token"]
|
||
|
||
response = await client.post(
|
||
f"/conversations/{conv.id}/resolve",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["code"] == 0
|
||
assert data["data"]["status"] == "resolved"
|