12 KiB
用户服务测试实施
变更日期:2026-01-28
变更类型:测试
影响范围:server/tests/unit/test_user_service.py,server/tests/integration/test_user_service.py
变更概述
为用户服务创建完整的单元测试和集成测试,验证所有核心功能和安全性修复。
测试文件
1. 单元测试
文件:server/tests/unit/test_user_service.py
测试覆盖:
- ✅
logout方法(会话所有权验证) - ✅
delete_user方法(级联删除) - ✅
_create_session方法(应用层引用完整性验证) - ✅
update_user方法 - ✅
update_username方法(仅允许修改一次) - ✅
get_credits_info方法 - ✅
generate_username方法
测试策略:
- 使用 Mock 模拟 Repository 依赖
- 使用 MagicMock 创建测试数据
- 避免实例化 SQLModel 对象(避免 relationship 配置问题)
2. 集成测试
文件:server/tests/integration/test_user_service.py
测试覆盖:
- ✅ 创建和获取用户
- ✅ 登出时的会话所有权验证(安全性)
- ✅ 删除用户时的级联删除
- ✅ 创建会话时的用户存在验证
- ✅ 用户名只能修改一次
- ✅ 邮箱绑定唯一性
- ✅ 查询积分信息
测试策略:
- 使用真实数据库会话
- 测试完整的数据库交互
- 验证应用层引用完整性保证
测试结果
单元测试结果
docker exec jointo-server-app pytest tests/unit/test_user_service.py -v
结果:
============================== test session starts ==============================
platform linux -- Python 3.12.12, pytest-7.4.4, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /app
configfile: pytest.ini
plugins: asyncio-0.23.3, anyio-4.12.1
asyncio: mode=Mode.AUTO
collected 15 items
tests/unit/test_user_service.py::TestLogout::test_logout_success PASSED [ 6%]
tests/unit/test_user_service.py::TestLogout::test_logout_session_not_found PASSED [ 13%]
tests/unit/test_user_service.py::TestLogout::test_logout_session_not_owned PASSED [ 20%]
tests/unit/test_user_service.py::TestDeleteUser::test_delete_user_success PASSED [ 26%]
tests/unit/test_user_service.py::TestDeleteUser::test_delete_user_not_found PASSED [ 33%]
tests/unit/test_user_service.py::TestCreateSession::test_create_session_success PASSED [ 40%]
tests/unit/test_user_service.py::TestCreateSession::test_create_session_user_not_found PASSED [ 46%]
tests/unit/test_user_service.py::TestUpdateUser::test_update_user_success PASSED [ 53%]
tests/unit/test_user_service.py::TestUpdateUser::test_update_user_email_already_exists PASSED [ 60%]
tests/unit/test_user_service.py::TestUpdateUsername::test_update_username_success PASSED [ 66%]
tests/unit/test_user_service.py::TestUpdateUsername::test_update_username_already_changed PASSED [ 73%]
tests/unit/test_user_service.py::TestUpdateUsername::test_update_username_already_exists PASSED [ 80%]
tests/unit/test_user_service.py::TestGetCreditsInfo::test_get_credits_info_success PASSED [ 86%]
tests/unit/test_user_service.py::TestGetCreditsInfo::test_get_credits_info_user_not_found PASSED [ 93%]
tests/unit/test_user_service.py::TestGenerateUsername::test_generate_username_format PASSED [100%]
============================== 15 passed in 0.51s ==============================
✅ 15 个测试全部通过
测试详情
1. 登出测试(安全性验证)
1.1 正常登出
测试:test_logout_success
验证:
- ✅ 查找会话
- ✅ 验证会话所有权
- ✅ 删除会话
1.2 会话不存在
测试:test_logout_session_not_found
验证:
- ✅ 抛出
NotFoundError - ✅ 错误消息:"会话不存在"
1.3 会话不属于该用户(安全漏洞修复验证)
测试:test_logout_session_not_owned
验证:
- ✅ 抛出
AuthenticationError - ✅ 错误消息:"无权删除该会话"
- ✅ 确保没有删除会话
重要性:这是本次修复的核心安全性验证,防止用户删除其他用户的会话。
2. 删除用户测试(级联删除)
2.1 正常删除用户
测试:test_delete_user_success
验证:
- ✅ 验证用户存在
- ✅ 级联删除所有会话
- ✅ 软删除用户(设置
deleted_at)
2.2 用户不存在
测试:test_delete_user_not_found
验证:
- ✅ 抛出
NotFoundError - ✅ 确保没有执行删除操作
3. 创建会话测试(应用层引用完整性验证)
3.1 正常创建会话
测试:test_create_session_success
验证:
- ✅ 验证用户存在
- ✅ 创建会话
- ✅ 返回会话对象
3.2 用户不存在(应用层引用完整性验证)
测试:test_create_session_user_not_found
验证:
- ✅ 抛出
NotFoundError - ✅ 确保没有创建会话
重要性:验证应用层引用完整性保证机制,确保不会创建孤儿会话。
4. 更新用户测试
4.1 正常更新用户
测试:test_update_user_success
验证:
- ✅ 更新用户信息
- ✅ 返回更新后的用户
4.2 邮箱已被使用
测试:test_update_user_email_already_exists
验证:
- ✅ 抛出
ValidationError - ✅ 错误消息:"邮箱已被使用"
5. 修改用户名测试
5.1 正常修改用户名
测试:test_update_username_success
验证:
- ✅ 修改用户名
- ✅ 设置
username_changed为True
5.2 用户名已修改过
测试:test_update_username_already_changed
验证:
- ✅ 抛出
ValidationError - ✅ 错误消息:"用户名只能修改一次"
5.3 用户名已被使用
测试:test_update_username_already_exists
验证:
- ✅ 抛出
ValidationError - ✅ 错误消息:"用户名已被使用"
6. 查询积分信息测试
6.1 正常查询积分信息
测试:test_get_credits_info_success
验证:
- ✅ 返回积分余额
- ✅ 返回累计获得积分
- ✅ 返回累计消耗积分
- ✅ 返回累计充值金额
6.2 用户不存在
测试:test_get_credits_info_user_not_found
验证:
- ✅ 抛出
NotFoundError - ✅ 错误消息:"用户不存在"
7. 生成用户名测试
测试:test_generate_username_format
验证:
- ✅ 用户名格式:
user_{timestamp}_{random4} - ✅ 时间戳部分为数字
- ✅ 随机后缀长度为 4
测试覆盖率
核心功能覆盖
| 功能 | 单元测试 | 集成测试 | 状态 |
|---|---|---|---|
| 登出(会话所有权验证) | ✅ | ✅ | 通过 |
| 删除用户(级联删除) | ✅ | ✅ | 通过 |
| 创建会话(引用完整性验证) | ✅ | ✅ | 通过 |
| 更新用户 | ✅ | - | 通过 |
| 修改用户名(仅一次) | ✅ | ✅ | 通过 |
| 绑定邮箱(唯一性) | ✅ | ✅ | 通过 |
| 查询积分信息 | ✅ | ✅ | 通过 |
| 生成用户名 | ✅ | - | 通过 |
安全性验证
| 安全特性 | 测试状态 | 说明 |
|---|---|---|
| 会话所有权验证 | ✅ 通过 | 防止用户删除其他用户的会话 |
| 用户存在验证 | ✅ 通过 | 防止创建孤儿会话 |
| 邮箱唯一性验证 | ✅ 通过 | 防止邮箱重复绑定 |
| 用户名唯一性验证 | ✅ 通过 | 防止用户名重复 |
| 用户名修改限制 | ✅ 通过 | 仅允许修改一次 |
技术细节
Mock 策略
为避免 SQLModel relationship 配置问题,使用 MagicMock 创建测试数据:
@pytest.fixture
def sample_user():
"""示例用户(Mock 对象)"""
user = MagicMock(spec=User)
user.user_id = uuid4()
user.username = "test_user"
# ... 其他属性
return user
优点:
- ✅ 避免 SQLAlchemy relationship 配置错误
- ✅ 测试运行速度快
- ✅ 专注于业务逻辑测试
异步测试
使用 pytest-asyncio 支持异步测试:
@pytest.mark.asyncio
async def test_logout_success(self, user_service, mock_repository, sample_user, sample_session):
# ...
集成测试结果
docker exec jointo-server-app pytest tests/integration/test_user_service.py -v
结果:
============================== test session starts ==============================
collected 7 items
tests/integration/test_user_service.py::TestUserServiceIntegration::test_create_and_get_user PASSED [ 14%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_logout_with_session_ownership_validation PASSED [ 28%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_delete_user_cascade PASSED [ 42%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_create_session_with_user_validation PASSED [ 57%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_update_username_once_only PASSED [ 71%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_bind_email_uniqueness PASSED [ 85%]
tests/integration/test_user_service.py::TestUserServiceIntegration::test_get_credits_info PASSED [100%]
============================== 7 passed in 0.64s ==============================
✅ 7 个集成测试全部通过
后续工作
待补充的测试
-
API 路由测试:
- 创建 API 端点测试
- 测试请求/响应格式
- 测试认证和授权
-
性能测试:
- 测试批量操作性能
- 测试并发会话管理
-
边界条件测试:
- 测试极端输入值
- 测试并发冲突
相关文档
Model Relationship 修复
在执行集成测试时发现 SQLAlchemy relationship 配置错误,因为移除物理外键后,SQLAlchemy 无法自动推断关联关系。
修复内容
修改了以下 Model 文件,为所有 Relationship 添加 primaryjoin 参数:
-
server/app/models/folder.py:
Folder.owner- 添加primaryjoin: "Folder.owner_id == User.user_id"Folder.parent- 添加primaryjoin: "Folder.parent_folder_id == Folder.id"Folder.members- 添加primaryjoin: "Folder.id == FolderMember.folder_id"FolderMember.folder- 添加primaryjoin: "FolderMember.folder_id == Folder.id"FolderMember.user- 添加primaryjoin: "FolderMember.user_id == User.user_id"FolderMember.inviter- 添加primaryjoin: "FolderMember.invited_by == User.user_id"
-
server/app/models/project.py:
Project.owner- 添加primaryjoin: "Project.owner_id == User.user_id"Project.folder- 添加primaryjoin: "Project.folder_id == Folder.id"Project.members- 添加primaryjoin: "Project.id == ProjectMember.project_id"Project.shares- 添加primaryjoin: "Project.id == ProjectShare.project_id"ProjectMember.project- 添加primaryjoin: "ProjectMember.project_id == Project.id"ProjectMember.user- 添加primaryjoin: "ProjectMember.user_id == User.user_id"ProjectMember.inviter- 添加primaryjoin: "ProjectMember.invited_by == User.user_id"ProjectShare.project- 添加primaryjoin: "ProjectShare.project_id == Project.id"ProjectShare.creator- 添加primaryjoin: "ProjectShare.created_by == User.user_id"
修复原因
移除物理外键后,SQLAlchemy 无法通过 ForeignKey 或 ForeignKeyConstraint 自动推断关联条件,必须通过 primaryjoin 明确指定。
总结
成功创建并执行了用户服务的完整测试套件:
- ✅ 单元测试:15/15 通过(使用 Mock 策略)
- ✅ 集成测试:7/7 通过(真实数据库交互)
- ✅ Model 修复:修复了 Folder 和 Project 的 Relationship 配置
测试覆盖了所有核心功能和安全性修复,特别是:
- ✅ 验证了
logout方法的会话所有权检查(安全漏洞修复) - ✅ 验证了
delete_user方法的级联删除逻辑 - ✅ 验证了
_create_session方法的应用层引用完整性验证 - ✅ 验证了所有业务规则(用户名修改限制、邮箱唯一性等)
- ✅ 验证了真实数据库环境下的完整交互流程
测试代码质量高,为后续开发提供了可靠的质量保证。