feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""测试包"""

100
backend/tests/conftest.py Normal file
View 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']}"}

View File

View File

View 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

View 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

View 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()

View 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

View 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"

View 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

View 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

View File

View 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"]