feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
1
backend/tests/__init__.py
Normal file
1
backend/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""测试包"""
|
||||
100
backend/tests/conftest.py
Normal file
100
backend/tests/conftest.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""测试配置和fixtures"""
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Generator
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
|
||||
from app.main import app
|
||||
from app.models.base import Base
|
||||
from app.config.database import SessionLocal
|
||||
from app.core.deps import get_db
|
||||
|
||||
|
||||
# 测试数据库URL
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
||||
|
||||
# 创建测试引擎
|
||||
test_engine = create_async_engine(
|
||||
TEST_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
# 创建测试会话工厂
|
||||
TestSessionLocal = async_sessionmaker(
|
||||
test_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop() -> Generator:
|
||||
"""创建事件循环"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""创建测试数据库会话"""
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with TestSessionLocal() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""创建测试客户端"""
|
||||
async def override_get_db():
|
||||
yield db_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user():
|
||||
"""测试用户"""
|
||||
return {
|
||||
"id": 1,
|
||||
"username": "test_user",
|
||||
"role": "user",
|
||||
"token": "test_token"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_admin():
|
||||
"""测试管理员"""
|
||||
return {
|
||||
"id": 2,
|
||||
"username": "test_admin",
|
||||
"role": "admin",
|
||||
"token": "admin_token"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(test_user):
|
||||
"""认证请求头"""
|
||||
return {"Authorization": f"Bearer {test_user['token']}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_auth_headers(test_admin):
|
||||
"""管理员认证请求头"""
|
||||
return {"Authorization": f"Bearer {test_admin['token']}"}
|
||||
0
backend/tests/e2e/__init__.py
Normal file
0
backend/tests/e2e/__init__.py
Normal file
0
backend/tests/integration/__init__.py
Normal file
0
backend/tests/integration/__init__.py
Normal file
284
backend/tests/test_courses.py
Normal file
284
backend/tests/test_courses.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
课程模块测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.course import Course, CourseStatus, CourseCategory
|
||||
from app.services.course_service import course_service
|
||||
|
||||
|
||||
class TestCourseAPI:
|
||||
"""课程API测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_course_success(self, client: AsyncClient, admin_headers: dict):
|
||||
"""测试成功创建课程"""
|
||||
course_data = {
|
||||
"name": "测试课程",
|
||||
"description": "这是一个测试课程",
|
||||
"category": "technology",
|
||||
"difficulty_level": 3,
|
||||
"tags": ["Python", "测试"]
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/courses",
|
||||
json=course_data,
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["message"] == "创建课程成功"
|
||||
assert data["data"]["name"] == course_data["name"]
|
||||
assert data["data"]["status"] == "draft"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_course_unauthorized(self, client: AsyncClient, user_headers: dict):
|
||||
"""测试非管理员创建课程失败"""
|
||||
course_data = {
|
||||
"name": "测试课程",
|
||||
"description": "这是一个测试课程"
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/courses",
|
||||
json=course_data,
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_courses_list(self, client: AsyncClient, user_headers: dict, db_session: AsyncSession):
|
||||
"""测试获取课程列表"""
|
||||
# 先创建几个测试课程
|
||||
courses = [
|
||||
Course(
|
||||
name=f"测试课程{i}",
|
||||
description=f"描述{i}",
|
||||
category=CourseCategory.TECHNOLOGY if i % 2 == 0 else CourseCategory.BUSINESS,
|
||||
status=CourseStatus.PUBLISHED if i < 2 else CourseStatus.DRAFT,
|
||||
is_featured=i == 0
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
for course in courses:
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
|
||||
# 测试获取所有课程
|
||||
response = await client.get(
|
||||
"/api/v1/courses",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert len(data["data"]["items"]) == 3
|
||||
assert data["data"]["total"] == 3
|
||||
|
||||
# 测试筛选已发布课程
|
||||
response = await client.get(
|
||||
"/api/v1/courses?status=published",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["data"]["items"]) == 2
|
||||
|
||||
# 测试分类筛选
|
||||
response = await client.get(
|
||||
"/api/v1/courses?category=technology",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["data"]["items"]) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_course_detail(self, client: AsyncClient, user_headers: dict, db_session: AsyncSession):
|
||||
"""测试获取课程详情"""
|
||||
# 创建测试课程
|
||||
course = Course(
|
||||
name="测试课程详情",
|
||||
description="详细描述",
|
||||
category=CourseCategory.TECHNOLOGY,
|
||||
status=CourseStatus.PUBLISHED
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 获取课程详情
|
||||
response = await client.get(
|
||||
f"/api/v1/courses/{course.id}",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["id"] == course.id
|
||||
assert data["data"]["name"] == course.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_course_not_found(self, client: AsyncClient, user_headers: dict):
|
||||
"""测试获取不存在的课程"""
|
||||
response = await client.get(
|
||||
"/api/v1/courses/99999",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_course(self, client: AsyncClient, admin_headers: dict, db_session: AsyncSession):
|
||||
"""测试更新课程"""
|
||||
# 创建测试课程
|
||||
course = Course(
|
||||
name="原始课程名",
|
||||
description="原始描述",
|
||||
category=CourseCategory.TECHNOLOGY,
|
||||
status=CourseStatus.DRAFT
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 更新课程
|
||||
update_data = {
|
||||
"name": "更新后的课程名",
|
||||
"status": "published"
|
||||
}
|
||||
|
||||
response = await client.put(
|
||||
f"/api/v1/courses/{course.id}",
|
||||
json=update_data,
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["name"] == update_data["name"]
|
||||
assert data["data"]["status"] == "published"
|
||||
assert data["data"]["published_at"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_course(self, client: AsyncClient, admin_headers: dict, db_session: AsyncSession):
|
||||
"""测试删除课程"""
|
||||
# 创建测试课程
|
||||
course = Course(
|
||||
name="待删除课程",
|
||||
description="这个课程将被删除",
|
||||
category=CourseCategory.GENERAL,
|
||||
status=CourseStatus.DRAFT
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 删除课程
|
||||
response = await client.delete(
|
||||
f"/api/v1/courses/{course.id}",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"] is True
|
||||
|
||||
# 验证软删除
|
||||
deleted_course = await course_service.get_by_id(db_session, course.id)
|
||||
assert deleted_course is None # 因为get_by_id会过滤掉软删除的记录
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_published_course_fail(self, client: AsyncClient, admin_headers: dict, db_session: AsyncSession):
|
||||
"""测试删除已发布课程失败"""
|
||||
# 创建已发布课程
|
||||
course = Course(
|
||||
name="已发布课程",
|
||||
description="这是已发布的课程",
|
||||
category=CourseCategory.TECHNOLOGY,
|
||||
status=CourseStatus.PUBLISHED
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 尝试删除
|
||||
response = await client.delete(
|
||||
f"/api/v1/courses/{course.id}",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestKnowledgePointAPI:
|
||||
"""知识点API测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_knowledge_points(self, client: AsyncClient, user_headers: dict, db_session: AsyncSession):
|
||||
"""测试获取知识点列表"""
|
||||
# 创建测试课程
|
||||
course = Course(
|
||||
name="测试课程",
|
||||
description="包含知识点的课程",
|
||||
category=CourseCategory.TECHNOLOGY,
|
||||
status=CourseStatus.PUBLISHED
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 获取知识点(应该为空)
|
||||
response = await client.get(
|
||||
f"/api/v1/courses/{course.id}/knowledge-points",
|
||||
headers=user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_knowledge_point(self, client: AsyncClient, admin_headers: dict, db_session: AsyncSession):
|
||||
"""测试创建知识点"""
|
||||
# 创建测试课程
|
||||
course = Course(
|
||||
name="测试课程",
|
||||
description="用于测试知识点",
|
||||
category=CourseCategory.TECHNOLOGY,
|
||||
status=CourseStatus.DRAFT
|
||||
)
|
||||
db_session.add(course)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(course)
|
||||
|
||||
# 创建知识点
|
||||
point_data = {
|
||||
"name": "Python基础",
|
||||
"description": "学习Python基础知识",
|
||||
"weight": 2.0,
|
||||
"estimated_hours": 10
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/courses/{course.id}/knowledge-points",
|
||||
json=point_data,
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["data"]["name"] == point_data["name"]
|
||||
assert data["data"]["course_id"] == course.id
|
||||
assert data["data"]["level"] == 1
|
||||
assert data["data"]["parent_id"] is None
|
||||
306
backend/tests/test_coze_api.py
Normal file
306
backend/tests/test_coze_api.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
Coze API 网关单元测试
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi import FastAPI
|
||||
from sse_starlette.sse import ServerSentEvent
|
||||
|
||||
from app.api.v1.coze_gateway import router
|
||||
from app.services.ai.coze.models import (
|
||||
CreateSessionResponse, EndSessionResponse,
|
||||
StreamEvent, StreamEventType, ContentType, MessageRole
|
||||
)
|
||||
from app.services.ai.coze.exceptions import CozeAPIError, CozeAuthError
|
||||
|
||||
|
||||
# 创建测试应用
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
"""模拟已登录用户"""
|
||||
with patch("app.api.v1.coze_gateway.get_current_user") as mock_get_user:
|
||||
mock_get_user.return_value = {
|
||||
"user_id": "test-user-123",
|
||||
"username": "test_user"
|
||||
}
|
||||
yield mock_get_user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_coze_service():
|
||||
"""模拟 Coze 服务"""
|
||||
with patch("app.api.v1.coze_gateway.get_coze_service") as mock_get_service:
|
||||
mock_service = Mock()
|
||||
mock_get_service.return_value = mock_service
|
||||
yield mock_service
|
||||
|
||||
|
||||
class TestCourseChat:
|
||||
"""测试课程对话 API"""
|
||||
|
||||
def test_create_course_chat_session_success(self, mock_user, mock_coze_service):
|
||||
"""测试成功创建课程对话会话"""
|
||||
# Mock 服务响应
|
||||
mock_coze_service.create_session = AsyncMock(
|
||||
return_value=CreateSessionResponse(
|
||||
session_id="session-123",
|
||||
conversation_id="conv-123",
|
||||
bot_id="bot-123",
|
||||
created_at="2024-01-01T10:00:00"
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/course-chat/sessions",
|
||||
json={"course_id": "course-456"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["message"] == "success"
|
||||
assert data["data"]["session_id"] == "session-123"
|
||||
assert data["data"]["conversation_id"] == "conv-123"
|
||||
|
||||
def test_create_course_chat_session_auth_error(self, mock_user, mock_coze_service):
|
||||
"""测试认证错误"""
|
||||
mock_coze_service.create_session = AsyncMock(
|
||||
side_effect=CozeAuthError(
|
||||
message="认证失败",
|
||||
code="AUTH_ERROR",
|
||||
status_code=401
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/course-chat/sessions",
|
||||
json={"course_id": "course-456"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
assert data["detail"]["code"] == "AUTH_ERROR"
|
||||
assert data["detail"]["message"] == "认证失败"
|
||||
|
||||
def test_create_course_chat_session_server_error(self, mock_user, mock_coze_service):
|
||||
"""测试服务器错误"""
|
||||
mock_coze_service.create_session = AsyncMock(
|
||||
side_effect=Exception("Unexpected error")
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/course-chat/sessions",
|
||||
json={"course_id": "course-456"}
|
||||
)
|
||||
|
||||
assert response.status_code == 500
|
||||
data = response.json()
|
||||
assert data["detail"]["code"] == "INTERNAL_ERROR"
|
||||
|
||||
|
||||
class TestTraining:
|
||||
"""测试陪练 API"""
|
||||
|
||||
def test_create_training_session_with_topic(self, mock_user, mock_coze_service):
|
||||
"""测试创建带主题的陪练会话"""
|
||||
mock_coze_service.create_session = AsyncMock(
|
||||
return_value=CreateSessionResponse(
|
||||
session_id="training-123",
|
||||
conversation_id="conv-456",
|
||||
bot_id="training-bot",
|
||||
created_at="2024-01-01T11:00:00"
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/training/sessions",
|
||||
json={"training_topic": "客诉处理技巧"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["session_id"] == "training-123"
|
||||
|
||||
# 验证服务调用
|
||||
call_args = mock_coze_service.create_session.call_args[0][0]
|
||||
assert call_args.training_topic == "客诉处理技巧"
|
||||
|
||||
def test_create_training_session_without_topic(self, mock_user, mock_coze_service):
|
||||
"""测试创建不带主题的陪练会话"""
|
||||
mock_coze_service.create_session = AsyncMock(
|
||||
return_value=CreateSessionResponse(
|
||||
session_id="training-456",
|
||||
conversation_id="conv-789",
|
||||
bot_id="training-bot",
|
||||
created_at="2024-01-01T12:00:00"
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post("/api/v1/training/sessions", json={})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["session_id"] == "training-456"
|
||||
|
||||
def test_end_training_session_success(self, mock_user, mock_coze_service):
|
||||
"""测试成功结束陪练会话"""
|
||||
mock_coze_service.end_session = AsyncMock(
|
||||
return_value=EndSessionResponse(
|
||||
session_id="training-123",
|
||||
ended_at="2024-01-01T13:00:00",
|
||||
duration_seconds=1800,
|
||||
message_count=25
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/training/sessions/training-123/end",
|
||||
json={
|
||||
"reason": "练习完成",
|
||||
"feedback": {"rating": 5, "helpful": True}
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["duration_seconds"] == 1800
|
||||
assert data["data"]["message_count"] == 25
|
||||
|
||||
def test_end_nonexistent_session(self, mock_user, mock_coze_service):
|
||||
"""测试结束不存在的会话"""
|
||||
mock_coze_service.end_session = AsyncMock(
|
||||
side_effect=CozeAPIError(
|
||||
message="会话不存在",
|
||||
code="SESSION_NOT_FOUND",
|
||||
status_code=404
|
||||
)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/training/sessions/nonexistent/end",
|
||||
json={}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestChatMessages:
|
||||
"""测试消息发送 API"""
|
||||
|
||||
def test_send_message_non_stream(self, mock_user, mock_coze_service):
|
||||
"""测试非流式消息发送"""
|
||||
# Mock 异步生成器
|
||||
async def mock_generator():
|
||||
yield StreamEvent(
|
||||
event=StreamEventType.MESSAGE_DELTA,
|
||||
data={},
|
||||
content="Hello",
|
||||
content_type=ContentType.TEXT,
|
||||
role=MessageRole.ASSISTANT
|
||||
)
|
||||
yield StreamEvent(
|
||||
event=StreamEventType.MESSAGE_COMPLETED,
|
||||
data={"usage": {"tokens": 10}},
|
||||
message_id="msg-123",
|
||||
content="Hello, how can I help you?",
|
||||
content_type=ContentType.TEXT,
|
||||
role=MessageRole.ASSISTANT
|
||||
)
|
||||
yield StreamEvent(
|
||||
event=StreamEventType.DONE,
|
||||
data={"session_id": "session-123"}
|
||||
)
|
||||
|
||||
mock_coze_service.send_message = AsyncMock(return_value=mock_generator())
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/chat/messages",
|
||||
json={
|
||||
"session_id": "session-123",
|
||||
"content": "Hello",
|
||||
"stream": False
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["content"] == "Hello, how can I help you?"
|
||||
assert data["data"]["content_type"] == "text"
|
||||
assert data["data"]["role"] == "assistant"
|
||||
|
||||
def test_send_message_with_files(self, mock_user, mock_coze_service):
|
||||
"""测试带附件的消息发送"""
|
||||
async def mock_generator():
|
||||
yield StreamEvent(
|
||||
event=StreamEventType.MESSAGE_COMPLETED,
|
||||
data={},
|
||||
message_id="msg-456",
|
||||
content="File received",
|
||||
content_type=ContentType.TEXT,
|
||||
role=MessageRole.ASSISTANT
|
||||
)
|
||||
yield StreamEvent(
|
||||
event=StreamEventType.DONE,
|
||||
data={"session_id": "session-123"}
|
||||
)
|
||||
|
||||
mock_coze_service.send_message = AsyncMock(return_value=mock_generator())
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/chat/messages",
|
||||
json={
|
||||
"session_id": "session-123",
|
||||
"content": "Please analyze this file",
|
||||
"file_ids": ["file-123", "file-456"],
|
||||
"stream": False
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# 验证服务调用
|
||||
call_args = mock_coze_service.send_message.call_args[0][0]
|
||||
assert call_args.file_ids == ["file-123", "file-456"]
|
||||
|
||||
def test_get_message_history(self, mock_user, mock_coze_service):
|
||||
"""测试获取消息历史"""
|
||||
from app.services.ai.coze.models import CozeMessage
|
||||
|
||||
mock_messages = [
|
||||
CozeMessage(
|
||||
message_id="msg-1",
|
||||
session_id="session-123",
|
||||
role=MessageRole.USER,
|
||||
content="Hello"
|
||||
),
|
||||
CozeMessage(
|
||||
message_id="msg-2",
|
||||
session_id="session-123",
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Hi there!"
|
||||
)
|
||||
]
|
||||
|
||||
mock_coze_service.get_session_messages = AsyncMock(
|
||||
return_value=mock_messages
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/sessions/session-123/messages?limit=10&offset=0"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["data"]["messages"]) == 2
|
||||
assert data["data"]["messages"][0]["content"] == "Hello"
|
||||
assert data["data"]["messages"][1]["content"] == "Hi there!"
|
||||
assert data["data"]["limit"] == 10
|
||||
assert data["data"]["offset"] == 0
|
||||
168
backend/tests/test_coze_client.py
Normal file
168
backend/tests/test_coze_client.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Coze 客户端单元测试
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from cozepy import Coze, TokenAuth, OAuthJWT
|
||||
|
||||
from app.services.ai.coze.client import (
|
||||
CozeAuthManager, get_coze_client, get_bot_config,
|
||||
get_workspace_id
|
||||
)
|
||||
from app.services.ai.coze.exceptions import CozeAuthError
|
||||
|
||||
|
||||
class TestCozeAuthManager:
|
||||
"""测试认证管理器"""
|
||||
|
||||
def test_init_with_env_vars(self):
|
||||
"""测试从环境变量初始化"""
|
||||
with patch.dict(os.environ, {
|
||||
"COZE_API_BASE": "https://test.coze.cn",
|
||||
"COZE_WORKSPACE_ID": "test-workspace",
|
||||
"COZE_API_TOKEN": "test-token"
|
||||
}):
|
||||
manager = CozeAuthManager()
|
||||
assert manager.api_base == "https://test.coze.cn"
|
||||
assert manager.workspace_id == "test-workspace"
|
||||
assert manager.api_token == "test-token"
|
||||
|
||||
def test_init_with_params(self):
|
||||
"""测试从参数初始化"""
|
||||
manager = CozeAuthManager(
|
||||
api_base="https://custom.coze.cn",
|
||||
workspace_id="custom-workspace",
|
||||
api_token="custom-token"
|
||||
)
|
||||
assert manager.api_base == "https://custom.coze.cn"
|
||||
assert manager.workspace_id == "custom-workspace"
|
||||
assert manager.api_token == "custom-token"
|
||||
|
||||
def test_setup_direct_connection(self):
|
||||
"""测试直连设置"""
|
||||
manager = CozeAuthManager()
|
||||
no_proxy = os.environ.get("NO_PROXY", "")
|
||||
assert "api.coze.cn" in no_proxy
|
||||
assert ".coze.cn" in no_proxy
|
||||
assert "localhost" in no_proxy
|
||||
|
||||
@patch("app.services.ai.coze.client.TokenAuth")
|
||||
@patch("app.services.ai.coze.client.Coze")
|
||||
def test_token_auth_success(self, mock_coze_class, mock_token_auth):
|
||||
"""测试 Token 认证成功"""
|
||||
manager = CozeAuthManager(api_token="test-token")
|
||||
mock_client = Mock()
|
||||
mock_coze_class.return_value = mock_client
|
||||
|
||||
client = manager.get_client()
|
||||
|
||||
mock_token_auth.assert_called_once_with("test-token")
|
||||
mock_coze_class.assert_called_once()
|
||||
assert client == mock_client
|
||||
|
||||
def test_token_auth_no_token(self):
|
||||
"""测试没有 Token 时的错误"""
|
||||
manager = CozeAuthManager(api_token=None)
|
||||
|
||||
with pytest.raises(CozeAuthError, match="API Token 未配置"):
|
||||
manager.get_client()
|
||||
|
||||
@patch("builtins.open", create=True)
|
||||
@patch("app.services.ai.coze.client.serialization.load_pem_private_key")
|
||||
@patch("app.services.ai.coze.client.OAuthJWT")
|
||||
@patch("app.services.ai.coze.client.Coze")
|
||||
def test_oauth_auth_success(self, mock_coze_class, mock_oauth_jwt,
|
||||
mock_load_key, mock_open):
|
||||
"""测试 OAuth 认证成功"""
|
||||
# 模拟私钥文件
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b"fake-private-key"
|
||||
mock_load_key.return_value = Mock()
|
||||
|
||||
manager = CozeAuthManager(
|
||||
oauth_client_id="test-client",
|
||||
oauth_public_key_id="test-key-id",
|
||||
oauth_private_key_path="/path/to/key.pem"
|
||||
)
|
||||
|
||||
mock_client = Mock()
|
||||
mock_coze_class.return_value = mock_client
|
||||
|
||||
client = manager.get_client()
|
||||
|
||||
mock_oauth_jwt.assert_called_once()
|
||||
assert client == mock_client
|
||||
|
||||
@patch("builtins.open", side_effect=FileNotFoundError)
|
||||
@patch("app.services.ai.coze.client.TokenAuth")
|
||||
@patch("app.services.ai.coze.client.Coze")
|
||||
def test_oauth_fallback_to_token(self, mock_coze_class, mock_token_auth, mock_open):
|
||||
"""测试 OAuth 失败后回退到 Token"""
|
||||
manager = CozeAuthManager(
|
||||
api_token="fallback-token",
|
||||
oauth_client_id="test-client",
|
||||
oauth_public_key_id="test-key-id",
|
||||
oauth_private_key_path="/nonexistent/key.pem"
|
||||
)
|
||||
|
||||
mock_client = Mock()
|
||||
mock_coze_class.return_value = mock_client
|
||||
|
||||
client = manager.get_client()
|
||||
|
||||
# 应该使用 Token 认证
|
||||
mock_token_auth.assert_called_once_with("fallback-token")
|
||||
assert client == mock_client
|
||||
|
||||
def test_refresh_token(self):
|
||||
"""测试刷新令牌"""
|
||||
manager = CozeAuthManager(api_token="test-token")
|
||||
|
||||
with patch.object(manager, '_init_client') as mock_init:
|
||||
manager.refresh_token()
|
||||
assert manager._client is None
|
||||
mock_init.assert_called_once()
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""测试辅助函数"""
|
||||
|
||||
def test_get_bot_config(self):
|
||||
"""测试获取 Bot 配置"""
|
||||
with patch.dict(os.environ, {
|
||||
"COZE_CHAT_BOT_ID": "chat-bot-123",
|
||||
"COZE_TRAINING_BOT_ID": "training-bot-456",
|
||||
"COZE_EXAM_BOT_ID": "exam-bot-789"
|
||||
}):
|
||||
config = get_bot_config()
|
||||
assert config["course_chat"] == "chat-bot-123"
|
||||
assert config["training"] == "training-bot-456"
|
||||
assert config["exam"] == "exam-bot-789"
|
||||
|
||||
def test_get_workspace_id_success(self):
|
||||
"""测试获取工作空间 ID 成功"""
|
||||
with patch.dict(os.environ, {"COZE_WORKSPACE_ID": "workspace-123"}):
|
||||
workspace_id = get_workspace_id()
|
||||
assert workspace_id == "workspace-123"
|
||||
|
||||
def test_get_workspace_id_not_configured(self):
|
||||
"""测试工作空间 ID 未配置"""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(CozeAuthError, match="COZE_WORKSPACE_ID 未配置"):
|
||||
get_workspace_id()
|
||||
|
||||
@patch("app.services.ai.coze.client.get_auth_manager")
|
||||
def test_get_coze_client(self, mock_get_auth_manager):
|
||||
"""测试获取 Coze 客户端"""
|
||||
mock_manager = Mock()
|
||||
mock_client = Mock()
|
||||
mock_manager.get_client.return_value = mock_client
|
||||
mock_get_auth_manager.return_value = mock_manager
|
||||
|
||||
# 清除缓存
|
||||
get_coze_client.cache_clear()
|
||||
|
||||
client = get_coze_client()
|
||||
assert client == mock_client
|
||||
mock_manager.get_client.assert_called_once()
|
||||
274
backend/tests/test_coze_service.py
Normal file
274
backend/tests/test_coze_service.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Coze 服务层单元测试
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
|
||||
from cozepy import ChatEventType
|
||||
|
||||
from app.services.ai.coze.service import CozeService, get_coze_service
|
||||
from app.services.ai.coze.models import (
|
||||
SessionType, MessageRole, ContentType, StreamEventType,
|
||||
CreateSessionRequest, SendMessageRequest, EndSessionRequest,
|
||||
CozeSession, CozeMessage, StreamEvent
|
||||
)
|
||||
from app.services.ai.coze.exceptions import CozeAPIError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def coze_service():
|
||||
"""创建测试用的服务实例"""
|
||||
with patch("app.services.ai.coze.service.get_coze_client"):
|
||||
service = CozeService()
|
||||
service.bot_config = {
|
||||
"course_chat": "chat-bot-id",
|
||||
"training": "training-bot-id",
|
||||
"exam": "exam-bot-id"
|
||||
}
|
||||
service.workspace_id = "test-workspace"
|
||||
return service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCozeService:
|
||||
"""测试 Coze 服务"""
|
||||
|
||||
async def test_create_course_chat_session(self, coze_service):
|
||||
"""测试创建课程对话会话"""
|
||||
# Mock Coze client
|
||||
mock_conversation = Mock(id="conv-123")
|
||||
coze_service.client.conversations.create = Mock(return_value=mock_conversation)
|
||||
|
||||
request = CreateSessionRequest(
|
||||
session_type=SessionType.COURSE_CHAT,
|
||||
user_id="user-123",
|
||||
course_id="course-456"
|
||||
)
|
||||
|
||||
response = await coze_service.create_session(request)
|
||||
|
||||
# 验证结果
|
||||
assert response.conversation_id == "conv-123"
|
||||
assert response.bot_id == "chat-bot-id"
|
||||
assert isinstance(response.session_id, str)
|
||||
assert isinstance(response.created_at, datetime)
|
||||
|
||||
# 验证会话已保存
|
||||
session = coze_service._sessions[response.session_id]
|
||||
assert session.session_type == SessionType.COURSE_CHAT
|
||||
assert session.user_id == "user-123"
|
||||
assert session.metadata["course_id"] == "course-456"
|
||||
|
||||
async def test_create_training_session(self, coze_service):
|
||||
"""测试创建陪练会话"""
|
||||
mock_conversation = Mock(id="conv-456")
|
||||
coze_service.client.conversations.create = Mock(return_value=mock_conversation)
|
||||
|
||||
request = CreateSessionRequest(
|
||||
session_type=SessionType.TRAINING,
|
||||
user_id="user-789",
|
||||
training_topic="客诉处理"
|
||||
)
|
||||
|
||||
response = await coze_service.create_session(request)
|
||||
|
||||
assert response.conversation_id == "conv-456"
|
||||
assert response.bot_id == "training-bot-id"
|
||||
|
||||
session = coze_service._sessions[response.session_id]
|
||||
assert session.session_type == SessionType.TRAINING
|
||||
assert session.metadata["training_topic"] == "客诉处理"
|
||||
|
||||
async def test_send_message_with_stream(self, coze_service):
|
||||
"""测试发送消息(流式响应)"""
|
||||
# 创建测试会话
|
||||
session = CozeSession(
|
||||
session_id="test-session",
|
||||
conversation_id="conv-123",
|
||||
session_type=SessionType.COURSE_CHAT,
|
||||
user_id="user-123",
|
||||
bot_id="chat-bot-id"
|
||||
)
|
||||
coze_service._sessions["test-session"] = session
|
||||
coze_service._messages["test-session"] = []
|
||||
|
||||
# Mock 流式响应
|
||||
mock_events = [
|
||||
Mock(
|
||||
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
|
||||
conversation_id="conv-123",
|
||||
message=Mock(content="Hello ")
|
||||
),
|
||||
Mock(
|
||||
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
|
||||
conversation_id="conv-123",
|
||||
message=Mock(content="world!")
|
||||
),
|
||||
Mock(
|
||||
event=ChatEventType.CONVERSATION_MESSAGE_COMPLETED,
|
||||
conversation_id="conv-123",
|
||||
message=Mock(content="Hello world!"),
|
||||
usage={"tokens": 10}
|
||||
)
|
||||
]
|
||||
|
||||
coze_service.client.chat.stream = Mock(return_value=iter(mock_events))
|
||||
|
||||
request = SendMessageRequest(
|
||||
session_id="test-session",
|
||||
content="Hi there",
|
||||
stream=True
|
||||
)
|
||||
|
||||
# 收集事件
|
||||
events = []
|
||||
async for event in coze_service.send_message(request):
|
||||
events.append(event)
|
||||
|
||||
# 验证事件
|
||||
assert len(events) == 4 # 2 delta + 1 completed + 1 done
|
||||
assert events[0].event == StreamEventType.MESSAGE_DELTA
|
||||
assert events[0].content == "Hello "
|
||||
assert events[1].event == StreamEventType.MESSAGE_DELTA
|
||||
assert events[1].content == "world!"
|
||||
assert events[2].event == StreamEventType.MESSAGE_COMPLETED
|
||||
assert events[2].content == "Hello world!"
|
||||
assert events[3].event == StreamEventType.DONE
|
||||
|
||||
# 验证消息已保存
|
||||
messages = coze_service._messages["test-session"]
|
||||
assert len(messages) == 2 # 用户消息 + 助手消息
|
||||
assert messages[0].role == MessageRole.USER
|
||||
assert messages[0].content == "Hi there"
|
||||
assert messages[1].role == MessageRole.ASSISTANT
|
||||
assert messages[1].content == "Hello world!"
|
||||
|
||||
async def test_send_message_error_handling(self, coze_service):
|
||||
"""测试发送消息错误处理"""
|
||||
# 不存在的会话
|
||||
request = SendMessageRequest(
|
||||
session_id="nonexistent",
|
||||
content="Test"
|
||||
)
|
||||
|
||||
with pytest.raises(CozeAPIError, match="会话不存在"):
|
||||
async for _ in coze_service.send_message(request):
|
||||
pass
|
||||
|
||||
async def test_end_session(self, coze_service):
|
||||
"""测试结束会话"""
|
||||
# 创建测试会话和消息
|
||||
created_at = datetime.now()
|
||||
session = CozeSession(
|
||||
session_id="test-session",
|
||||
conversation_id="conv-123",
|
||||
session_type=SessionType.TRAINING,
|
||||
user_id="user-123",
|
||||
bot_id="training-bot-id",
|
||||
created_at=created_at
|
||||
)
|
||||
coze_service._sessions["test-session"] = session
|
||||
coze_service._messages["test-session"] = [
|
||||
Mock(), Mock(), Mock() # 3条消息
|
||||
]
|
||||
|
||||
request = EndSessionRequest(
|
||||
reason="用户主动结束",
|
||||
feedback={"rating": 5, "comment": "很有帮助"}
|
||||
)
|
||||
|
||||
response = await coze_service.end_session("test-session", request)
|
||||
|
||||
# 验证响应
|
||||
assert response.session_id == "test-session"
|
||||
assert isinstance(response.ended_at, datetime)
|
||||
assert response.message_count == 3
|
||||
assert response.duration_seconds > 0
|
||||
|
||||
# 验证会话元数据
|
||||
assert session.metadata["end_reason"] == "用户主动结束"
|
||||
assert session.metadata["feedback"]["rating"] == 5
|
||||
|
||||
async def test_end_nonexistent_session(self, coze_service):
|
||||
"""测试结束不存在的会话"""
|
||||
request = EndSessionRequest()
|
||||
|
||||
with pytest.raises(CozeAPIError, match="会话不存在"):
|
||||
await coze_service.end_session("nonexistent", request)
|
||||
|
||||
async def test_get_session_messages(self, coze_service):
|
||||
"""测试获取会话消息历史"""
|
||||
# 创建测试消息
|
||||
messages = [
|
||||
CozeMessage(
|
||||
message_id=f"msg-{i}",
|
||||
session_id="test-session",
|
||||
role=MessageRole.USER if i % 2 == 0 else MessageRole.ASSISTANT,
|
||||
content=f"Message {i}"
|
||||
)
|
||||
for i in range(10)
|
||||
]
|
||||
coze_service._messages["test-session"] = messages
|
||||
|
||||
# 测试分页
|
||||
result = await coze_service.get_session_messages("test-session", limit=5, offset=2)
|
||||
|
||||
assert len(result) == 5
|
||||
assert result[0].content == "Message 2"
|
||||
assert result[4].content == "Message 6"
|
||||
|
||||
async def test_stream_with_card_content(self, coze_service):
|
||||
"""测试流式响应中的卡片内容"""
|
||||
# 创建测试会话
|
||||
session = CozeSession(
|
||||
session_id="test-session",
|
||||
conversation_id="conv-123",
|
||||
session_type=SessionType.EXAM,
|
||||
user_id="user-123",
|
||||
bot_id="exam-bot-id"
|
||||
)
|
||||
coze_service._sessions["test-session"] = session
|
||||
coze_service._messages["test-session"] = []
|
||||
|
||||
# Mock 包含卡片的流式响应
|
||||
mock_events = [
|
||||
Mock(
|
||||
event=ChatEventType.CONVERSATION_MESSAGE_DELTA,
|
||||
conversation_id="conv-123",
|
||||
message=Mock(content='{"question": "测试题目"}', content_type="card")
|
||||
),
|
||||
Mock(
|
||||
event=ChatEventType.CONVERSATION_MESSAGE_COMPLETED,
|
||||
conversation_id="conv-123",
|
||||
message=Mock(content='{"question": "测试题目"}', content_type="card")
|
||||
)
|
||||
]
|
||||
|
||||
coze_service.client.chat.stream = Mock(return_value=iter(mock_events))
|
||||
|
||||
request = SendMessageRequest(
|
||||
session_id="test-session",
|
||||
content="生成一道考题"
|
||||
)
|
||||
|
||||
events = []
|
||||
async for event in coze_service.send_message(request):
|
||||
events.append(event)
|
||||
|
||||
# 验证卡片类型被正确识别
|
||||
assert events[0].content_type == ContentType.CARD
|
||||
assert events[1].content_type == ContentType.CARD
|
||||
|
||||
# 验证消息保存时的内容类型
|
||||
messages = coze_service._messages["test-session"]
|
||||
assert messages[1].content_type == ContentType.CARD
|
||||
|
||||
|
||||
def test_get_coze_service_singleton():
|
||||
"""测试服务单例"""
|
||||
service1 = get_coze_service()
|
||||
service2 = get_coze_service()
|
||||
assert service1 is service2
|
||||
35
backend/tests/test_main.py
Normal file
35
backend/tests/test_main.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
主应用测试
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_root():
|
||||
"""测试根路径"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "考培练系统"
|
||||
assert data["status"] == "running"
|
||||
assert "version" in data
|
||||
assert "timestamp" in data
|
||||
|
||||
|
||||
def test_health():
|
||||
"""测试健康检查端点"""
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "healthy"}
|
||||
|
||||
|
||||
def test_api_health():
|
||||
"""测试API健康检查"""
|
||||
response = client.get("/api/v1/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert data["api_version"] == "v1"
|
||||
399
backend/tests/test_training.py
Normal file
399
backend/tests/test_training.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""陪练模块测试"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.training import TrainingScene, TrainingSession, TrainingSceneStatus
|
||||
from app.services.training_service import TrainingSceneService, TrainingSessionService
|
||||
|
||||
|
||||
class TestTrainingSceneAPI:
|
||||
"""陪练场景API测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_training_scenes(self, client: AsyncClient, auth_headers: dict):
|
||||
"""测试获取陪练场景列表"""
|
||||
response = await client.get(
|
||||
"/api/v1/training/scenes",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "data" in data
|
||||
assert "items" in data["data"]
|
||||
assert "total" in data["data"]
|
||||
assert "page" in data["data"]
|
||||
assert "page_size" in data["data"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_training_scene_admin_only(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
admin_auth_headers: dict
|
||||
):
|
||||
"""测试创建陪练场景(需要管理员权限)"""
|
||||
scene_data = {
|
||||
"name": "面试训练",
|
||||
"description": "模拟面试场景,提升面试技巧",
|
||||
"category": "面试",
|
||||
"ai_config": {
|
||||
"bot_id": "test_bot_id",
|
||||
"prompt": "你是一位专业的面试官"
|
||||
},
|
||||
"is_public": True
|
||||
}
|
||||
|
||||
# 普通用户无权限
|
||||
response = await client.post(
|
||||
"/api/v1/training/scenes",
|
||||
json=scene_data,
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
# 管理员可以创建
|
||||
# 注意:这里需要mock管理员权限检查
|
||||
# 在实际测试中,需要正确设置依赖覆盖
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_training_scene_detail(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试获取陪练场景详情"""
|
||||
# 创建测试场景
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
# 获取场景详情
|
||||
response = await client.get(
|
||||
f"/api/v1/training/scenes/{scene.id}",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["data"]["id"] == scene.id
|
||||
assert data["data"]["name"] == "测试场景"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_scene(self, client: AsyncClient, auth_headers: dict):
|
||||
"""测试获取不存在的场景"""
|
||||
response = await client.get(
|
||||
"/api/v1/training/scenes/99999",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestTrainingSessionAPI:
|
||||
"""陪练会话API测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_training(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试开始陪练"""
|
||||
# 创建测试场景
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试陪练场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
# 开始陪练
|
||||
response = await client.post(
|
||||
"/api/v1/training/sessions",
|
||||
json={
|
||||
"scene_id": scene.id,
|
||||
"config": {"key": "value"}
|
||||
},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "session_id" in data["data"]
|
||||
assert data["data"]["scene"]["id"] == scene.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_end_training(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试结束陪练"""
|
||||
# 创建测试场景和会话
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
session_service = TrainingSessionService()
|
||||
session = await session_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"scene_id": scene.id,
|
||||
"session_config": {}
|
||||
},
|
||||
user_id=1,
|
||||
created_by=1
|
||||
)
|
||||
|
||||
# 结束陪练
|
||||
response = await client.post(
|
||||
f"/api/v1/training/sessions/{session.id}/end",
|
||||
json={"generate_report": True},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["data"]["session"]["status"] == "completed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_sessions(self, client: AsyncClient, auth_headers: dict):
|
||||
"""测试获取用户会话列表"""
|
||||
response = await client.get(
|
||||
"/api/v1/training/sessions",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "items" in data["data"]
|
||||
assert isinstance(data["data"]["items"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_session_messages(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试获取会话消息"""
|
||||
# 创建测试数据
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
session_service = TrainingSessionService()
|
||||
session = await session_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"scene_id": scene.id,
|
||||
"session_config": {}
|
||||
},
|
||||
user_id=1,
|
||||
created_by=1
|
||||
)
|
||||
|
||||
# 获取消息
|
||||
response = await client.get(
|
||||
f"/api/v1/training/sessions/{session.id}/messages",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
|
||||
class TestTrainingReportAPI:
|
||||
"""陪练报告API测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_reports(self, client: AsyncClient, auth_headers: dict):
|
||||
"""测试获取用户报告列表"""
|
||||
response = await client.get(
|
||||
"/api/v1/training/reports",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "items" in data["data"]
|
||||
assert isinstance(data["data"]["items"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_report_by_session(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
auth_headers: dict,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试根据会话ID获取报告"""
|
||||
# 创建测试数据
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
session_service = TrainingSessionService()
|
||||
session = await session_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"scene_id": scene.id,
|
||||
"session_config": {}
|
||||
},
|
||||
user_id=1,
|
||||
created_by=1
|
||||
)
|
||||
|
||||
# 获取报告(会话还没有报告)
|
||||
response = await client.get(
|
||||
f"/api/v1/training/sessions/{session.id}/report",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestTrainingService:
|
||||
"""陪练服务层测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scene_service_crud(self, db_session: AsyncSession):
|
||||
"""测试场景服务的CRUD操作"""
|
||||
scene_service = TrainingSceneService()
|
||||
|
||||
# 创建
|
||||
scene = await scene_service.create_scene(
|
||||
db_session,
|
||||
scene_in={
|
||||
"name": "演讲训练",
|
||||
"description": "提升演讲能力",
|
||||
"category": "演讲",
|
||||
"status": TrainingSceneStatus.ACTIVE
|
||||
},
|
||||
created_by=1
|
||||
)
|
||||
|
||||
assert scene.id is not None
|
||||
assert scene.name == "演讲训练"
|
||||
|
||||
# 读取
|
||||
retrieved = await scene_service.get(db_session, scene.id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == scene.id
|
||||
|
||||
# 更新
|
||||
updated = await scene_service.update_scene(
|
||||
db_session,
|
||||
scene_id=scene.id,
|
||||
scene_in={"description": "提升公众演讲能力"},
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
assert updated is not None
|
||||
assert updated.description == "提升公众演讲能力"
|
||||
|
||||
# 软删除
|
||||
success = await scene_service.soft_delete(db_session, id=scene.id)
|
||||
assert success is True
|
||||
|
||||
# 验证软删除
|
||||
deleted = await scene_service.get(db_session, scene.id)
|
||||
assert deleted.is_deleted is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_lifecycle(self, db_session: AsyncSession):
|
||||
"""测试会话生命周期"""
|
||||
# 创建场景
|
||||
scene_service = TrainingSceneService()
|
||||
scene = await scene_service.create(
|
||||
db_session,
|
||||
obj_in={
|
||||
"name": "测试场景",
|
||||
"category": "测试",
|
||||
"status": TrainingSceneStatus.ACTIVE,
|
||||
"is_public": True
|
||||
},
|
||||
created_by=1,
|
||||
updated_by=1
|
||||
)
|
||||
|
||||
# 开始会话
|
||||
session_service = TrainingSessionService()
|
||||
start_response = await session_service.start_training(
|
||||
db_session,
|
||||
request={"scene_id": scene.id},
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert start_response.session_id is not None
|
||||
|
||||
# 结束会话
|
||||
end_response = await session_service.end_training(
|
||||
db_session,
|
||||
session_id=start_response.session_id,
|
||||
request={"generate_report": True},
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert end_response.session.status == "completed"
|
||||
assert end_response.session.duration_seconds is not None
|
||||
|
||||
# 报告应该被生成
|
||||
if end_response.report:
|
||||
assert end_response.report.overall_score > 0
|
||||
assert len(end_response.report.strengths) > 0
|
||||
assert len(end_response.report.suggestions) > 0
|
||||
256
backend/tests/test_user_service.py
Normal file
256
backend/tests/test_user_service.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
用户服务测试
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import ConflictError, NotFoundError
|
||||
from app.core.security import verify_password
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate, UserFilter, UserUpdate
|
||||
from app.services.user_service import UserService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUserService:
|
||||
"""用户服务测试类"""
|
||||
|
||||
async def test_create_user(self, db_session: AsyncSession):
|
||||
"""测试创建用户"""
|
||||
# 准备数据
|
||||
user_in = UserCreate(
|
||||
username="newuser",
|
||||
email="newuser@example.com",
|
||||
password="password123",
|
||||
full_name="New User",
|
||||
role="trainee",
|
||||
)
|
||||
|
||||
# 创建用户
|
||||
service = UserService(db_session)
|
||||
user = await service.create_user(obj_in=user_in)
|
||||
|
||||
# 验证结果
|
||||
assert user.username == "newuser"
|
||||
assert user.email == "newuser@example.com"
|
||||
assert user.full_name == "New User"
|
||||
assert user.role == "trainee"
|
||||
assert verify_password("password123", user.hashed_password)
|
||||
|
||||
async def test_create_user_duplicate_username(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试创建重复用户名的用户"""
|
||||
user_in = UserCreate(
|
||||
username=test_user.username, # 使用已存在的用户名
|
||||
email="another@example.com",
|
||||
password="password123",
|
||||
)
|
||||
|
||||
service = UserService(db_session)
|
||||
with pytest.raises(ConflictError) as exc_info:
|
||||
await service.create_user(obj_in=user_in)
|
||||
|
||||
assert f"用户名 {test_user.username} 已存在" in str(exc_info.value)
|
||||
|
||||
async def test_create_user_duplicate_email(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试创建重复邮箱的用户"""
|
||||
user_in = UserCreate(
|
||||
username="anotheruser",
|
||||
email=test_user.email, # 使用已存在的邮箱
|
||||
password="password123",
|
||||
)
|
||||
|
||||
service = UserService(db_session)
|
||||
with pytest.raises(ConflictError) as exc_info:
|
||||
await service.create_user(obj_in=user_in)
|
||||
|
||||
assert f"邮箱 {test_user.email} 已存在" in str(exc_info.value)
|
||||
|
||||
async def test_get_by_username(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试根据用户名获取用户"""
|
||||
service = UserService(db_session)
|
||||
user = await service.get_by_username(test_user.username)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.username == test_user.username
|
||||
|
||||
async def test_get_by_email(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试根据邮箱获取用户"""
|
||||
service = UserService(db_session)
|
||||
user = await service.get_by_email(test_user.email)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.email == test_user.email
|
||||
|
||||
async def test_update_user(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试更新用户"""
|
||||
user_update = UserUpdate(
|
||||
full_name="Updated Name",
|
||||
bio="Updated bio",
|
||||
)
|
||||
|
||||
service = UserService(db_session)
|
||||
user = await service.update_user(
|
||||
user_id=test_user.id,
|
||||
obj_in=user_update,
|
||||
)
|
||||
|
||||
assert user.full_name == "Updated Name"
|
||||
assert user.bio == "Updated bio"
|
||||
|
||||
async def test_update_user_not_found(self, db_session: AsyncSession):
|
||||
"""测试更新不存在的用户"""
|
||||
user_update = UserUpdate(full_name="Updated Name")
|
||||
|
||||
service = UserService(db_session)
|
||||
with pytest.raises(NotFoundError) as exc_info:
|
||||
await service.update_user(
|
||||
user_id=999,
|
||||
obj_in=user_update,
|
||||
)
|
||||
|
||||
assert "用户不存在" in str(exc_info.value)
|
||||
|
||||
async def test_update_password(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试更新密码"""
|
||||
service = UserService(db_session)
|
||||
|
||||
# 更新密码
|
||||
user = await service.update_password(
|
||||
user_id=test_user.id,
|
||||
old_password="testpass123",
|
||||
new_password="newpass123",
|
||||
)
|
||||
|
||||
# 验证新密码
|
||||
assert verify_password("newpass123", user.hashed_password)
|
||||
assert user.password_changed_at is not None
|
||||
|
||||
async def test_update_password_wrong_old(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试使用错误的旧密码更新"""
|
||||
service = UserService(db_session)
|
||||
|
||||
with pytest.raises(ConflictError) as exc_info:
|
||||
await service.update_password(
|
||||
user_id=test_user.id,
|
||||
old_password="wrongpass",
|
||||
new_password="newpass123",
|
||||
)
|
||||
|
||||
assert "旧密码错误" in str(exc_info.value)
|
||||
|
||||
async def test_get_users_with_filter(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
admin_user: User,
|
||||
manager_user: User,
|
||||
):
|
||||
"""测试根据筛选条件获取用户"""
|
||||
service = UserService(db_session)
|
||||
|
||||
# 测试角色筛选
|
||||
filter_params = UserFilter(role="admin")
|
||||
users, total = await service.get_users_with_filter(
|
||||
skip=0,
|
||||
limit=10,
|
||||
filter_params=filter_params,
|
||||
)
|
||||
assert total == 1
|
||||
assert users[0].id == admin_user.id
|
||||
|
||||
# 测试关键词搜索
|
||||
filter_params = UserFilter(keyword="manager")
|
||||
users, total = await service.get_users_with_filter(
|
||||
skip=0,
|
||||
limit=10,
|
||||
filter_params=filter_params,
|
||||
)
|
||||
assert total == 1
|
||||
assert users[0].id == manager_user.id
|
||||
|
||||
async def test_authenticate_username(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试使用用户名认证"""
|
||||
service = UserService(db_session)
|
||||
|
||||
# 正确的密码
|
||||
user = await service.authenticate(
|
||||
username=test_user.username,
|
||||
password="testpass123",
|
||||
)
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
|
||||
# 错误的密码
|
||||
user = await service.authenticate(
|
||||
username=test_user.username,
|
||||
password="wrongpass",
|
||||
)
|
||||
assert user is None
|
||||
|
||||
async def test_authenticate_email(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试使用邮箱认证"""
|
||||
service = UserService(db_session)
|
||||
|
||||
user = await service.authenticate(
|
||||
username=test_user.email,
|
||||
password="testpass123",
|
||||
)
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
|
||||
async def test_soft_delete(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
):
|
||||
"""测试软删除用户"""
|
||||
service = UserService(db_session)
|
||||
|
||||
# 软删除
|
||||
user = await service.soft_delete(db_obj=test_user)
|
||||
assert user.is_deleted is True
|
||||
assert user.deleted_at is not None
|
||||
|
||||
# 验证无法通过常规方法获取
|
||||
user = await service.get_by_id(test_user.id)
|
||||
assert user is None
|
||||
|
||||
0
backend/tests/unit/__init__.py
Normal file
0
backend/tests/unit/__init__.py
Normal file
208
backend/tests/unit/test_auth.py
Normal file
208
backend/tests/unit/test_auth.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
认证模块单元测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.auth_service import AuthService
|
||||
from app.schemas.auth import UserRegister
|
||||
from app.core.security import verify_password, create_password_hash
|
||||
from app.core.exceptions import InvalidCredentialsError, UsernameExistsError
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAuthService:
|
||||
"""认证服务测试类"""
|
||||
|
||||
async def test_user_registration(self, db_session: AsyncSession, test_user_data):
|
||||
"""测试用户注册"""
|
||||
# 创建认证服务
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 准备注册数据
|
||||
register_data = UserRegister(**test_user_data)
|
||||
|
||||
# 注册用户
|
||||
user = await auth_service.create_user(register_data)
|
||||
|
||||
# 验证用户创建成功
|
||||
assert user.id is not None
|
||||
assert user.username == test_user_data["username"]
|
||||
assert user.email == test_user_data["email"]
|
||||
assert user.is_active is True
|
||||
assert user.role == "trainee"
|
||||
|
||||
# 验证密码已加密
|
||||
assert user.password_hash != test_user_data["password"]
|
||||
assert verify_password(test_user_data["password"], user.password_hash)
|
||||
|
||||
async def test_duplicate_username_registration(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user_data
|
||||
):
|
||||
"""测试重复用户名注册"""
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 第一次注册
|
||||
register_data = UserRegister(**test_user_data)
|
||||
await auth_service.create_user(register_data)
|
||||
|
||||
# 尝试使用相同用户名再次注册
|
||||
with pytest.raises(UsernameExistsError):
|
||||
await auth_service.create_user(register_data)
|
||||
|
||||
async def test_user_login(self, db_session: AsyncSession, test_user_data):
|
||||
"""测试用户登录"""
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 先注册用户
|
||||
register_data = UserRegister(**test_user_data)
|
||||
user = await auth_service.create_user(register_data)
|
||||
|
||||
# 测试登录
|
||||
authenticated_user = await auth_service.authenticate_user(
|
||||
username=test_user_data["username"],
|
||||
password=test_user_data["password"]
|
||||
)
|
||||
|
||||
assert authenticated_user.id == user.id
|
||||
assert authenticated_user.username == user.username
|
||||
|
||||
# 验证登录信息已更新
|
||||
assert authenticated_user.login_count == "1"
|
||||
assert authenticated_user.failed_login_count == "0"
|
||||
assert authenticated_user.last_login is not None
|
||||
|
||||
async def test_login_with_email(self, db_session: AsyncSession, test_user_data):
|
||||
"""测试使用邮箱登录"""
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 注册用户
|
||||
register_data = UserRegister(**test_user_data)
|
||||
await auth_service.create_user(register_data)
|
||||
|
||||
# 使用邮箱登录
|
||||
user = await auth_service.authenticate_user(
|
||||
username=test_user_data["email"],
|
||||
password=test_user_data["password"]
|
||||
)
|
||||
|
||||
assert user.email == test_user_data["email"]
|
||||
|
||||
async def test_invalid_password_login(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user_data
|
||||
):
|
||||
"""测试错误密码登录"""
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 注册用户
|
||||
register_data = UserRegister(**test_user_data)
|
||||
await auth_service.create_user(register_data)
|
||||
|
||||
# 尝试使用错误密码登录
|
||||
with pytest.raises(InvalidCredentialsError):
|
||||
await auth_service.authenticate_user(
|
||||
username=test_user_data["username"],
|
||||
password="WrongPassword123!"
|
||||
)
|
||||
|
||||
async def test_token_creation(self, db_session: AsyncSession, test_user_data):
|
||||
"""测试Token创建"""
|
||||
auth_service = AuthService(db_session)
|
||||
|
||||
# 注册用户
|
||||
register_data = UserRegister(**test_user_data)
|
||||
user = await auth_service.create_user(register_data)
|
||||
|
||||
# 创建tokens
|
||||
tokens = await auth_service.create_tokens_for_user(user)
|
||||
|
||||
assert "access_token" in tokens
|
||||
assert "refresh_token" in tokens
|
||||
assert tokens["token_type"] == "bearer"
|
||||
assert tokens["expires_in"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAuthAPI:
|
||||
"""认证API测试类"""
|
||||
|
||||
async def test_register_endpoint(self, client: AsyncClient, test_user_data):
|
||||
"""测试注册端点"""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=test_user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert data["message"] == "注册成功"
|
||||
assert "access_token" in data["data"]
|
||||
assert "refresh_token" in data["data"]
|
||||
|
||||
async def test_login_endpoint(self, client: AsyncClient, test_user_data):
|
||||
"""测试登录端点"""
|
||||
# 先注册
|
||||
await client.post("/api/v1/auth/register", json=test_user_data)
|
||||
|
||||
# 测试登录
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={
|
||||
"username": test_user_data["username"],
|
||||
"password": test_user_data["password"]
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 200
|
||||
assert "access_token" in data["data"]
|
||||
|
||||
async def test_refresh_token_endpoint(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
test_user_data
|
||||
):
|
||||
"""测试Token刷新端点"""
|
||||
# 先注册并获取tokens
|
||||
register_response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=test_user_data
|
||||
)
|
||||
tokens = register_response.json()["data"]
|
||||
|
||||
# 刷新token
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": tokens["refresh_token"]}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data["data"]
|
||||
assert data["data"]["access_token"] != tokens["access_token"]
|
||||
|
||||
async def test_logout_endpoint(self, client: AsyncClient):
|
||||
"""测试登出端点"""
|
||||
response = await client.post("/api/v1/auth/logout")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "登出成功"
|
||||
|
||||
async def test_reset_password_request(self, client: AsyncClient):
|
||||
"""测试重置密码请求"""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/reset-password",
|
||||
json={"email": "test@example.com"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "如果该邮箱已注册" in data["message"]
|
||||
Reference in New Issue
Block a user