Files
wecom_it_smart_desk/backend/tests/test_evaluation.py
T

773 lines
24 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 满意度评价功能测试 (P1-25)
# =============================================================================
# 测试范围:
# 1. POST /conversation/{conversation_id}/evaluate - 提交评价
# 2. GET /conversation/{conversation_id}/evaluation - 获取会话评价
# 3. GET /api/evaluations/stats - 获取评价统计
# 4. POST /conversations/{conversation_id}/send-evaluation-invite - 发送评价邀请
# =============================================================================
import pytest
import pytest_asyncio
from datetime import datetime
from unittest.mock import patch, AsyncMock
from sqlalchemy import select
from app.models.conversation import Conversation
from app.models.employee import Employee
from app.models.agent import Agent
from app.models.role import Role
from app.models.user_role import UserRole
from app.models.conversation_evaluation import ConversationEvaluation
def create_test_employee(
db_session,
employee_id: str = "test_employee_001",
name: str = "测试员工",
corp_id: str = "test_corp_id",
):
"""创建测试员工"""
employee = Employee(
employee_id=employee_id,
name=name,
corp_id=corp_id,
department="技术部",
position="工程师",
status=1,
)
db_session.add(employee)
return employee
# =============================================================================
# 测试用例:提交评价 (POST /conversation/{conversation_id}/evaluate)
# =============================================================================
@pytest.mark.asyncio
async def test_submit_evaluation_success(client, db_session, mock_redis):
"""测试成功提交满意度评价"""
# 1. 创建测试会话(状态为 resolved)
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 2. 创建员工记录
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
# 3. 模拟 H5 员工登录 - 使用 Redis Token
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 5,
"emoji": "satisfied",
"feedback_text": "服务态度很好!",
},
)
# 4. 验证响应
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"]["star_rating"] == 5
assert data["data"]["emoji"] == "satisfied"
assert data["data"]["feedback_text"] == "服务态度很好!"
assert data["data"]["employee_id"] == "test_employee_001"
assert data["data"]["conversation_id"] == conversation.id
@pytest.mark.asyncio
async def test_submit_evaluation_invalid_star_rating(client, db_session, mock_redis):
"""测试提交评价时星级超出范围(1-5"""
# 创建测试会话
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
# 提交星级为 0(超出范围)
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 0, # 无效:小于1
"emoji": "satisfied",
},
)
# 应该返回 422 验证错误
assert response.status_code == 422
@pytest.mark.asyncio
async def test_submit_evaluation_conversation_not_resolved(client, db_session, mock_redis):
"""测试只能评价已结单的会话"""
# 创建未结单的会话
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="serving", # 未结单
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 5,
"emoji": "satisfied",
},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3040 # 只能评价已结单的会话
@pytest.mark.asyncio
async def test_submit_evaluation_duplicate(client, db_session, mock_redis):
"""测试重复评价(防止重复提交)"""
# 创建测试会话
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 创建员工
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
# 创建已有评价
evaluation = ConversationEvaluation(
conversation_id=conversation.id,
employee_id="test_employee_001",
employee_name="测试员工",
star_rating=5,
emoji="satisfied",
)
db_session.add(evaluation)
await db_session.flush()
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
# 再次提交评价
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 4,
"emoji": "neutral",
},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3041 # 您已对该会话提交过评价
@pytest.mark.asyncio
async def test_submit_evaluation_conversation_not_found(client, db_session, mock_redis):
"""测试评价不存在的会话"""
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
response = await client.post(
"/conversation/nonexistent-id/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 5,
"emoji": "satisfied",
},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3003 # 会话不存在
@pytest.mark.asyncio
async def test_submit_evaluation_all_emoji_options(client, db_session, mock_redis):
"""测试所有表情选项"""
# Test satisfied
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 5,
"emoji": "satisfied",
},
)
assert response.status_code == 200
assert response.json()["data"]["emoji"] == "satisfied"
# Create new conversation to test neutral
conversation2 = Conversation(
employee_id="test_employee_002",
employee_name="测试员工2",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation2)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_002")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_002", 28800, "test_employee_002")
response = await client.post(
f"/conversation/{conversation2.id}/evaluate",
headers={"Authorization": "Bearer test_token_002"},
json={
"star_rating": 3,
"emoji": "neutral",
},
)
assert response.status_code == 200
assert response.json()["data"]["emoji"] == "neutral"
# Create new conversation to test dissatisfied
conversation3 = Conversation(
employee_id="test_employee_003",
employee_name="测试员工3",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation3)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_003")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_003", 28800, "test_employee_003")
response = await client.post(
f"/conversation/{conversation3.id}/evaluate",
headers={"Authorization": "Bearer test_token_003"},
json={
"star_rating": 1,
"emoji": "dissatisfied",
},
)
assert response.status_code == 200
assert response.json()["data"]["emoji"] == "dissatisfied"
@pytest.mark.asyncio
async def test_submit_evaluation_feedback_text_optional(client, db_session, mock_redis):
"""测试文字反馈为可选字段"""
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
create_test_employee(db_session, employee_id="test_employee_001")
await db_session.flush()
await mock_redis.setex("employee:token:test_token_001", 28800, "test_employee_001")
# 不提供 feedback_text
response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_001"},
json={
"star_rating": 4,
"emoji": "satisfied",
},
)
assert response.status_code == 200
data = response.json()
assert data["data"]["feedback_text"] is None
# =============================================================================
# 测试用例:获取会话评价 (GET /conversation/{conversation_id}/evaluation)
# =============================================================================
@pytest.mark.asyncio
async def test_get_evaluation_success(client, db_session):
"""测试获取会话评价 - 存在评价"""
# 创建会话和评价
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
evaluation = ConversationEvaluation(
conversation_id=conversation.id,
employee_id="test_employee_001",
employee_name="测试员工",
star_rating=5,
emoji="satisfied",
feedback_text="很好!",
)
db_session.add(evaluation)
await db_session.flush()
# 获取评价
response = await client.get(f"/conversation/{conversation.id}/evaluation")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"]["star_rating"] == 5
assert data["data"]["emoji"] == "satisfied"
assert data["data"]["feedback_text"] == "很好!"
@pytest.mark.asyncio
async def test_get_evaluation_not_found(client, db_session):
"""测试获取会话评价 - 不存在"""
# 创建会话但没有评价
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 获取评价
response = await client.get(f"/conversation/{conversation.id}/evaluation")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"] is None # 没有评价时返回 null
# =============================================================================
# 测试用例:获取评价统计 (GET /api/evaluations/stats)
# =============================================================================
@pytest.mark.asyncio
async def test_get_evaluation_stats_success(client, db_session):
"""测试获取评价统计 - 有数据"""
# 创建多个会话和评价
for i in range(5):
conv = Conversation(
employee_id=f"test_employee_{i:03d}",
employee_name=f"测试员工{i}",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conv)
await db_session.flush()
# 3个5星满意, 1个3星一般, 1个1星不满意
if i < 3:
star, emoji = 5, "satisfied"
elif i == 3:
star, emoji = 3, "neutral"
else:
star, emoji = 1, "dissatisfied"
eval = ConversationEvaluation(
conversation_id=conv.id,
employee_id=f"test_employee_{i:03d}",
employee_name=f"测试员工{i}",
star_rating=star,
emoji=emoji,
)
db_session.add(eval)
await db_session.flush()
# 需要管理员权限才能访问统计接口
# 先创建管理员角色
role_stmt = select(Role).where(Role.name == "admin")
role_result = await db_session.execute(role_stmt)
admin_role = role_result.scalars().first()
if not admin_role:
admin_role = Role(
name="admin",
display_name="管理员",
description="管理员角色",
permissions=["evaluation:read:all"],
)
db_session.add(admin_role)
await db_session.flush()
# 为测试坐席添加管理员角色
agent = Agent(
user_id="test_agent_admin",
name="测试管理员",
status="online",
)
db_session.add(agent)
await db_session.flush()
user_role = UserRole(
employee_id="test_agent_admin",
role_id=admin_role.id,
source="manual",
assigned_by="test",
)
db_session.add(user_role)
await db_session.flush()
# 登录获取 token
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_admin",
"name": "测试管理员",
})
token = login_response.json()["data"]["token"]
# 获取统计
response = await client.get(
"/evaluations/stats",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
stats = data["data"]
assert stats["total_count"] == 5
# (5+5+5+3+1)/5 = 3.8
assert abs(stats["avg_star_rating"] - 3.8) < 0.01
# 验证星级分布
star_dist = {item["label"]: item for item in stats["star_distribution"]}
assert star_dist["5星"]["count"] == 3
assert star_dist["3星"]["count"] == 1
assert star_dist["1星"]["count"] == 1
# 验证表情分布
emoji_dist = {item["label"]: item for item in stats["emoji_distribution"]}
assert emoji_dist["满意"]["count"] == 3
assert emoji_dist["一般"]["count"] == 1
assert emoji_dist["不满意"]["count"] == 1
@pytest.mark.asyncio
async def test_get_evaluation_stats_empty(client, db_session):
"""测试获取评价统计 - 无数据"""
# 需要管理员权限
role_stmt = select(Role).where(Role.name == "admin")
role_result = await db_session.execute(role_stmt)
admin_role = role_result.scalars().first()
if not admin_role:
admin_role = Role(
name="admin",
display_name="管理员",
description="管理员角色",
permissions=["evaluation:read:all"],
)
db_session.add(admin_role)
await db_session.flush()
agent = Agent(
user_id="test_agent_admin",
name="测试管理员",
status="online",
)
db_session.add(agent)
await db_session.flush()
user_role = UserRole(
employee_id="test_agent_admin",
role_id=admin_role.id,
source="manual",
assigned_by="test",
)
db_session.add(user_role)
await db_session.flush()
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_admin",
"name": "测试管理员",
})
token = login_response.json()["data"]["token"]
# 获取统计(无数据)
response = await client.get(
"/evaluations/stats",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
stats = data["data"]
assert stats["total_count"] == 0
assert stats["avg_star_rating"] == 0.0
assert stats["star_distribution"][0]["count"] == 0
# =============================================================================
# 测试用例:发送评价邀请 (POST /conversations/{conversation_id}/send-evaluation-invite)
# =============================================================================
@pytest.mark.asyncio
async def test_send_evaluation_invite_success(client, db_session, mock_redis):
"""测试发送评价邀请 - 成功"""
# 创建会话(已结单)
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 登录坐席获取 token
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_001",
"name": "测试坐席",
})
token = login_response.json()["data"]["token"]
# 发送评价邀请
response = await client.post(
f"/conversations/{conversation.id}/send-evaluation-invite",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert "评价邀请已发送" in data["data"]["message"]
@pytest.mark.asyncio
async def test_send_evaluation_invite_conversation_not_resolved(client, db_session):
"""测试只能对已结单的会话发送评价邀请"""
# 创建未结单的会话
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="serving", # 未结单
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 登录坐席
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_001",
"name": "测试坐席",
})
token = login_response.json()["data"]["token"]
# 发送评价邀请
response = await client.post(
f"/conversations/{conversation.id}/send-evaluation-invite",
headers={"Authorization": f"Bearer {token}"},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3042 # 只能对已结单的会话发送评价邀请
@pytest.mark.asyncio
async def test_send_evaluation_invite_already_evaluated(client, db_session):
"""测试该会话已收到评价时不能再发送邀请"""
# 创建会话和评价
conversation = Conversation(
employee_id="test_employee_001",
employee_name="测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 已有评价
evaluation = ConversationEvaluation(
conversation_id=conversation.id,
employee_id="test_employee_001",
employee_name="测试员工",
star_rating=5,
emoji="satisfied",
)
db_session.add(evaluation)
await db_session.flush()
# 登录坐席
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_001",
"name": "测试坐席",
})
token = login_response.json()["data"]["token"]
# 发送评价邀请
response = await client.post(
f"/conversations/{conversation.id}/send-evaluation-invite",
headers={"Authorization": f"Bearer {token}"},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3043 # 该会话已收到评价,无需再次邀请
@pytest.mark.asyncio
async def test_send_evaluation_invite_conversation_not_found(client, db_session):
"""测试发送评价邀请时会话不存在"""
# 登录坐席
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_001",
"name": "测试坐席",
})
token = login_response.json()["data"]["token"]
# 发送评价邀请
response = await client.post(
"/conversations/nonexistent-id/send-evaluation-invite",
headers={"Authorization": f"Bearer {token}"},
)
# 应该返回错误
assert response.status_code == 200
data = response.json()
assert data["code"] == 3003 # 会话不存在
# =============================================================================
# 测试用例:端到端流程测试
# =============================================================================
@pytest.mark.asyncio
async def test_evaluation_full_flow(client, db_session, mock_redis):
"""测试完整流程:会话结单 -> 发送评价邀请 -> 提交评价 -> 查看评价 -> 统计"""
# 1. 创建会话
conversation = Conversation(
employee_id="test_employee_flow",
employee_name="流程测试员工",
status="resolved",
department="技术部",
position="工程师",
)
db_session.add(conversation)
await db_session.flush()
# 创建员工
create_test_employee(db_session, employee_id="test_employee_flow")
await db_session.flush()
# 2. 坐席发送评价邀请
login_response = await client.post("/agents/login", json={
"user_id": "test_agent_flow",
"name": "流程测试坐席",
})
agent_token = login_response.json()["data"]["token"]
invite_response = await client.post(
f"/conversations/{conversation.id}/send-evaluation-invite",
headers={"Authorization": f"Bearer {agent_token}"},
)
assert invite_response.status_code == 200
assert invite_response.json()["code"] == 0
# 3. 员工提交评价
await mock_redis.setex("employee:token:test_token_flow", 28800, "test_employee_flow")
eval_response = await client.post(
f"/conversation/{conversation.id}/evaluate",
headers={"Authorization": "Bearer test_token_flow"},
json={
"star_rating": 4,
"emoji": "satisfied",
"feedback_text": "服务很专业!",
},
)
assert eval_response.status_code == 200
assert eval_response.json()["code"] == 0
assert eval_response.json()["data"]["star_rating"] == 4
# 4. 查看会话评价
get_response = await client.get(f"/conversation/{conversation.id}/evaluation")
assert get_response.status_code == 200
assert get_response.json()["code"] == 0
assert get_response.json()["data"]["star_rating"] == 4
# 5. 尝试再次发送邀请(应该失败)
invite_again_response = await client.post(
f"/conversations/{conversation.id}/send-evaluation-invite",
headers={"Authorization": f"Bearer {agent_token}"},
)
assert invite_again_response.status_code == 200
assert invite_again_response.json()["code"] == 3043