2026-06-14 16:49:18 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 企微IT智能服务台 — 消息体验功能测试
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 说明:测试消息体验相关功能,包括:
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 1. 撤回消息 (POST /messages/{id}/recall)
|
|
|
|
|
|
# 2. 删除消息 (DELETE /messages/{id})
|
2026-06-14 16:49:18 +08:00
|
|
|
|
# 3. 标记已读 (POST /api/conversations/{id}/mark-read)
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 4. 图片上传 (POST /messages/image)
|
|
|
|
|
|
# 5. 文件上传 (POST /messages/file)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
import pytest_asyncio
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from uuid import uuid4
|
2026-07-04 21:01:39 +08:00
|
|
|
|
from tests.conftest import create_test_conversation, create_test_agent, MockRedis, login_test_agent
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 测试用例:撤回消息
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_recall_message_within_2min(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试撤回消息 - 2分钟内可撤回
|
|
|
|
|
|
|
|
|
|
|
|
预期:成功撤回消息,状态变为 "recalled"
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 创建测试会话
|
|
|
|
|
|
conv = create_test_conversation(status="serving")
|
|
|
|
|
|
db_session.add(conv)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
# 创建2分钟内的消息
|
|
|
|
|
|
message = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="agent",
|
|
|
|
|
|
sender_id="test_agent_001",
|
|
|
|
|
|
sender_name="测试坐席",
|
|
|
|
|
|
content="测试消息内容",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
recallable_until=datetime.now() + timedelta(minutes=2),
|
|
|
|
|
|
)
|
|
|
|
|
|
db_session.add(message)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_agent_001", "测试坐席")
|
|
|
|
|
|
|
|
|
|
|
|
# 调用撤回消息接口(带认证)
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/messages/{message.id}/recall",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
# 验证
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
assert data.get("code") == 0
|
|
|
|
|
|
assert "撤回成功" in data.get("message", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_recall_message_after_2min_fails(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试撤回消息 - 2分钟后不可撤回
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回403错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
conv = create_test_conversation(status="serving")
|
|
|
|
|
|
db_session.add(conv)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
# 创建超过2分钟的消息
|
|
|
|
|
|
message = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="agent",
|
|
|
|
|
|
sender_id="test_agent_001",
|
|
|
|
|
|
sender_name="测试坐席",
|
|
|
|
|
|
content="测试消息内容",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
recallable_until=datetime.now() - timedelta(minutes=1), # 已过期
|
|
|
|
|
|
)
|
|
|
|
|
|
db_session.add(message)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_agent_001", "测试坐席")
|
|
|
|
|
|
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/messages/{message.id}/recall",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
# 应该返回403错误
|
|
|
|
|
|
assert response.status_code == 403 or (response.status_code == 200 and response.json().get("code") == 403)
|
2026-07-04 21:01:39 +08:00
|
|
|
|
await db_session.flush()
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_recall_nonexistent_message(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试撤回不存在的消息
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回404错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
fake_id = str(uuid4())
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 需要坐席认证才能调用撤回接口
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_recall_nx", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/messages/{fake_id}/recall",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
# 端点返回 200,错误码在 body.code 中
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
assert response.json().get("code") == 404
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_recall_non_agent_message_fails(client, db_session, mock_redis):
|
2026-07-04 21:01:39 +08:00
|
|
|
|
"""测试撤回非坐席发送的消息
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
预期:返回403错误(只能撤回坐席发送的消息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
conv = create_test_conversation(status="serving")
|
|
|
|
|
|
db_session.add(conv)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
# 员工发送的消息
|
|
|
|
|
|
message = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="employee",
|
|
|
|
|
|
sender_id="emp_001",
|
|
|
|
|
|
sender_name="测试员工",
|
|
|
|
|
|
content="员工消息",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
)
|
|
|
|
|
|
db_session.add(message)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 需要认证:撤回接口需要坐席认证
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_recall_emp", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/messages/{message.id}/recall",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
# 应该返回403错误
|
|
|
|
|
|
assert response.status_code == 403 or (response.status_code == 200 and response.json().get("code") == 403)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 测试用例:删除消息
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_delete_message_success(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试删除消息 - 成功删除
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回200,消息被删除
|
|
|
|
|
|
"""
|
|
|
|
|
|
conv = create_test_conversation(status="serving")
|
|
|
|
|
|
db_session.add(conv)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
message = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="agent",
|
|
|
|
|
|
sender_id="test_agent_001",
|
|
|
|
|
|
sender_name="测试坐席",
|
|
|
|
|
|
content="测试消息内容",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
)
|
|
|
|
|
|
db_session.add(message)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_agent_001", "测试坐席")
|
|
|
|
|
|
response = await client.delete(
|
|
|
|
|
|
f"/messages/{message.id}",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code in [200, 204]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_delete_nonexistent_message(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试删除不存在的消息
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回404错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
fake_id = str(uuid4())
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 需要坐席认证
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_del_nx", "测试坐席")
|
|
|
|
|
|
response = await client.delete(
|
|
|
|
|
|
f"/messages/{fake_id}",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
# 端点返回 200,错误码在 body.code 中
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
assert response.json().get("code") == 404
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 测试用例:标记已读
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_mark_read_updates_messages(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试标记会话已读
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回200,所有未读消息被标记为已读
|
|
|
|
|
|
"""
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_mark_read", "测试坐席")
|
|
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
conv = create_test_conversation(status="serving")
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 确保坐席是该会话的主责或协作坐席(mark_read 需要校验)
|
|
|
|
|
|
conv.assigned_agent_id = "test_mark_read"
|
2026-06-14 16:49:18 +08:00
|
|
|
|
db_session.add(conv)
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
msg1 = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="employee",
|
|
|
|
|
|
sender_id="emp_001",
|
|
|
|
|
|
sender_name="员工",
|
|
|
|
|
|
content="员工消息1",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
is_read=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
msg2 = Message(
|
|
|
|
|
|
conversation_id=conv.id,
|
|
|
|
|
|
sender_type="employee",
|
|
|
|
|
|
sender_id="emp_001",
|
|
|
|
|
|
sender_name="员工",
|
|
|
|
|
|
content="员工消息2",
|
|
|
|
|
|
msg_type="text",
|
|
|
|
|
|
is_read=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
db_session.add_all([msg1, msg2])
|
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/conversations/{conv.id}/mark-read",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
assert data.get("code") == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_mark_read_nonexistent_conversation(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试标记不存在的会话已读
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回404错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
fake_id = str(uuid4())
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 需要坐席认证
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_mr_nx", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
f"/conversations/{fake_id}/mark-read",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
# 端点返回 200,错误码在 body.code 中
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
assert response.json().get("code") == 3003
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 测试用例:图片上传
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_upload_image_within_limit(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试图片上传 - 10MB以内
|
|
|
|
|
|
|
|
|
|
|
|
预期:成功上传,返回文件URL
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 创建小图片数据(约50KB)
|
|
|
|
|
|
image_data = b"\x89PNG\r\n\x1a\n" + b"fake_image_data" * 5000
|
|
|
|
|
|
files = {"file": ("test.png", image_data, "image/png")}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token(上传接口需要坐席认证)
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_up_img", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
"/messages/image",
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
assert data.get("code") == 0
|
|
|
|
|
|
assert "url" in data.get("data", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_upload_image_exceeds_limit(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试图片上传 - 超过10MB
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回400错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 创建大于10MB的数据
|
|
|
|
|
|
large_data = b"x" * (11 * 1024 * 1024) # 11MB
|
|
|
|
|
|
files = {"file": ("large.png", large_data, "image/png")}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
token = await login_test_agent(client, db_session, "test_up_lg", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
"/messages/image",
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400 or (response.status_code == 200 and response.json().get("code") == 400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_upload_invalid_image_type(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试上传不支持的图片格式
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回400错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 模拟不支持的格式
|
|
|
|
|
|
image_data = b"fake_image"
|
|
|
|
|
|
files = {"file": ("test.bmp", image_data, "image/bmp")}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
token = await login_test_agent(client, db_session, "test_up_inv", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
"/messages/image",
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400 or (response.status_code == 200 and response.json().get("code") == 400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 测试用例:文件上传
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_upload_file_within_limit(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试文件上传 - 10MB以内
|
|
|
|
|
|
|
|
|
|
|
|
预期:成功上传,返回文件URL
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 创建小文件(约50KB)
|
|
|
|
|
|
file_data = b"fake_file_content" * 5000
|
|
|
|
|
|
files = {"file": ("test.pdf", file_data, "application/pdf")}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
# 获取认证token
|
|
|
|
|
|
token = await login_test_agent(client, db_session, "test_up_file", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
"/messages/file",
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
assert data.get("code") == 0
|
|
|
|
|
|
assert "url" in data.get("data", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_upload_file_exceeds_limit(client, db_session, mock_redis):
|
|
|
|
|
|
"""测试文件上传 - 超过10MB
|
|
|
|
|
|
|
|
|
|
|
|
预期:返回400错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
large_data = b"x" * (11 * 1024 * 1024) # 11MB
|
|
|
|
|
|
files = {"file": ("large.pdf", large_data, "application/pdf")}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
token = await login_test_agent(client, db_session, "test_up_fl_lg", "测试坐席")
|
|
|
|
|
|
response = await client.post(
|
|
|
|
|
|
"/messages/file",
|
|
|
|
|
|
files=files,
|
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400 or (response.status_code == 200 and response.json().get("code") == 400)
|