You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

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_changedTrue

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 个集成测试全部通过


后续工作

待补充的测试

  1. API 路由测试

    • 创建 API 端点测试
    • 测试请求/响应格式
    • 测试认证和授权
  2. 性能测试

    • 测试批量操作性能
    • 测试并发会话管理
  3. 边界条件测试

    • 测试极端输入值
    • 测试并发冲突

相关文档


Model Relationship 修复

在执行集成测试时发现 SQLAlchemy relationship 配置错误,因为移除物理外键后,SQLAlchemy 无法自动推断关联关系。

修复内容

修改了以下 Model 文件,为所有 Relationship 添加 primaryjoin 参数:

  1. 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"
  2. 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 无法通过 ForeignKeyForeignKeyConstraint 自动推断关联条件,必须通过 primaryjoin 明确指定。


总结

成功创建并执行了用户服务的完整测试套件:

  1. 单元测试:15/15 通过(使用 Mock 策略)
  2. 集成测试:7/7 通过(真实数据库交互)
  3. Model 修复:修复了 Folder 和 Project 的 Relationship 配置

测试覆盖了所有核心功能和安全性修复,特别是:

  1. 验证了 logout 方法的会话所有权检查(安全漏洞修复)
  2. 验证了 delete_user 方法的级联删除逻辑
  3. 验证了 _create_session 方法的应用层引用完整性验证
  4. 验证了所有业务规则(用户名修改限制、邮箱唯一性等)
  5. 验证了真实数据库环境下的完整交互流程

测试代码质量高,为后续开发提供了可靠的质量保证。