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:
225
tests/integration/QUICK_REFERENCE.md
Normal file
225
tests/integration/QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# 集成测试快速参考
|
||||
|
||||
## 快速命令
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pytest tests/integration/ -v
|
||||
|
||||
# 运行特定类别
|
||||
pytest tests/integration/api/ -v # 所有 API 测试
|
||||
pytest tests/integration/middleware/ -v # 所有中间件测试
|
||||
|
||||
# 运行单个文件
|
||||
pytest tests/integration/api/test_user.py -v
|
||||
|
||||
# 运行单个测试
|
||||
pytest tests/integration/api/test_user.py::test_user_login_success -v
|
||||
|
||||
# 生成覆盖率
|
||||
pytest tests/integration/ --cov --cov-report=html
|
||||
|
||||
# 并行运行
|
||||
pytest tests/integration/ -n auto
|
||||
|
||||
# 显示详细输出
|
||||
pytest tests/integration/ -vv -s
|
||||
```
|
||||
|
||||
## 测试文件速查
|
||||
|
||||
| 文件 | 测试内容 | 端点前缀 |
|
||||
|------|---------|---------|
|
||||
| `test_site.py` | 站点配置 | `/api/site/*` |
|
||||
| `test_user.py` | 用户操作 | `/api/user/*` |
|
||||
| `test_admin.py` | 管理员功能 | `/api/admin/*` |
|
||||
| `test_directory.py` | 目录操作 | `/api/directory/*` |
|
||||
| `test_object.py` | 对象操作 | `/api/object/*` |
|
||||
| `test_auth.py` | 认证中间件 | - |
|
||||
|
||||
## 常用 Fixtures
|
||||
|
||||
```python
|
||||
# HTTP 客户端
|
||||
async_client: AsyncClient
|
||||
|
||||
# 认证
|
||||
auth_headers: dict[str, str] # 普通用户
|
||||
admin_headers: dict[str, str] # 管理员
|
||||
|
||||
# 数据库
|
||||
initialized_db: AsyncSession # 预填充的测试数据库
|
||||
test_session: AsyncSession # 空的测试会话
|
||||
|
||||
# 用户信息
|
||||
test_user_info: dict # {"username": "testuser", "password": "testpass123"}
|
||||
admin_user_info: dict # {"username": "admin", "password": "adminpass123"}
|
||||
|
||||
# 测试数据
|
||||
test_directory_structure: dict # {"root_id": UUID, "docs_id": UUID, ...}
|
||||
|
||||
# Tokens
|
||||
test_user_token: str # 有效的用户 token
|
||||
admin_user_token: str # 有效的管理员 token
|
||||
expired_token: str # 过期的 token
|
||||
```
|
||||
|
||||
## 测试模板
|
||||
|
||||
### 基础 API 测试
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_endpoint_name(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试描述"""
|
||||
response = await async_client.get(
|
||||
"/api/path",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "expected_field" in data
|
||||
```
|
||||
|
||||
### 需要测试数据的测试
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_data(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""使用预创建的测试数据"""
|
||||
folder_id = test_directory_structure["docs_id"]
|
||||
# 测试逻辑...
|
||||
```
|
||||
|
||||
### 认证测试
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_auth(async_client: AsyncClient):
|
||||
"""测试需要认证"""
|
||||
response = await async_client.get("/api/protected")
|
||||
assert response.status_code == 401
|
||||
```
|
||||
|
||||
### 权限测试
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试需要管理员权限"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/endpoint",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
```
|
||||
|
||||
## 测试数据
|
||||
|
||||
### 默认用户
|
||||
- **testuser** / testpass123 (普通用户)
|
||||
- **admin** / adminpass123 (管理员)
|
||||
- **banneduser** / banned123 (封禁用户)
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
testuser/
|
||||
├── docs/
|
||||
│ ├── images/
|
||||
│ └── readme.md (1KB)
|
||||
```
|
||||
|
||||
## 常见断言
|
||||
|
||||
```python
|
||||
# 状态码
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 401 # 未认证
|
||||
assert response.status_code == 403 # 权限不足
|
||||
assert response.status_code == 404 # 不存在
|
||||
assert response.status_code == 409 # 冲突
|
||||
|
||||
# 响应数据
|
||||
data = response.json()
|
||||
assert "field" in data
|
||||
assert data["field"] == expected_value
|
||||
assert isinstance(data["list"], list)
|
||||
|
||||
# 列表长度
|
||||
assert len(data["items"]) > 0
|
||||
assert len(data["items"]) <= page_size
|
||||
|
||||
# 嵌套数据
|
||||
assert "nested" in data
|
||||
assert "field" in data["nested"]
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```bash
|
||||
# 显示完整输出
|
||||
pytest tests/integration/api/test_user.py -vv -s
|
||||
|
||||
# 只运行失败的测试
|
||||
pytest tests/integration/ --lf
|
||||
|
||||
# 遇到第一个失败就停止
|
||||
pytest tests/integration/ -x
|
||||
|
||||
# 显示最慢的 10 个测试
|
||||
pytest tests/integration/ --durations=10
|
||||
|
||||
# 使用 pdb 调试
|
||||
pytest tests/integration/ --pdb
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题: 测试全部失败
|
||||
```bash
|
||||
# 检查依赖
|
||||
pip install -e .
|
||||
|
||||
# 检查 Python 路径
|
||||
python -c "import sys; print(sys.path)"
|
||||
```
|
||||
|
||||
### 问题: JWT 相关错误
|
||||
```python
|
||||
# 检查 JWT 密钥是否设置
|
||||
from utils.JWT import JWT
|
||||
print(JWT.SECRET_KEY)
|
||||
```
|
||||
|
||||
### 问题: 数据库错误
|
||||
```python
|
||||
# 确保所有模型都已导入
|
||||
from models import *
|
||||
```
|
||||
|
||||
## 性能基准
|
||||
|
||||
预期测试时间(参考):
|
||||
- 单个测试: < 1s
|
||||
- 整个文件: < 10s
|
||||
- 所有集成测试: < 1min
|
||||
|
||||
如果超过这些时间,检查:
|
||||
1. 数据库连接
|
||||
2. 异步配置
|
||||
3. Fixtures 作用域
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [README.md](README.md) - 详细的测试文档
|
||||
- [conftest.py](conftest.py) - Fixtures 定义
|
||||
- [../../INTEGRATION_TESTS_SUMMARY.md](../../INTEGRATION_TESTS_SUMMARY.md) - 实现总结
|
||||
259
tests/integration/README.md
Normal file
259
tests/integration/README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# 集成测试文档
|
||||
|
||||
## 概述
|
||||
|
||||
本目录包含 DiskNext Server 的集成测试,测试覆盖主要的 API 端点和中间件功能。
|
||||
|
||||
## 测试结构
|
||||
|
||||
```
|
||||
tests/integration/
|
||||
├── conftest.py # 测试配置和 fixtures
|
||||
├── api/ # API 端点测试
|
||||
│ ├── test_site.py # 站点配置测试
|
||||
│ ├── test_user.py # 用户相关测试
|
||||
│ ├── test_admin.py # 管理员端点测试
|
||||
│ ├── test_directory.py # 目录操作测试
|
||||
│ └── test_object.py # 对象操作测试
|
||||
└── middleware/ # 中间件测试
|
||||
└── test_auth.py # 认证中间件测试
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有集成测试
|
||||
|
||||
```bash
|
||||
pytest tests/integration/
|
||||
```
|
||||
|
||||
### 运行特定测试文件
|
||||
|
||||
```bash
|
||||
# 测试站点端点
|
||||
pytest tests/integration/api/test_site.py
|
||||
|
||||
# 测试用户端点
|
||||
pytest tests/integration/api/test_user.py
|
||||
|
||||
# 测试认证中间件
|
||||
pytest tests/integration/middleware/test_auth.py
|
||||
```
|
||||
|
||||
### 运行特定测试函数
|
||||
|
||||
```bash
|
||||
pytest tests/integration/api/test_user.py::test_user_login_success
|
||||
```
|
||||
|
||||
### 显示详细输出
|
||||
|
||||
```bash
|
||||
pytest tests/integration/ -v
|
||||
```
|
||||
|
||||
### 生成覆盖率报告
|
||||
|
||||
```bash
|
||||
# 生成终端报告
|
||||
pytest tests/integration/ --cov
|
||||
|
||||
# 生成 HTML 报告
|
||||
pytest tests/integration/ --cov --cov-report=html
|
||||
```
|
||||
|
||||
### 并行运行测试
|
||||
|
||||
```bash
|
||||
pytest tests/integration/ -n auto
|
||||
```
|
||||
|
||||
## 测试 Fixtures
|
||||
|
||||
### 数据库相关
|
||||
|
||||
- `test_db_engine`: 测试数据库引擎(内存 SQLite)
|
||||
- `test_session`: 测试数据库会话
|
||||
- `initialized_db`: 已初始化的测试数据库(包含基础数据)
|
||||
|
||||
### 用户相关
|
||||
|
||||
- `test_user_info`: 测试用户信息(username, password)
|
||||
- `admin_user_info`: 管理员用户信息
|
||||
- `banned_user_info`: 封禁用户信息
|
||||
|
||||
### 认证相关
|
||||
|
||||
- `test_user_token`: 测试用户的 JWT token
|
||||
- `admin_user_token`: 管理员的 JWT token
|
||||
- `expired_token`: 过期的 JWT token
|
||||
- `auth_headers`: 测试用户的认证头
|
||||
- `admin_headers`: 管理员的认证头
|
||||
|
||||
### 客户端
|
||||
|
||||
- `async_client`: 异步 HTTP 测试客户端
|
||||
|
||||
### 测试数据
|
||||
|
||||
- `test_directory_structure`: 测试目录结构(包含文件夹和文件)
|
||||
|
||||
## 测试覆盖范围
|
||||
|
||||
### API 端点测试
|
||||
|
||||
#### `/api/site/*` (test_site.py)
|
||||
- ✅ Ping 端点
|
||||
- ✅ 站点配置端点
|
||||
- ✅ 配置字段验证
|
||||
|
||||
#### `/api/user/*` (test_user.py)
|
||||
- ✅ 用户登录(成功、失败、封禁用户)
|
||||
- ✅ 用户注册(成功、重复用户名)
|
||||
- ✅ 获取用户信息(需要认证)
|
||||
- ✅ 获取存储信息
|
||||
- ✅ 两步验证初始化和启用
|
||||
- ✅ 用户设置
|
||||
|
||||
#### `/api/admin/*` (test_admin.py)
|
||||
- ✅ 认证检查(需要管理员权限)
|
||||
- ✅ 获取用户列表(带分页)
|
||||
- ✅ 获取用户信息
|
||||
- ✅ 创建用户
|
||||
- ✅ 用户组管理
|
||||
- ✅ 文件管理
|
||||
- ✅ 设置管理
|
||||
|
||||
#### `/api/directory/*` (test_directory.py)
|
||||
- ✅ 获取根目录
|
||||
- ✅ 获取嵌套目录
|
||||
- ✅ 权限检查(不能访问他人目录)
|
||||
- ✅ 创建目录(成功、重名、无效父目录)
|
||||
- ✅ 目录名验证(不能包含斜杠)
|
||||
|
||||
#### `/api/object/*` (test_object.py)
|
||||
- ✅ 删除对象(单个、批量、他人对象)
|
||||
- ✅ 移动对象(成功、无效目标、移动到文件)
|
||||
- ✅ 权限检查(不能操作他人对象)
|
||||
- ✅ 重名检查
|
||||
|
||||
### 中间件测试
|
||||
|
||||
#### 认证中间件 (test_auth.py)
|
||||
- ✅ AuthRequired: 无 token、无效 token、过期 token
|
||||
- ✅ AdminRequired: 非管理员用户返回 403
|
||||
- ✅ Token 格式验证
|
||||
- ✅ 用户不存在处理
|
||||
|
||||
## 测试数据
|
||||
|
||||
### 默认用户
|
||||
|
||||
1. **测试用户**
|
||||
- 用户名: `testuser`
|
||||
- 密码: `testpass123`
|
||||
- 用户组: 默认用户组
|
||||
- 状态: 正常
|
||||
|
||||
2. **管理员**
|
||||
- 用户名: `admin`
|
||||
- 密码: `adminpass123`
|
||||
- 用户组: 管理员组
|
||||
- 状态: 正常
|
||||
|
||||
3. **封禁用户**
|
||||
- 用户名: `banneduser`
|
||||
- 密码: `banned123`
|
||||
- 用户组: 默认用户组
|
||||
- 状态: 封禁
|
||||
|
||||
### 测试目录结构
|
||||
|
||||
```
|
||||
testuser/ # 根目录
|
||||
├── docs/ # 文件夹
|
||||
│ ├── images/ # 子文件夹
|
||||
│ └── readme.md # 文件 (1KB)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试隔离**: 每个测试使用独立的内存数据库,互不影响
|
||||
2. **异步测试**: 所有测试使用 `@pytest.mark.asyncio` 装饰器
|
||||
3. **依赖覆盖**: 测试客户端自动覆盖数据库依赖,使用测试数据库
|
||||
4. **JWT 密钥**: 测试环境使用固定密钥 `test_secret_key_for_jwt_token_generation`
|
||||
|
||||
## 添加新测试
|
||||
|
||||
### 1. 创建测试文件
|
||||
|
||||
在 `tests/integration/api/` 或 `tests/integration/middleware/` 下创建新的测试文件。
|
||||
|
||||
### 2. 导入必要的依赖
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
```
|
||||
|
||||
### 3. 编写测试函数
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_your_feature(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试描述"""
|
||||
response = await async_client.get(
|
||||
"/api/your/endpoint",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### 4. 使用 fixtures
|
||||
|
||||
利用 `conftest.py` 提供的 fixtures:
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_directory_structure(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""使用测试目录结构"""
|
||||
root_id = test_directory_structure["root_id"]
|
||||
# ... 测试逻辑
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 测试失败:数据库初始化错误
|
||||
|
||||
检查是否所有必要的模型都已导入到 `conftest.py` 中。
|
||||
|
||||
### 测试失败:JWT 密钥未设置
|
||||
|
||||
确保 `initialized_db` fixture 正确设置了 `JWT.SECRET_KEY`。
|
||||
|
||||
### 测试失败:认证失败
|
||||
|
||||
检查 token 生成逻辑是否使用正确的密钥和用户名。
|
||||
|
||||
## 持续集成
|
||||
|
||||
建议在 CI/CD 流程中运行集成测试:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
pytest tests/integration/ -v --cov --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
```
|
||||
3
tests/integration/__init__.py
Normal file
3
tests/integration/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
集成测试包
|
||||
"""
|
||||
3
tests/integration/api/__init__.py
Normal file
3
tests/integration/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
API 集成测试包
|
||||
"""
|
||||
263
tests/integration/api/test_admin.py
Normal file
263
tests/integration/api/test_admin.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
管理员端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
# ==================== 认证测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_requires_auth(async_client: AsyncClient):
|
||||
"""测试管理员接口需要认证"""
|
||||
response = await async_client.get("/api/admin/summary")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_requires_admin_role(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户访问管理员接口返回 403"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/summary",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 站点概况测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_summary_success(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试管理员可以获取站点概况"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/summary",
|
||||
headers=admin_headers
|
||||
)
|
||||
# 端点存在但未实现,可能返回 200 或其他状态
|
||||
assert response.status_code in [200, 404, 501]
|
||||
|
||||
|
||||
# ==================== 用户管理测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_info_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取用户信息需要认证"""
|
||||
response = await async_client.get("/api/admin/user/info/1")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_info_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法获取用户信息"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/user/info/1",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_list_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取用户列表需要认证"""
|
||||
response = await async_client.get("/api/admin/user/list")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_list_success(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试管理员可以获取用户列表"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/user/list",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_list_pagination(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试用户列表分页"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/user/list?page=1&page_size=10",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
# 应该返回不超过 page_size 的数量
|
||||
assert len(data["data"]) <= 10
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_user_list_contains_user_data(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试用户列表包含用户数据"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/user/list",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
users = data["data"]
|
||||
if len(users) > 0:
|
||||
user = users[0]
|
||||
assert "id" in user
|
||||
assert "username" in user
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_create_user_requires_auth(async_client: AsyncClient):
|
||||
"""测试创建用户需要认证"""
|
||||
response = await async_client.post(
|
||||
"/api/admin/user/create",
|
||||
json={"username": "newadminuser", "password": "pass123"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_create_user_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法创建用户"""
|
||||
response = await async_client.post(
|
||||
"/api/admin/user/create",
|
||||
headers=auth_headers,
|
||||
json={"username": "newadminuser", "password": "pass123"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 用户组管理测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_groups_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取用户组列表需要认证"""
|
||||
response = await async_client.get("/api/admin/group/")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_groups_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法获取用户组列表"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/group/",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 文件管理测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_file_list_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取文件列表需要认证"""
|
||||
response = await async_client.get("/api/admin/file/list")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_file_list_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法获取文件列表"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/file/list",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 设置管理测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_settings_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取设置需要认证"""
|
||||
response = await async_client.get("/api/admin/settings")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_get_settings_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法获取设置"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/settings",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_update_settings_requires_auth(async_client: AsyncClient):
|
||||
"""测试更新设置需要认证"""
|
||||
response = await async_client.patch(
|
||||
"/api/admin/settings",
|
||||
json={"siteName": "New Site Name"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_update_settings_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法更新设置"""
|
||||
response = await async_client.patch(
|
||||
"/api/admin/settings",
|
||||
headers=auth_headers,
|
||||
json={"siteName": "New Site Name"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 存储策略管理测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_policy_list_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取存储策略列表需要认证"""
|
||||
response = await async_client.get("/api/admin/policy/list")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_policy_list_requires_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试普通用户无法获取存储策略列表"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/policy/list",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
302
tests/integration/api/test_directory.py
Normal file
302
tests/integration/api/test_directory.py
Normal file
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
目录操作端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
# ==================== 认证测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取目录需要认证"""
|
||||
response = await async_client.get("/api/directory/testuser")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# ==================== 获取目录测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_get_root(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试获取用户根目录"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/testuser",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert "parent" in data
|
||||
assert "objects" in data
|
||||
assert "policy" in data
|
||||
assert data["parent"] is None # 根目录的 parent 为 None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_get_nested(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试获取嵌套目录"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/testuser/docs",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert "objects" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_get_contains_children(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试目录包含子对象"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/testuser/docs",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
objects = data["objects"]
|
||||
assert isinstance(objects, list)
|
||||
# docs 目录下应该有 images 文件夹和 readme.md 文件
|
||||
assert len(objects) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_forbidden_other_user(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试访问他人目录返回 403"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/admin",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_not_found(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试目录不存在返回 404"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/testuser/nonexistent",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_empty_path_returns_400(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试空路径返回 400"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_response_includes_policy(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试目录响应包含存储策略"""
|
||||
response = await async_client.get(
|
||||
"/api/directory/testuser",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "policy" in data
|
||||
policy = data["policy"]
|
||||
assert "id" in policy
|
||||
assert "name" in policy
|
||||
assert "type" in policy
|
||||
|
||||
|
||||
# ==================== 创建目录测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_requires_auth(async_client: AsyncClient):
|
||||
"""测试创建目录需要认证"""
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
json={
|
||||
"parent_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "newfolder"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_success(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试成功创建目录"""
|
||||
parent_id = test_directory_structure["root_id"]
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(parent_id),
|
||||
"name": "newfolder"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
folder_data = data["data"]
|
||||
assert "id" in folder_data
|
||||
assert "name" in folder_data
|
||||
assert folder_data["name"] == "newfolder"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_duplicate_name(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试重名目录返回 409"""
|
||||
parent_id = test_directory_structure["root_id"]
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(parent_id),
|
||||
"name": "docs" # 已存在的目录名
|
||||
}
|
||||
)
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_invalid_parent(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试无效父目录返回 404"""
|
||||
invalid_uuid = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": invalid_uuid,
|
||||
"name": "newfolder"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_empty_name(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试空目录名返回 400"""
|
||||
parent_id = test_directory_structure["root_id"]
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(parent_id),
|
||||
"name": ""
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_name_with_slash(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试目录名包含斜杠返回 400"""
|
||||
parent_id = test_directory_structure["root_id"]
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(parent_id),
|
||||
"name": "invalid/name"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_parent_is_file(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试父路径是文件返回 400"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(file_id),
|
||||
"name": "newfolder"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_directory_create_other_user_parent(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试在他人目录下创建目录返回 404"""
|
||||
# 先用管理员账号获取管理员的根目录ID
|
||||
admin_response = await async_client.get(
|
||||
"/api/directory/admin",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert admin_response.status_code == 200
|
||||
admin_root_id = admin_response.json()["id"]
|
||||
|
||||
# 普通用户尝试在管理员目录下创建文件夹
|
||||
response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": admin_root_id,
|
||||
"name": "hackfolder"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
366
tests/integration/api/test_object.py
Normal file
366
tests/integration/api/test_object.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
对象操作端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
# ==================== 删除对象测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_delete_requires_auth(async_client: AsyncClient):
|
||||
"""测试删除对象需要认证"""
|
||||
response = await async_client.delete(
|
||||
"/api/object/",
|
||||
json={"ids": ["00000000-0000-0000-0000-000000000000"]}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_delete_single(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试删除单个对象"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
response = await async_client.delete(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={"ids": [str(file_id)]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
result = data["data"]
|
||||
assert "deleted" in result
|
||||
assert "total" in result
|
||||
assert result["deleted"] == 1
|
||||
assert result["total"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_delete_multiple(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试批量删除"""
|
||||
docs_id = test_directory_structure["docs_id"]
|
||||
images_id = test_directory_structure["images_id"]
|
||||
|
||||
response = await async_client.delete(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={"ids": [str(docs_id), str(images_id)]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
assert result["deleted"] >= 1
|
||||
assert result["total"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_delete_not_owned(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试删除他人对象无效"""
|
||||
# 先用管理员创建一个文件夹
|
||||
admin_dir_response = await async_client.get(
|
||||
"/api/directory/admin",
|
||||
headers=admin_headers
|
||||
)
|
||||
admin_root_id = admin_dir_response.json()["id"]
|
||||
|
||||
create_response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"parent_id": admin_root_id,
|
||||
"name": "adminfolder"
|
||||
}
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
admin_folder_id = create_response.json()["data"]["id"]
|
||||
|
||||
# 普通用户尝试删除管理员的文件夹
|
||||
response = await async_client.delete(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={"ids": [admin_folder_id]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
# 无权删除,deleted 应该为 0
|
||||
assert result["deleted"] == 0
|
||||
assert result["total"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_delete_nonexistent(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试删除不存在的对象"""
|
||||
fake_id = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
response = await async_client.delete(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={"ids": [fake_id]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
assert result["deleted"] == 0
|
||||
|
||||
|
||||
# ==================== 移动对象测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_requires_auth(async_client: AsyncClient):
|
||||
"""测试移动对象需要认证"""
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
json={
|
||||
"src_ids": ["00000000-0000-0000-0000-000000000000"],
|
||||
"dst_id": "00000000-0000-0000-0000-000000000001"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_success(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试成功移动对象"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
images_id = test_directory_structure["images_id"]
|
||||
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [str(file_id)],
|
||||
"dst_id": str(images_id)
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
assert "moved" in result
|
||||
assert "total" in result
|
||||
assert result["moved"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_to_invalid_target(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试无效目标返回 404"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
invalid_dst = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [str(file_id)],
|
||||
"dst_id": invalid_dst
|
||||
}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_to_file(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试移动到文件返回 400"""
|
||||
docs_id = test_directory_structure["docs_id"]
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [str(docs_id)],
|
||||
"dst_id": str(file_id)
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_to_self(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试移动到自身应该被跳过"""
|
||||
docs_id = test_directory_structure["docs_id"]
|
||||
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [str(docs_id)],
|
||||
"dst_id": str(docs_id)
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
# 移动到自身应该被跳过
|
||||
assert result["moved"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_duplicate_name_skipped(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试移动到同名位置应该被跳过"""
|
||||
root_id = test_directory_structure["root_id"]
|
||||
docs_id = test_directory_structure["docs_id"]
|
||||
images_id = test_directory_structure["images_id"]
|
||||
|
||||
# 先在根目录创建一个与 images 同名的文件夹
|
||||
await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"parent_id": str(root_id),
|
||||
"name": "images"
|
||||
}
|
||||
)
|
||||
|
||||
# 尝试将 docs/images 移动到根目录(已存在同名)
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [str(images_id)],
|
||||
"dst_id": str(root_id)
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
# 同名冲突应该被跳过
|
||||
assert result["moved"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_move_other_user_object(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
admin_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID]
|
||||
):
|
||||
"""测试移动他人对象应该被跳过"""
|
||||
# 获取管理员的根目录
|
||||
admin_response = await async_client.get(
|
||||
"/api/directory/admin",
|
||||
headers=admin_headers
|
||||
)
|
||||
admin_root_id = admin_response.json()["id"]
|
||||
|
||||
# 创建管理员的文件夹
|
||||
create_response = await async_client.put(
|
||||
"/api/directory/",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"parent_id": admin_root_id,
|
||||
"name": "adminfolder"
|
||||
}
|
||||
)
|
||||
admin_folder_id = create_response.json()["data"]["id"]
|
||||
|
||||
# 普通用户尝试移动管理员的文件夹
|
||||
user_root_id = test_directory_structure["root_id"]
|
||||
response = await async_client.patch(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"src_ids": [admin_folder_id],
|
||||
"dst_id": str(user_root_id)
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
result = data["data"]
|
||||
# 无权移动他人对象
|
||||
assert result["moved"] == 0
|
||||
|
||||
|
||||
# ==================== 其他对象操作测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_copy_endpoint_exists(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试复制对象端点存在"""
|
||||
response = await async_client.post(
|
||||
"/api/object/copy",
|
||||
headers=auth_headers,
|
||||
json={"src_id": "00000000-0000-0000-0000-000000000000"}
|
||||
)
|
||||
# 未实现的端点
|
||||
assert response.status_code in [200, 404, 501]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_rename_endpoint_exists(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试重命名对象端点存在"""
|
||||
response = await async_client.post(
|
||||
"/api/object/rename",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "newname"
|
||||
}
|
||||
)
|
||||
# 未实现的端点
|
||||
assert response.status_code in [200, 404, 501]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_property_endpoint_exists(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试获取对象属性端点存在"""
|
||||
response = await async_client.get(
|
||||
"/api/object/property/00000000-0000-0000-0000-000000000000",
|
||||
headers=auth_headers
|
||||
)
|
||||
# 未实现的端点
|
||||
assert response.status_code in [200, 404, 501]
|
||||
91
tests/integration/api/test_site.py
Normal file
91
tests/integration/api/test_site.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
站点配置端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_ping(async_client: AsyncClient):
|
||||
"""测试 /api/site/ping 返回 200"""
|
||||
response = await async_client.get("/api/site/ping")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_ping_response_format(async_client: AsyncClient):
|
||||
"""测试 /api/site/ping 响应包含版本号"""
|
||||
response = await async_client.get("/api/site/ping")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
# BackendVersion 应该是字符串格式的版本号
|
||||
assert isinstance(data["data"], str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_config(async_client: AsyncClient):
|
||||
"""测试 /api/site/config 返回配置"""
|
||||
response = await async_client.get("/api/site/config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_config_contains_title(async_client: AsyncClient):
|
||||
"""测试配置包含站点标题"""
|
||||
response = await async_client.get("/api/site/config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
config = data["data"]
|
||||
assert "title" in config
|
||||
assert config["title"] == "DiskNext Test"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_config_contains_themes(async_client: AsyncClient):
|
||||
"""测试配置包含主题设置"""
|
||||
response = await async_client.get("/api/site/config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
config = data["data"]
|
||||
assert "themes" in config
|
||||
assert "defaultTheme" in config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_config_register_enabled(async_client: AsyncClient):
|
||||
"""测试配置包含注册开关"""
|
||||
response = await async_client.get("/api/site/config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
config = data["data"]
|
||||
assert "registerEnabled" in config
|
||||
assert config["registerEnabled"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_config_captcha_settings(async_client: AsyncClient):
|
||||
"""测试配置包含验证码设置"""
|
||||
response = await async_client.get("/api/site/config")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
config = data["data"]
|
||||
assert "loginCaptcha" in config
|
||||
assert "regCaptcha" in config
|
||||
assert "forgetCaptcha" in config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_site_captcha_endpoint_exists(async_client: AsyncClient):
|
||||
"""测试验证码端点存在(即使未实现也应返回有效响应)"""
|
||||
response = await async_client.get("/api/site/captcha")
|
||||
# 未实现的端点可能返回 404 或其他状态码
|
||||
assert response.status_code in [200, 404, 501]
|
||||
290
tests/integration/api/test_user.py
Normal file
290
tests/integration/api/test_user.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
用户相关端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
# ==================== 登录测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_login_success(
|
||||
async_client: AsyncClient,
|
||||
test_user_info: dict[str, str]
|
||||
):
|
||||
"""测试成功登录"""
|
||||
response = await async_client.post(
|
||||
"/api/user/session",
|
||||
data={
|
||||
"username": test_user_info["username"],
|
||||
"password": test_user_info["password"],
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
assert "access_expires" in data
|
||||
assert "refresh_expires" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_login_wrong_password(
|
||||
async_client: AsyncClient,
|
||||
test_user_info: dict[str, str]
|
||||
):
|
||||
"""测试密码错误返回 401"""
|
||||
response = await async_client.post(
|
||||
"/api/user/session",
|
||||
data={
|
||||
"username": test_user_info["username"],
|
||||
"password": "wrongpassword",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_login_nonexistent_user(async_client: AsyncClient):
|
||||
"""测试不存在的用户返回 401"""
|
||||
response = await async_client.post(
|
||||
"/api/user/session",
|
||||
data={
|
||||
"username": "nonexistent",
|
||||
"password": "anypassword",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_login_user_banned(
|
||||
async_client: AsyncClient,
|
||||
banned_user_info: dict[str, str]
|
||||
):
|
||||
"""测试封禁用户返回 403"""
|
||||
response = await async_client.post(
|
||||
"/api/user/session",
|
||||
data={
|
||||
"username": banned_user_info["username"],
|
||||
"password": banned_user_info["password"],
|
||||
}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ==================== 注册测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_register_success(async_client: AsyncClient):
|
||||
"""测试成功注册"""
|
||||
response = await async_client.post(
|
||||
"/api/user/",
|
||||
json={
|
||||
"username": "newuser",
|
||||
"password": "newpass123",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
assert "user_id" in data["data"]
|
||||
assert "username" in data["data"]
|
||||
assert data["data"]["username"] == "newuser"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_register_duplicate_username(
|
||||
async_client: AsyncClient,
|
||||
test_user_info: dict[str, str]
|
||||
):
|
||||
"""测试重复用户名返回 400"""
|
||||
response = await async_client.post(
|
||||
"/api/user/",
|
||||
json={
|
||||
"username": test_user_info["username"],
|
||||
"password": "anypassword",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ==================== 用户信息测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_me_requires_auth(async_client: AsyncClient):
|
||||
"""测试 /api/user/me 需要认证"""
|
||||
response = await async_client.get("/api/user/me")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_me_with_invalid_token(async_client: AsyncClient):
|
||||
"""测试无效token返回 401"""
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": "Bearer invalid_token"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_me_returns_user_info(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试返回用户信息"""
|
||||
response = await async_client.get("/api/user/me", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
user_data = data["data"]
|
||||
assert "id" in user_data
|
||||
assert "username" in user_data
|
||||
assert user_data["username"] == "testuser"
|
||||
assert "group" in user_data
|
||||
assert "tags" in user_data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_me_contains_group_info(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试用户信息包含用户组"""
|
||||
response = await async_client.get("/api/user/me", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
user_data = data["data"]
|
||||
assert user_data["group"] is not None
|
||||
assert "name" in user_data["group"]
|
||||
|
||||
|
||||
# ==================== 存储信息测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_storage_requires_auth(async_client: AsyncClient):
|
||||
"""测试 /api/user/storage 需要认证"""
|
||||
response = await async_client.get("/api/user/storage")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_storage_info(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试返回存储信息"""
|
||||
response = await async_client.get("/api/user/storage", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
storage_data = data["data"]
|
||||
assert "used" in storage_data
|
||||
assert "free" in storage_data
|
||||
assert "total" in storage_data
|
||||
assert storage_data["total"] == storage_data["used"] + storage_data["free"]
|
||||
|
||||
|
||||
# ==================== 两步验证测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_2fa_init_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取2FA初始化信息需要认证"""
|
||||
response = await async_client.get("/api/user/settings/2fa")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_2fa_init(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试获取2FA初始化信息"""
|
||||
response = await async_client.get(
|
||||
"/api/user/settings/2fa",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
# 应该包含二维码URL和密钥
|
||||
assert isinstance(data["data"], dict)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_2fa_enable_requires_auth(async_client: AsyncClient):
|
||||
"""测试启用2FA需要认证"""
|
||||
response = await async_client.post(
|
||||
"/api/user/settings/2fa",
|
||||
params={"setup_token": "fake_token", "code": "123456"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_2fa_enable_invalid_token(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试无效的setup_token返回 400"""
|
||||
response = await async_client.post(
|
||||
"/api/user/settings/2fa",
|
||||
params={"setup_token": "invalid_token", "code": "123456"},
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ==================== 用户设置测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_settings_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取用户设置需要认证"""
|
||||
response = await async_client.get("/api/user/settings/")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_settings_returns_data(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试返回用户设置"""
|
||||
response = await async_client.get(
|
||||
"/api/user/settings/",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
|
||||
|
||||
# ==================== WebAuthn 测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_authn_start_requires_auth(async_client: AsyncClient):
|
||||
"""测试WebAuthn初始化需要认证"""
|
||||
response = await async_client.put("/api/user/authn/start")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_authn_start_disabled(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试WebAuthn未启用时返回 400"""
|
||||
response = await async_client.put(
|
||||
"/api/user/authn/start",
|
||||
headers=auth_headers
|
||||
)
|
||||
# WebAuthn 在测试环境中未启用
|
||||
assert response.status_code == 400
|
||||
413
tests/integration/conftest.py
Normal file
413
tests/integration/conftest.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
集成测试配置文件
|
||||
|
||||
提供测试数据库、测试客户端、测试用户等 fixtures
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import AsyncGenerator
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from sqlmodel import SQLModel
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
from main import app
|
||||
from models import Group, GroupOptions, Object, ObjectType, Policy, PolicyType, Setting, SettingsType, User
|
||||
from utils import Password
|
||||
from utils.JWT import create_access_token
|
||||
from utils.JWT import JWT
|
||||
|
||||
|
||||
# ==================== 事件循环配置 ====================
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""提供会话级别的事件循环"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
# ==================== 测试数据库 ====================
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def test_db_engine() -> AsyncGenerator[AsyncEngine, None]:
|
||||
"""创建测试数据库引擎(内存SQLite)"""
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
|
||||
# 创建所有表
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
yield engine
|
||||
|
||||
# 清理
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def test_session(test_db_engine: AsyncEngine) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""提供测试数据库会话"""
|
||||
async_session_factory = sessionmaker(
|
||||
test_db_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
async with async_session_factory() as session:
|
||||
yield session
|
||||
|
||||
|
||||
# ==================== 测试数据初始化 ====================
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def initialized_db(test_session: AsyncSession) -> AsyncSession:
|
||||
"""初始化测试数据库(包含基础配置和测试数据)"""
|
||||
|
||||
# 1. 创建基础设置
|
||||
settings = [
|
||||
Setting(type=SettingsType.BASIC, name="siteName", value="DiskNext Test"),
|
||||
Setting(type=SettingsType.BASIC, name="siteURL", value="http://localhost:8000"),
|
||||
Setting(type=SettingsType.BASIC, name="siteTitle", value="DiskNext"),
|
||||
Setting(type=SettingsType.BASIC, name="themes", value='{"default": "#5898d4"}'),
|
||||
Setting(type=SettingsType.BASIC, name="defaultTheme", value="default"),
|
||||
Setting(type=SettingsType.LOGIN, name="login_captcha", value="0"),
|
||||
Setting(type=SettingsType.LOGIN, name="reg_captcha", value="0"),
|
||||
Setting(type=SettingsType.LOGIN, name="forget_captcha", value="0"),
|
||||
Setting(type=SettingsType.LOGIN, name="email_active", value="0"),
|
||||
Setting(type=SettingsType.VIEW, name="home_view_method", value="list"),
|
||||
Setting(type=SettingsType.VIEW, name="share_view_method", value="grid"),
|
||||
Setting(type=SettingsType.AUTHN, name="authn_enabled", value="0"),
|
||||
Setting(type=SettingsType.CAPTCHA, name="captcha_ReCaptchaKey", value=""),
|
||||
Setting(type=SettingsType.CAPTCHA, name="captcha_CloudflareKey", value=""),
|
||||
Setting(type=SettingsType.REGISTER, name="register_enabled", value="1"),
|
||||
Setting(type=SettingsType.AUTH, name="secret_key", value="test_secret_key_for_jwt_token_generation"),
|
||||
]
|
||||
for setting in settings:
|
||||
test_session.add(setting)
|
||||
|
||||
# 2. 创建默认存储策略
|
||||
default_policy = Policy(
|
||||
id=uuid4(),
|
||||
name="本地存储",
|
||||
type=PolicyType.LOCAL,
|
||||
max_size=0,
|
||||
auto_rename=False,
|
||||
directory_naming_rule="",
|
||||
file_naming_rule="",
|
||||
is_origin_link_enabled=False,
|
||||
option_serialization={},
|
||||
)
|
||||
test_session.add(default_policy)
|
||||
|
||||
# 3. 创建用户组
|
||||
default_group = Group(
|
||||
id=uuid4(),
|
||||
name="默认用户组",
|
||||
max_storage=1024 * 1024 * 1024, # 1GB
|
||||
share_enabled=True,
|
||||
web_dav_enabled=True,
|
||||
admin=False,
|
||||
speed_limit=0,
|
||||
)
|
||||
test_session.add(default_group)
|
||||
|
||||
admin_group = Group(
|
||||
id=uuid4(),
|
||||
name="管理员组",
|
||||
max_storage=10 * 1024 * 1024 * 1024, # 10GB
|
||||
share_enabled=True,
|
||||
web_dav_enabled=True,
|
||||
admin=True,
|
||||
speed_limit=0,
|
||||
)
|
||||
test_session.add(admin_group)
|
||||
|
||||
await test_session.commit()
|
||||
|
||||
# 刷新以获取ID
|
||||
await test_session.refresh(default_group)
|
||||
await test_session.refresh(admin_group)
|
||||
await test_session.refresh(default_policy)
|
||||
|
||||
# 4. 创建用户组选项
|
||||
default_group_options = GroupOptions(
|
||||
group_id=default_group.id,
|
||||
share_download=True,
|
||||
share_free=False,
|
||||
relocate=False,
|
||||
source_batch=0,
|
||||
select_node=False,
|
||||
advance_delete=False,
|
||||
)
|
||||
test_session.add(default_group_options)
|
||||
|
||||
admin_group_options = GroupOptions(
|
||||
group_id=admin_group.id,
|
||||
share_download=True,
|
||||
share_free=True,
|
||||
relocate=True,
|
||||
source_batch=10,
|
||||
select_node=True,
|
||||
advance_delete=True,
|
||||
)
|
||||
test_session.add(admin_group_options)
|
||||
|
||||
# 5. 添加默认用户组UUID到设置
|
||||
default_group_setting = Setting(
|
||||
type=SettingsType.REGISTER,
|
||||
name="default_group",
|
||||
value=str(default_group.id),
|
||||
)
|
||||
test_session.add(default_group_setting)
|
||||
|
||||
await test_session.commit()
|
||||
|
||||
# 6. 创建测试用户
|
||||
test_user = User(
|
||||
id=uuid4(),
|
||||
username="testuser",
|
||||
password=Password.hash("testpass123"),
|
||||
nickname="测试用户",
|
||||
status=True,
|
||||
storage=0,
|
||||
score=0,
|
||||
group_id=default_group.id,
|
||||
avatar="default",
|
||||
theme="system",
|
||||
)
|
||||
test_session.add(test_user)
|
||||
|
||||
admin_user = User(
|
||||
id=uuid4(),
|
||||
username="admin",
|
||||
password=Password.hash("adminpass123"),
|
||||
nickname="管理员",
|
||||
status=True,
|
||||
storage=0,
|
||||
score=0,
|
||||
group_id=admin_group.id,
|
||||
avatar="default",
|
||||
theme="system",
|
||||
)
|
||||
test_session.add(admin_user)
|
||||
|
||||
banned_user = User(
|
||||
id=uuid4(),
|
||||
username="banneduser",
|
||||
password=Password.hash("banned123"),
|
||||
nickname="封禁用户",
|
||||
status=False, # 封禁状态
|
||||
storage=0,
|
||||
score=0,
|
||||
group_id=default_group.id,
|
||||
avatar="default",
|
||||
theme="system",
|
||||
)
|
||||
test_session.add(banned_user)
|
||||
|
||||
await test_session.commit()
|
||||
|
||||
# 刷新用户对象
|
||||
await test_session.refresh(test_user)
|
||||
await test_session.refresh(admin_user)
|
||||
await test_session.refresh(banned_user)
|
||||
|
||||
# 7. 创建用户根目录
|
||||
test_user_root = Object(
|
||||
id=uuid4(),
|
||||
name=test_user.username,
|
||||
type=ObjectType.FOLDER,
|
||||
owner_id=test_user.id,
|
||||
parent_id=None,
|
||||
policy_id=default_policy.id,
|
||||
size=0,
|
||||
)
|
||||
test_session.add(test_user_root)
|
||||
|
||||
admin_user_root = Object(
|
||||
id=uuid4(),
|
||||
name=admin_user.username,
|
||||
type=ObjectType.FOLDER,
|
||||
owner_id=admin_user.id,
|
||||
parent_id=None,
|
||||
policy_id=default_policy.id,
|
||||
size=0,
|
||||
)
|
||||
test_session.add(admin_user_root)
|
||||
|
||||
await test_session.commit()
|
||||
|
||||
# 8. 设置JWT密钥(从数据库加载)
|
||||
JWT.SECRET_KEY = "test_secret_key_for_jwt_token_generation"
|
||||
|
||||
return test_session
|
||||
|
||||
|
||||
# ==================== 测试用户信息 ====================
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_info() -> dict[str, str]:
|
||||
"""测试用户信息"""
|
||||
return {
|
||||
"username": "testuser",
|
||||
"password": "testpass123",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user_info() -> dict[str, str]:
|
||||
"""管理员用户信息"""
|
||||
return {
|
||||
"username": "admin",
|
||||
"password": "adminpass123",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def banned_user_info() -> dict[str, str]:
|
||||
"""封禁用户信息"""
|
||||
return {
|
||||
"username": "banneduser",
|
||||
"password": "banned123",
|
||||
}
|
||||
|
||||
|
||||
# ==================== JWT Token ====================
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_token(test_user_info: dict[str, str]) -> str:
|
||||
"""生成测试用户的JWT token"""
|
||||
token, _ = JWT.create_access_token(
|
||||
data={"sub": test_user_info["username"]},
|
||||
expires_delta=timedelta(hours=1),
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user_token(admin_user_info: dict[str, str]) -> str:
|
||||
"""生成管理员的JWT token"""
|
||||
token, _ = JWT.create_access_token(
|
||||
data={"sub": admin_user_info["username"]},
|
||||
expires_delta=timedelta(hours=1),
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expired_token() -> str:
|
||||
"""生成过期的JWT token"""
|
||||
token, _ = JWT.create_access_token(
|
||||
data={"sub": "testuser"},
|
||||
expires_delta=timedelta(seconds=-1), # 已过期
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
# ==================== 认证头 ====================
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(test_user_token: str) -> dict[str, str]:
|
||||
"""测试用户的认证头"""
|
||||
return {"Authorization": f"Bearer {test_user_token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_headers(admin_user_token: str) -> dict[str, str]:
|
||||
"""管理员的认证头"""
|
||||
return {"Authorization": f"Bearer {admin_user_token}"}
|
||||
|
||||
|
||||
# ==================== HTTP 客户端 ====================
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_client(initialized_db: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""异步HTTP测试客户端"""
|
||||
|
||||
# 覆盖依赖项,使用测试数据库
|
||||
from middleware.dependencies import get_session
|
||||
|
||||
async def override_get_session():
|
||||
yield initialized_db
|
||||
|
||||
app.dependency_overrides[get_session] = override_get_session
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
# 清理
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# ==================== 测试目录结构 ====================
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_directory_structure(initialized_db: AsyncSession) -> dict[str, UUID]:
|
||||
"""创建测试目录结构"""
|
||||
|
||||
# 获取测试用户和根目录
|
||||
test_user = await User.get(initialized_db, User.username == "testuser")
|
||||
test_user_root = await Object.get_root(initialized_db, test_user.id)
|
||||
|
||||
default_policy = await Policy.get(initialized_db, Policy.name == "本地存储")
|
||||
|
||||
# 创建 docs 目录
|
||||
docs_folder = Object(
|
||||
id=uuid4(),
|
||||
name="docs",
|
||||
type=ObjectType.FOLDER,
|
||||
owner_id=test_user.id,
|
||||
parent_id=test_user_root.id,
|
||||
policy_id=default_policy.id,
|
||||
size=0,
|
||||
)
|
||||
initialized_db.add(docs_folder)
|
||||
|
||||
# 创建 images 子目录
|
||||
images_folder = Object(
|
||||
id=uuid4(),
|
||||
name="images",
|
||||
type=ObjectType.FOLDER,
|
||||
owner_id=test_user.id,
|
||||
parent_id=docs_folder.id,
|
||||
policy_id=default_policy.id,
|
||||
size=0,
|
||||
)
|
||||
initialized_db.add(images_folder)
|
||||
|
||||
# 创建测试文件
|
||||
test_file = Object(
|
||||
id=uuid4(),
|
||||
name="readme.md",
|
||||
type=ObjectType.FILE,
|
||||
owner_id=test_user.id,
|
||||
parent_id=docs_folder.id,
|
||||
policy_id=default_policy.id,
|
||||
size=1024,
|
||||
)
|
||||
initialized_db.add(test_file)
|
||||
|
||||
await initialized_db.commit()
|
||||
|
||||
return {
|
||||
"root_id": test_user_root.id,
|
||||
"docs_id": docs_folder.id,
|
||||
"images_id": images_folder.id,
|
||||
"file_id": test_file.id,
|
||||
}
|
||||
3
tests/integration/middleware/__init__.py
Normal file
3
tests/integration/middleware/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
中间件集成测试包
|
||||
"""
|
||||
256
tests/integration/middleware/test_auth.py
Normal file
256
tests/integration/middleware/test_auth.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
认证中间件集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from datetime import timedelta
|
||||
|
||||
from utils.JWT import JWT
|
||||
|
||||
|
||||
# ==================== AuthRequired 测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_no_token(async_client: AsyncClient):
|
||||
"""测试无token返回 401"""
|
||||
response = await async_client.get("/api/user/me")
|
||||
assert response.status_code == 401
|
||||
assert "WWW-Authenticate" in response.headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_invalid_token(async_client: AsyncClient):
|
||||
"""测试无效token返回 401"""
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": "Bearer invalid_token_string"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_malformed_token(async_client: AsyncClient):
|
||||
"""测试格式错误的token返回 401"""
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": "InvalidFormat"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_expired_token(
|
||||
async_client: AsyncClient,
|
||||
expired_token: str
|
||||
):
|
||||
"""测试过期token返回 401"""
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": f"Bearer {expired_token}"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_valid_token(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试有效token通过认证"""
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_token_without_sub(async_client: AsyncClient):
|
||||
"""测试缺少sub字段的token返回 401"""
|
||||
token, _ = JWT.create_access_token(
|
||||
data={"other_field": "value"},
|
||||
expires_delta=timedelta(hours=1)
|
||||
)
|
||||
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_required_nonexistent_user_token(async_client: AsyncClient):
|
||||
"""测试用户不存在的token返回 401"""
|
||||
token, _ = JWT.create_access_token(
|
||||
data={"sub": "nonexistent_user"},
|
||||
expires_delta=timedelta(hours=1)
|
||||
)
|
||||
|
||||
response = await async_client.get(
|
||||
"/api/user/me",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# ==================== AdminRequired 测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_required_no_auth(async_client: AsyncClient):
|
||||
"""测试管理员端点无认证返回 401"""
|
||||
response = await async_client.get("/api/admin/summary")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_required_non_admin(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试非管理员返回 403"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/summary",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 403
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
assert data["detail"] == "Admin Required"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_required_admin(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试管理员通过认证"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/summary",
|
||||
headers=admin_headers
|
||||
)
|
||||
# 端点可能未实现,但应该通过认证检查
|
||||
assert response.status_code != 403
|
||||
assert response.status_code != 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_required_on_user_list(
|
||||
async_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试管理员可以访问用户列表"""
|
||||
response = await async_client.get(
|
||||
"/api/admin/user/list",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_required_on_settings(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
admin_headers: dict[str, str]
|
||||
):
|
||||
"""测试管理员可以访问设置,普通用户不能"""
|
||||
# 普通用户
|
||||
user_response = await async_client.get(
|
||||
"/api/admin/settings",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert user_response.status_code == 403
|
||||
|
||||
# 管理员
|
||||
admin_response = await async_client.get(
|
||||
"/api/admin/settings",
|
||||
headers=admin_headers
|
||||
)
|
||||
assert admin_response.status_code != 403
|
||||
|
||||
|
||||
# ==================== 认证装饰器应用测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_on_directory_endpoint(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试目录端点应用认证"""
|
||||
# 无认证
|
||||
response_no_auth = await async_client.get("/api/directory/testuser")
|
||||
assert response_no_auth.status_code == 401
|
||||
|
||||
# 有认证
|
||||
response_with_auth = await async_client.get(
|
||||
"/api/directory/testuser",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response_with_auth.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_on_object_endpoint(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试对象端点应用认证"""
|
||||
# 无认证
|
||||
response_no_auth = await async_client.delete(
|
||||
"/api/object/",
|
||||
json={"ids": ["00000000-0000-0000-0000-000000000000"]}
|
||||
)
|
||||
assert response_no_auth.status_code == 401
|
||||
|
||||
# 有认证
|
||||
response_with_auth = await async_client.delete(
|
||||
"/api/object/",
|
||||
headers=auth_headers,
|
||||
json={"ids": ["00000000-0000-0000-0000-000000000000"]}
|
||||
)
|
||||
assert response_with_auth.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_on_storage_endpoint(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
):
|
||||
"""测试存储端点应用认证"""
|
||||
# 无认证
|
||||
response_no_auth = await async_client.get("/api/user/storage")
|
||||
assert response_no_auth.status_code == 401
|
||||
|
||||
# 有认证
|
||||
response_with_auth = await async_client.get(
|
||||
"/api/user/storage",
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response_with_auth.status_code == 200
|
||||
|
||||
|
||||
# ==================== Token 刷新测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_format(test_user_info: dict[str, str]):
|
||||
"""测试刷新token格式正确"""
|
||||
refresh_token, _ = JWT.create_refresh_token(
|
||||
data={"sub": test_user_info["username"]},
|
||||
expires_delta=timedelta(days=7)
|
||||
)
|
||||
|
||||
assert isinstance(refresh_token, str)
|
||||
assert len(refresh_token) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_token_format(test_user_info: dict[str, str]):
|
||||
"""测试访问token格式正确"""
|
||||
access_token, expires = JWT.create_access_token(
|
||||
data={"sub": test_user_info["username"]},
|
||||
expires_delta=timedelta(hours=1)
|
||||
)
|
||||
|
||||
assert isinstance(access_token, str)
|
||||
assert len(access_token) > 0
|
||||
assert expires is not None
|
||||
Reference in New Issue
Block a user