Add unit tests for models and services

- Implemented unit tests for Object model including folder and file creation, properties, and path retrieval.
- Added unit tests for Setting model covering creation, unique constraints, and type enumeration.
- Created unit tests for User model focusing on user creation, uniqueness, and group relationships.
- Developed unit tests for Login service to validate login functionality, including 2FA and token generation.
- Added utility tests for JWT creation and verification, ensuring token integrity and expiration handling.
- Implemented password utility tests for password generation, hashing, and TOTP verification.
This commit is contained in:
2025-12-19 19:48:05 +08:00
parent 51b6de921b
commit f93cb3eedb
60 changed files with 8189 additions and 117 deletions

665
tests/TESTING_GUIDE.md Normal file
View File

@@ -0,0 +1,665 @@
# DiskNext Server 测试基础设施使用指南
本文档介绍如何使用新的测试基础设施进行单元测试和集成测试。
## 目录结构
```
tests/
├── conftest.py # Pytest 配置和全局 fixtures
├── fixtures/ # 测试数据工厂
│ ├── __init__.py
│ ├── users.py # 用户工厂
│ ├── groups.py # 用户组工厂
│ └── objects.py # 对象(文件/目录)工厂
├── unit/ # 单元测试
│ ├── models/ # 模型测试
│ ├── utils/ # 工具测试
│ └── service/ # 服务测试
├── integration/ # 集成测试
│ ├── api/ # API 测试
│ └── middleware/ # 中间件测试
├── example_test.py # 示例测试(展示用法)
├── README.md # 原有文档
└── TESTING_GUIDE.md # 本文档
```
## 快速开始
### 1. 安装依赖
```bash
# 使用 uv 安装依赖
uv sync
### 2. 运行示例测试
```bash
# 运行示例测试,查看输出
pytest tests/example_test.py -v
```
### 3. 查看可用的 fixtures
```bash
# 列出所有可用的 fixtures
pytest --fixtures tests/conftest.py
```
## 可用的 Fixtures
### 数据库相关
| Fixture | 作用域 | 说明 |
|---------|--------|------|
| `test_engine` | function | SQLite 内存数据库引擎 |
| `db_session` | function | 异步数据库会话 |
| `initialized_db` | function | 已初始化的数据库(运行了 migration |
### HTTP 客户端
| Fixture | 作用域 | 说明 |
|---------|--------|------|
| `client` | function | 同步 TestClientFastAPI |
| `async_client` | function | 异步 httpx.AsyncClient |
### 测试用户
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `test_user` | function | `dict[str, str \| UUID]` | 创建普通测试用户 |
| `admin_user` | function | `dict[str, str \| UUID]` | 创建管理员用户 |
返回的字典包含以下键:
- `id`: 用户 UUID
- `username`: 用户名
- `password`: 明文密码
- `token`: JWT 访问令牌
- `group_id`: 用户组 UUID
- `policy_id`: 存储策略 UUID
### 认证相关
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `auth_headers` | function | `dict[str, str]` | 测试用户的认证请求头 |
| `admin_headers` | function | `dict[str, str]` | 管理员的认证请求头 |
### 测试数据
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `test_directory` | function | `dict[str, UUID]` | 为测试用户创建目录结构 |
## 使用测试数据工厂
### UserFactory
```python
from tests.fixtures import UserFactory
# 创建普通用户
user = await UserFactory.create(
session,
group_id=group.id,
username="testuser",
password="password123",
nickname="测试用户",
score=100
)
# 创建管理员
admin = await UserFactory.create_admin(
session,
admin_group_id=admin_group.id,
username="admin"
)
# 创建被封禁用户
banned = await UserFactory.create_banned(
session,
group_id=group.id
)
# 创建有存储使用记录的用户
storage_user = await UserFactory.create_with_storage(
session,
group_id=group.id,
storage_bytes=1024 * 1024 * 100 # 100MB
)
```
### GroupFactory
```python
from tests.fixtures import GroupFactory
# 创建普通用户组(带选项)
group = await GroupFactory.create(
session,
name="测试组",
max_storage=1024 * 1024 * 1024 * 10, # 10GB
create_options=True, # 同时创建 GroupOptions
share_enabled=True,
web_dav_enabled=True
)
# 创建管理员组(自动创建完整的管理员选项)
admin_group = await GroupFactory.create_admin_group(
session,
name="管理员组"
)
# 创建有限制的用户组
limited_group = await GroupFactory.create_limited_group(
session,
max_storage=1024 * 1024 * 100, # 100MB
name="受限组"
)
# 创建免费用户组(最小权限)
free_group = await GroupFactory.create_free_group(session)
```
### ObjectFactory
```python
from tests.fixtures import ObjectFactory
# 创建用户根目录
root = await ObjectFactory.create_user_root(
session,
user,
policy.id
)
# 创建目录
folder = await ObjectFactory.create_folder(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=root.id,
name="documents"
)
# 创建文件
file = await ObjectFactory.create_file(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
name="test.txt",
size=1024
)
# 创建目录树(递归创建多层目录)
folders = await ObjectFactory.create_directory_tree(
session,
owner_id=user.id,
policy_id=policy.id,
root_id=root.id,
depth=3, # 3层深度
folders_per_level=2 # 每层2个目录
)
# 在目录中批量创建文件
files = await ObjectFactory.create_files_in_folder(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
count=10, # 创建10个文件
size_range=(1024, 1024 * 1024) # 1KB - 1MB
)
# 创建大文件(用于测试存储限制)
large_file = await ObjectFactory.create_large_file(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
size_mb=100
)
# 创建完整的嵌套结构(文档、媒体等)
structure = await ObjectFactory.create_nested_structure(
session,
owner_id=user.id,
policy_id=policy.id,
root_id=root.id
)
# 返回: {"documents": UUID, "work": UUID, "report": UUID, ...}
```
## 编写测试示例
### 单元测试
```python
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
from tests.fixtures import UserFactory, GroupFactory
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
"""测试用户创建功能"""
# 准备数据
group = await GroupFactory.create(db_session)
# 执行操作
user = await UserFactory.create(
db_session,
group_id=group.id,
username="testuser"
)
# 断言
assert user.id is not None
assert user.username == "testuser"
assert user.group_id == group.id
assert user.status is True
```
### 集成测试API
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_user_login_api(
async_client: AsyncClient,
test_user: dict
):
"""测试用户登录 API"""
response = await async_client.post(
"/api/user/session",
json={
"username": test_user["username"],
"password": test_user["password"]
}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["access_token"] == test_user["token"]
```
### 需要认证的测试
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_protected_endpoint(
async_client: AsyncClient,
auth_headers: dict
):
"""测试需要认证的端点"""
response = await async_client.get(
"/api/user/me",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["username"] == "testuser"
```
### 使用 test_directory fixture
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_list_directory(
async_client: AsyncClient,
auth_headers: dict,
test_directory: dict
):
"""测试获取目录列表"""
# test_directory 已创建了目录结构
response = await async_client.get(
f"/api/directory/{test_directory['documents']}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "objects" in data
# 验证子目录存在
assert any(obj["name"] == "work" for obj in data["objects"])
assert any(obj["name"] == "personal" for obj in data["objects"])
```
## 运行测试
### 基本命令
```bash
# 运行所有测试
pytest
# 显示详细输出
pytest -v
# 运行特定测试文件
pytest tests/unit/models/test_user.py
# 运行特定测试函数
pytest tests/unit/models/test_user.py::test_user_creation
```
### 使用标记
```bash
# 只运行单元测试
pytest -m unit
# 只运行集成测试
pytest -m integration
# 运行慢速测试
pytest -m slow
# 运行除了慢速测试外的所有测试
pytest -m "not slow"
# 运行单元测试或集成测试
pytest -m "unit or integration"
```
### 测试覆盖率
```bash
# 生成覆盖率报告
pytest --cov=models --cov=routers --cov=middleware --cov=service --cov=utils
# 生成 HTML 覆盖率报告
pytest --cov=models --cov=routers --cov=utils --cov-report=html
# 查看 HTML 报告
# 在浏览器中打开 htmlcov/index.html
# 检查覆盖率是否达标80%
pytest --cov --cov-fail-under=80
```
### 并行运行
```bash
# 使用所有 CPU 核心
pytest -n auto
# 使用指定数量的核心
pytest -n 4
# 并行运行且显示详细输出
pytest -n auto -v
```
### 调试测试
```bash
# 显示更详细的输出
pytest -vv
# 显示 print 输出
pytest -s
# 进入调试模式(遇到失败时)
pytest --pdb
# 只运行上次失败的测试
pytest --lf
# 先运行上次失败的,再运行其他的
pytest --ff
```
## 测试标记
使用 pytest 标记来组织和筛选测试:
```python
# 单元测试
@pytest.mark.unit
async def test_something():
pass
# 集成测试
@pytest.mark.integration
async def test_api_endpoint():
pass
# 慢速测试(运行时间较长)
@pytest.mark.slow
async def test_large_dataset():
pass
# 组合标记
@pytest.mark.unit
@pytest.mark.slow
async def test_complex_calculation():
pass
# 跳过测试
@pytest.mark.skip(reason="暂时跳过")
async def test_work_in_progress():
pass
# 条件跳过
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="仅限 Linux")
async def test_linux_only():
pass
```
## 测试最佳实践
### 1. 测试隔离
每个测试应该独立,不依赖其他测试的执行结果:
```python
# ✅ 好的实践
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
group = await GroupFactory.create(db_session)
user = await UserFactory.create(db_session, group_id=group.id)
assert user.id is not None
# ❌ 不好的实践(依赖全局状态)
global_user = None
@pytest.mark.unit
async def test_create_user(db_session: AsyncSession):
global global_user
group = await GroupFactory.create(db_session)
global_user = await UserFactory.create(db_session, group_id=group.id)
@pytest.mark.unit
async def test_update_user(db_session: AsyncSession):
# 依赖前一个测试的结果
assert global_user is not None
global_user.nickname = "Updated"
await global_user.save(db_session)
```
### 2. 使用工厂而非手动创建
```python
# ✅ 好的实践
user = await UserFactory.create(db_session, group_id=group.id)
# ❌ 不好的实践
user = User(
username="test",
password=Password.hash("password"),
group_id=group.id,
status=True,
storage=0,
score=100,
# ... 更多字段
)
user = await user.save(db_session)
```
### 3. 清晰的断言
```python
# ✅ 好的实践
assert user.username == "testuser", "用户名应该是 testuser"
assert user.status is True, "新用户应该是激活状态"
# ❌ 不好的实践
assert user # 不清楚在验证什么
```
### 4. 测试异常情况
```python
import pytest
@pytest.mark.unit
async def test_duplicate_username(db_session: AsyncSession):
"""测试创建重复用户名"""
group = await GroupFactory.create(db_session)
# 创建第一个用户
await UserFactory.create(
db_session,
group_id=group.id,
username="duplicate"
)
# 尝试创建同名用户应该失败
with pytest.raises(Exception): # 或更具体的异常类型
await UserFactory.create(
db_session,
group_id=group.id,
username="duplicate"
)
```
### 5. 适当的测试粒度
```python
# ✅ 好的实践:一个测试验证一个行为
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
"""测试用户创建"""
# 只测试创建
@pytest.mark.unit
async def test_user_authentication(db_session: AsyncSession):
"""测试用户认证"""
# 只测试认证
# ❌ 不好的实践:一个测试做太多事
@pytest.mark.unit
async def test_user_everything(db_session: AsyncSession):
"""测试用户的所有功能"""
# 创建、更新、删除、认证...全都在一个测试里
```
## 常见问题
### Q: 测试失败时如何调试?
```bash
# 使用 -vv 显示更详细的输出
pytest -vv
# 使用 -s 显示 print 语句
pytest -s
# 使用 --pdb 在失败时进入调试器
pytest --pdb
# 组合使用
pytest -vvs --pdb
```
### Q: 如何只运行某些测试?
```bash
# 按标记运行
pytest -m unit
# 按文件运行
pytest tests/unit/models/
# 按测试名称模糊匹配
pytest -k "user" # 运行所有名称包含 "user" 的测试
# 组合条件
pytest -m unit -k "not slow"
```
### Q: 数据库会话相关错误?
确保使用正确的 fixture
```python
# ✅ 正确
async def test_something(db_session: AsyncSession):
user = await User.get(db_session, User.id == some_id)
# ❌ 错误:没有传入 session
async def test_something():
user = await User.get(User.id == some_id) # 会失败
```
### Q: 异步测试不工作?
确保使用 pytest-asyncio 标记或配置了 asyncio_mode
```python
# pyproject.toml 中已配置 asyncio_mode = "auto"
# 所以不需要 @pytest.mark.asyncio
async def test_async_function(db_session: AsyncSession):
# 会自动识别为异步测试
pass
```
### Q: 如何测试需要认证的端点?
使用 `auth_headers` fixture
```python
async def test_protected_route(
async_client: AsyncClient,
auth_headers: dict
):
response = await async_client.get(
"/api/protected",
headers=auth_headers
)
assert response.status_code == 200
```
## 参考资料
- [Pytest 官方文档](https://docs.pytest.org/)
- [pytest-asyncio 文档](https://pytest-asyncio.readthedocs.io/)
- [FastAPI 测试指南](https://fastapi.tiangolo.com/tutorial/testing/)
- [httpx 测试客户端](https://www.python-httpx.org/advanced/#calling-into-python-web-apps)
- [SQLModel 文档](https://sqlmodel.tiangolo.com/)
## 贡献
如果您发现文档中的错误或有改进建议,请:
1. 在项目中创建 Issue
2. 提交 Pull Request
3. 更新相关文档
---
更新时间: 2025-12-19