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:
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
|
||||
Reference in New Issue
Block a user