Add unit tests for models and services

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

14
tests/fixtures/__init__.py vendored Normal file
View File

@@ -0,0 +1,14 @@
"""
测试数据工厂模块
提供便捷的测试数据创建工具,用于在测试中快速生成用户、用户组、对象等数据。
"""
from .users import UserFactory
from .groups import GroupFactory
from .objects import ObjectFactory
__all__ = [
"UserFactory",
"GroupFactory",
"ObjectFactory",
]

202
tests/fixtures/groups.py vendored Normal file
View File

@@ -0,0 +1,202 @@
"""
用户组测试数据工厂
提供创建测试用户组的便捷方法。
"""
from sqlmodel.ext.asyncio.session import AsyncSession
from models.group import Group, GroupOptions
class GroupFactory:
"""用户组工厂类,用于创建各种类型的测试用户组"""
@staticmethod
async def create(
session: AsyncSession,
name: str | None = None,
**kwargs
) -> Group:
"""
创建用户组
参数:
session: 数据库会话
name: 用户组名称(默认: test_group_{随机}
**kwargs: 其他用户组字段
返回:
Group: 创建的用户组实例
"""
import uuid
if name is None:
name = f"test_group_{uuid.uuid4().hex[:8]}"
group = Group(
name=name,
max_storage=kwargs.get("max_storage", 1024 * 1024 * 1024 * 10), # 默认 10GB
share_enabled=kwargs.get("share_enabled", True),
web_dav_enabled=kwargs.get("web_dav_enabled", True),
admin=kwargs.get("admin", False),
speed_limit=kwargs.get("speed_limit", 0),
)
group = await group.save(session)
# 如果提供了选项参数,创建 GroupOptions
if kwargs.get("create_options", False):
options = GroupOptions(
group_id=group.id,
share_download=kwargs.get("share_download", True),
share_free=kwargs.get("share_free", False),
relocate=kwargs.get("relocate", True),
source_batch=kwargs.get("source_batch", 10),
select_node=kwargs.get("select_node", False),
advance_delete=kwargs.get("advance_delete", False),
)
await options.save(session)
return group
@staticmethod
async def create_admin_group(
session: AsyncSession,
name: str | None = None
) -> Group:
"""
创建管理员组
参数:
session: 数据库会话
name: 用户组名称(默认: admin_group_{随机}
返回:
Group: 创建的管理员组实例
"""
import uuid
if name is None:
name = f"admin_group_{uuid.uuid4().hex[:8]}"
admin_group = Group(
name=name,
max_storage=0, # 无限制
share_enabled=True,
web_dav_enabled=True,
admin=True,
speed_limit=0,
)
admin_group = await admin_group.save(session)
# 创建管理员组选项
admin_options = GroupOptions(
group_id=admin_group.id,
share_download=True,
share_free=True,
relocate=True,
source_batch=100,
select_node=True,
advance_delete=True,
archive_download=True,
archive_task=True,
webdav_proxy=True,
aria2=True,
redirected_source=True,
)
await admin_options.save(session)
return admin_group
@staticmethod
async def create_limited_group(
session: AsyncSession,
max_storage: int,
name: str | None = None
) -> Group:
"""
创建有存储限制的用户组
参数:
session: 数据库会话
max_storage: 最大存储空间(字节)
name: 用户组名称(默认: limited_group_{随机}
返回:
Group: 创建的用户组实例
"""
import uuid
if name is None:
name = f"limited_group_{uuid.uuid4().hex[:8]}"
limited_group = Group(
name=name,
max_storage=max_storage,
share_enabled=True,
web_dav_enabled=False,
admin=False,
speed_limit=1024, # 1MB/s
)
limited_group = await limited_group.save(session)
# 创建限制组选项
limited_options = GroupOptions(
group_id=limited_group.id,
share_download=False,
share_free=False,
relocate=False,
source_batch=0,
select_node=False,
advance_delete=False,
)
await limited_options.save(session)
return limited_group
@staticmethod
async def create_free_group(
session: AsyncSession,
name: str | None = None
) -> Group:
"""
创建免费用户组(无特殊权限)
参数:
session: 数据库会话
name: 用户组名称(默认: free_group_{随机}
返回:
Group: 创建的用户组实例
"""
import uuid
if name is None:
name = f"free_group_{uuid.uuid4().hex[:8]}"
free_group = Group(
name=name,
max_storage=1024 * 1024 * 1024, # 1GB
share_enabled=False,
web_dav_enabled=False,
admin=False,
speed_limit=512, # 512KB/s
)
free_group = await free_group.save(session)
# 创建免费组选项
free_options = GroupOptions(
group_id=free_group.id,
share_download=False,
share_free=False,
relocate=False,
source_batch=0,
select_node=False,
advance_delete=False,
)
await free_options.save(session)
return free_group

364
tests/fixtures/objects.py vendored Normal file
View File

@@ -0,0 +1,364 @@
"""
对象(文件/目录)测试数据工厂
提供创建测试对象的便捷方法。
"""
from uuid import UUID
from sqlmodel.ext.asyncio.session import AsyncSession
from models.object import Object, ObjectType
from models.user import User
class ObjectFactory:
"""对象工厂类,用于创建测试文件和目录"""
@staticmethod
async def create_folder(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
parent_id: UUID | None = None,
name: str | None = None,
**kwargs
) -> Object:
"""
创建目录
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
parent_id: 父目录UUIDNone 表示根目录)
name: 目录名称(默认: folder_{随机}
**kwargs: 其他对象字段
返回:
Object: 创建的目录实例
"""
import uuid
if name is None:
name = f"folder_{uuid.uuid4().hex[:8]}"
folder = Object(
name=name,
type=ObjectType.FOLDER,
parent_id=parent_id,
owner_id=owner_id,
policy_id=policy_id,
size=0,
password=kwargs.get("password"),
)
folder = await folder.save(session)
return folder
@staticmethod
async def create_file(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
parent_id: UUID,
name: str | None = None,
size: int = 1024,
**kwargs
) -> Object:
"""
创建文件
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
parent_id: 父目录UUID
name: 文件名称(默认: file_{随机}.txt
size: 文件大小(字节,默认: 1024
**kwargs: 其他对象字段
返回:
Object: 创建的文件实例
"""
import uuid
if name is None:
name = f"file_{uuid.uuid4().hex[:8]}.txt"
file = Object(
name=name,
type=ObjectType.FILE,
parent_id=parent_id,
owner_id=owner_id,
policy_id=policy_id,
size=size,
source_name=kwargs.get("source_name", name),
upload_session_id=kwargs.get("upload_session_id"),
file_metadata=kwargs.get("file_metadata"),
password=kwargs.get("password"),
)
file = await file.save(session)
return file
@staticmethod
async def create_user_root(
session: AsyncSession,
user: User,
policy_id: UUID
) -> Object:
"""
为用户创建根目录
参数:
session: 数据库会话
user: 用户实例
policy_id: 存储策略UUID
返回:
Object: 创建的根目录实例
"""
root = Object(
name=user.username,
type=ObjectType.FOLDER,
parent_id=None,
owner_id=user.id,
policy_id=policy_id,
size=0,
)
root = await root.save(session)
return root
@staticmethod
async def create_directory_tree(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
root_id: UUID,
depth: int = 2,
folders_per_level: int = 2
) -> list[Object]:
"""
创建目录树结构(递归)
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
root_id: 根目录UUID
depth: 树的深度(默认: 2
folders_per_level: 每层的目录数量(默认: 2
返回:
list[Object]: 创建的所有目录列表
"""
folders = []
async def create_level(parent_id: UUID, current_depth: int):
if current_depth <= 0:
return
for i in range(folders_per_level):
folder = await ObjectFactory.create_folder(
session=session,
owner_id=owner_id,
policy_id=policy_id,
parent_id=parent_id,
name=f"level_{current_depth}_folder_{i}"
)
folders.append(folder)
# 递归创建子目录
await create_level(folder.id, current_depth - 1)
await create_level(root_id, depth)
return folders
@staticmethod
async def create_files_in_folder(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
parent_id: UUID,
count: int = 5,
size_range: tuple[int, int] = (1024, 1024 * 1024)
) -> list[Object]:
"""
在指定目录中创建多个文件
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
parent_id: 父目录UUID
count: 文件数量(默认: 5
size_range: 文件大小范围(字节,默认: 1KB - 1MB
返回:
list[Object]: 创建的所有文件列表
"""
import random
files = []
extensions = [".txt", ".pdf", ".jpg", ".png", ".mp4", ".zip", ".doc"]
for i in range(count):
ext = random.choice(extensions)
size = random.randint(size_range[0], size_range[1])
file = await ObjectFactory.create_file(
session=session,
owner_id=owner_id,
policy_id=policy_id,
parent_id=parent_id,
name=f"test_file_{i}{ext}",
size=size
)
files.append(file)
return files
@staticmethod
async def create_large_file(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
parent_id: UUID,
size_mb: int = 100,
name: str | None = None
) -> Object:
"""
创建大文件(用于测试存储限制)
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
parent_id: 父目录UUID
size_mb: 文件大小MB默认: 100
name: 文件名称(默认: large_file_{size_mb}MB.bin
返回:
Object: 创建的大文件实例
"""
if name is None:
name = f"large_file_{size_mb}MB.bin"
size_bytes = size_mb * 1024 * 1024
file = await ObjectFactory.create_file(
session=session,
owner_id=owner_id,
policy_id=policy_id,
parent_id=parent_id,
name=name,
size=size_bytes
)
return file
@staticmethod
async def create_nested_structure(
session: AsyncSession,
owner_id: UUID,
policy_id: UUID,
root_id: UUID
) -> dict[str, UUID]:
"""
创建嵌套的目录和文件结构(用于测试路径解析)
创建结构:
root/
├── documents/
│ ├── work/
│ │ ├── report.pdf
│ │ └── presentation.pptx
│ └── personal/
│ └── notes.txt
└── media/
├── images/
│ ├── photo1.jpg
│ └── photo2.png
└── videos/
└── clip.mp4
参数:
session: 数据库会话
owner_id: 所有者UUID
policy_id: 存储策略UUID
root_id: 根目录UUID
返回:
dict[str, UUID]: 创建的对象ID字典
"""
result = {"root": root_id}
# 创建 documents 目录
documents = await ObjectFactory.create_folder(
session, owner_id, policy_id, root_id, "documents"
)
result["documents"] = documents.id
# 创建 documents/work 目录
work = await ObjectFactory.create_folder(
session, owner_id, policy_id, documents.id, "work"
)
result["work"] = work.id
# 创建 documents/work 下的文件
report = await ObjectFactory.create_file(
session, owner_id, policy_id, work.id, "report.pdf", 1024 * 100
)
result["report"] = report.id
presentation = await ObjectFactory.create_file(
session, owner_id, policy_id, work.id, "presentation.pptx", 1024 * 500
)
result["presentation"] = presentation.id
# 创建 documents/personal 目录
personal = await ObjectFactory.create_folder(
session, owner_id, policy_id, documents.id, "personal"
)
result["personal"] = personal.id
notes = await ObjectFactory.create_file(
session, owner_id, policy_id, personal.id, "notes.txt", 1024
)
result["notes"] = notes.id
# 创建 media 目录
media = await ObjectFactory.create_folder(
session, owner_id, policy_id, root_id, "media"
)
result["media"] = media.id
# 创建 media/images 目录
images = await ObjectFactory.create_folder(
session, owner_id, policy_id, media.id, "images"
)
result["images"] = images.id
photo1 = await ObjectFactory.create_file(
session, owner_id, policy_id, images.id, "photo1.jpg", 1024 * 200
)
result["photo1"] = photo1.id
photo2 = await ObjectFactory.create_file(
session, owner_id, policy_id, images.id, "photo2.png", 1024 * 300
)
result["photo2"] = photo2.id
# 创建 media/videos 目录
videos = await ObjectFactory.create_folder(
session, owner_id, policy_id, media.id, "videos"
)
result["videos"] = videos.id
clip = await ObjectFactory.create_file(
session, owner_id, policy_id, videos.id, "clip.mp4", 1024 * 1024 * 10
)
result["clip"] = clip.id
return result

179
tests/fixtures/users.py vendored Normal file
View File

@@ -0,0 +1,179 @@
"""
用户测试数据工厂
提供创建测试用户的便捷方法。
"""
from uuid import UUID
from sqlmodel.ext.asyncio.session import AsyncSession
from models.user import User
from utils.password.pwd import Password
class UserFactory:
"""用户工厂类,用于创建各种类型的测试用户"""
@staticmethod
async def create(
session: AsyncSession,
group_id: UUID,
username: str | None = None,
password: str | None = None,
**kwargs
) -> User:
"""
创建普通用户
参数:
session: 数据库会话
group_id: 用户组UUID
username: 用户名(默认: test_user_{随机}
password: 明文密码(默认: password123
**kwargs: 其他用户字段
返回:
User: 创建的用户实例
"""
import uuid
if username is None:
username = f"test_user_{uuid.uuid4().hex[:8]}"
if password is None:
password = "password123"
user = User(
username=username,
nickname=kwargs.get("nickname", username),
password=Password.hash(password),
status=kwargs.get("status", True),
storage=kwargs.get("storage", 0),
score=kwargs.get("score", 100),
group_id=group_id,
two_factor=kwargs.get("two_factor"),
avatar=kwargs.get("avatar", "default"),
group_expires=kwargs.get("group_expires"),
theme=kwargs.get("theme", "system"),
language=kwargs.get("language", "zh-CN"),
timezone=kwargs.get("timezone", 8),
previous_group_id=kwargs.get("previous_group_id"),
)
user = await user.save(session)
return user
@staticmethod
async def create_admin(
session: AsyncSession,
admin_group_id: UUID,
username: str | None = None,
password: str | None = None
) -> User:
"""
创建管理员用户
参数:
session: 数据库会话
admin_group_id: 管理员组UUID
username: 用户名(默认: admin_{随机}
password: 明文密码(默认: admin_password
返回:
User: 创建的管理员用户实例
"""
import uuid
if username is None:
username = f"admin_{uuid.uuid4().hex[:8]}"
if password is None:
password = "admin_password"
admin = User(
username=username,
nickname=f"管理员 {username}",
password=Password.hash(password),
status=True,
storage=0,
score=9999,
group_id=admin_group_id,
avatar="default",
)
admin = await admin.save(session)
return admin
@staticmethod
async def create_banned(
session: AsyncSession,
group_id: UUID,
username: str | None = None
) -> User:
"""
创建被封禁用户
参数:
session: 数据库会话
group_id: 用户组UUID
username: 用户名(默认: banned_user_{随机}
返回:
User: 创建的被封禁用户实例
"""
import uuid
if username is None:
username = f"banned_user_{uuid.uuid4().hex[:8]}"
banned_user = User(
username=username,
nickname=f"封禁用户 {username}",
password=Password.hash("banned_password"),
status=False, # 封禁状态
storage=0,
score=0,
group_id=group_id,
avatar="default",
)
banned_user = await banned_user.save(session)
return banned_user
@staticmethod
async def create_with_storage(
session: AsyncSession,
group_id: UUID,
storage_bytes: int,
username: str | None = None
) -> User:
"""
创建已使用指定存储空间的用户
参数:
session: 数据库会话
group_id: 用户组UUID
storage_bytes: 已使用的存储空间(字节)
username: 用户名(默认: storage_user_{随机}
返回:
User: 创建的用户实例
"""
import uuid
if username is None:
username = f"storage_user_{uuid.uuid4().hex[:8]}"
user = User(
username=username,
nickname=username,
password=Password.hash("password123"),
status=True,
storage=storage_bytes,
score=100,
group_id=group_id,
avatar="default",
)
user = await user.save(session)
return user