feat: add models for physical files, policies, and user management
- Implement PhysicalFile model to manage physical file references and reference counting. - Create Policy model with associated options and group links for storage policies. - Introduce Redeem and Report models for handling redeem codes and reports. - Add Settings model for site configuration and user settings management. - Develop Share model for sharing objects with unique codes and associated metadata. - Implement SourceLink model for managing download links associated with objects. - Create StoragePack model for managing user storage packages. - Add Tag model for user-defined tags with manual and automatic types. - Implement Task model for managing background tasks with status tracking. - Develop User model with comprehensive user management features including authentication. - Introduce UserAuthn model for managing WebAuthn credentials. - Create WebDAV model for managing WebDAV accounts associated with users.
This commit is contained in:
@@ -8,8 +8,8 @@ import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.user import User
|
||||
from models.group import Group
|
||||
from sqlmodels.user import User
|
||||
from sqlmodels.group import Group
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -62,7 +62,7 @@ async def test_table_base_update(db_session: AsyncSession):
|
||||
group = await group.save(db_session)
|
||||
|
||||
# 更新数据
|
||||
from models.group import GroupBase
|
||||
from sqlmodels.group import GroupBase
|
||||
update_data = GroupBase(name="更新后名称")
|
||||
updated_group = await group.update(db_session, update_data)
|
||||
|
||||
@@ -200,7 +200,7 @@ async def test_timestamps_auto_update(db_session: AsyncSession):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 更新记录
|
||||
from models.group import GroupBase
|
||||
from sqlmodels.group import GroupBase
|
||||
update_data = GroupBase(name="更新后的名称")
|
||||
group = await group.update(db_session, update_data)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Group 和 GroupOptions 模型的单元测试
|
||||
import pytest
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.group import Group, GroupOptions, GroupResponse
|
||||
from sqlmodels.group import Group, GroupOptions, GroupResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -5,21 +5,21 @@ import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.object import Object, ObjectType
|
||||
from models.user import User
|
||||
from models.group import Group
|
||||
from sqlmodels.object import Object, ObjectType
|
||||
from sqlmodels.user import User
|
||||
from sqlmodels.group import Group
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_create_folder(db_session: AsyncSession):
|
||||
"""测试创建目录"""
|
||||
# 创建必要的依赖数据
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="testuser", password="password", group_id=group.id)
|
||||
user = User(email="testuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(
|
||||
@@ -48,12 +48,12 @@ async def test_object_create_folder(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_create_file(db_session: AsyncSession):
|
||||
"""测试创建文件"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="testuser", password="password", group_id=group.id)
|
||||
user = User(email="testuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(
|
||||
@@ -65,7 +65,7 @@ async def test_object_create_file(db_session: AsyncSession):
|
||||
|
||||
# 创建根目录
|
||||
root = Object(
|
||||
name=user.username,
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
@@ -81,7 +81,6 @@ async def test_object_create_file(db_session: AsyncSession):
|
||||
owner_id=user.id,
|
||||
policy_id=policy.id,
|
||||
size=1024,
|
||||
source_name="test_source.txt"
|
||||
)
|
||||
file = await file.save(db_session)
|
||||
|
||||
@@ -89,18 +88,17 @@ async def test_object_create_file(db_session: AsyncSession):
|
||||
assert file.name == "test.txt"
|
||||
assert file.type == ObjectType.FILE
|
||||
assert file.size == 1024
|
||||
assert file.source_name == "test_source.txt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_is_file_property(db_session: AsyncSession):
|
||||
"""测试 is_file 属性"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="testuser", password="password", group_id=group.id)
|
||||
user = User(email="testuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -122,12 +120,12 @@ async def test_object_is_file_property(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_is_folder_property(db_session: AsyncSession):
|
||||
"""测试 is_folder 属性"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="testuser", password="password", group_id=group.id)
|
||||
user = User(email="testuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -148,12 +146,12 @@ async def test_object_is_folder_property(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_root(db_session: AsyncSession):
|
||||
"""测试 get_root() 方法"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="rootuser", password="password", group_id=group.id)
|
||||
user = User(email="rootuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -161,7 +159,7 @@ async def test_object_get_root(db_session: AsyncSession):
|
||||
|
||||
# 创建根目录
|
||||
root = Object(
|
||||
name=user.username,
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
@@ -180,12 +178,12 @@ async def test_object_get_root(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_by_path_root(db_session: AsyncSession):
|
||||
"""测试获取根目录"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="pathuser", password="password", group_id=group.id)
|
||||
user = User(email="pathuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -193,7 +191,7 @@ async def test_object_get_by_path_root(db_session: AsyncSession):
|
||||
|
||||
# 创建根目录
|
||||
root = Object(
|
||||
name=user.username,
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
@@ -202,7 +200,7 @@ async def test_object_get_by_path_root(db_session: AsyncSession):
|
||||
root = await root.save(db_session)
|
||||
|
||||
# 通过路径获取根目录
|
||||
result = await Object.get_by_path(db_session, user.id, "/pathuser", user.username)
|
||||
result = await Object.get_by_path(db_session, user.id, "/")
|
||||
|
||||
assert result is not None
|
||||
assert result.id == root.id
|
||||
@@ -211,12 +209,12 @@ async def test_object_get_by_path_root(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_by_path_nested(db_session: AsyncSession):
|
||||
"""测试获取嵌套路径"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="nesteduser", password="password", group_id=group.id)
|
||||
user = User(email="nesteduser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -224,7 +222,7 @@ async def test_object_get_by_path_nested(db_session: AsyncSession):
|
||||
|
||||
# 创建目录结构: root -> docs -> work -> project
|
||||
root = Object(
|
||||
name=user.username,
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
@@ -263,8 +261,7 @@ async def test_object_get_by_path_nested(db_session: AsyncSession):
|
||||
result = await Object.get_by_path(
|
||||
db_session,
|
||||
user.id,
|
||||
"/nesteduser/docs/work/project",
|
||||
user.username
|
||||
"/docs/work/project",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
@@ -275,12 +272,12 @@ async def test_object_get_by_path_nested(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_by_path_not_found(db_session: AsyncSession):
|
||||
"""测试路径不存在"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="notfounduser", password="password", group_id=group.id)
|
||||
user = User(email="notfounduser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -288,7 +285,7 @@ async def test_object_get_by_path_not_found(db_session: AsyncSession):
|
||||
|
||||
# 创建根目录
|
||||
root = Object(
|
||||
name=user.username,
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
@@ -300,8 +297,7 @@ async def test_object_get_by_path_not_found(db_session: AsyncSession):
|
||||
result = await Object.get_by_path(
|
||||
db_session,
|
||||
user.id,
|
||||
"/notfounduser/nonexistent",
|
||||
user.username
|
||||
"/nonexistent",
|
||||
)
|
||||
|
||||
assert result is None
|
||||
@@ -310,12 +306,12 @@ async def test_object_get_by_path_not_found(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_children(db_session: AsyncSession):
|
||||
"""测试 get_children() 方法"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="childrenuser", password="password", group_id=group.id)
|
||||
user = User(email="childrenuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -362,12 +358,12 @@ async def test_object_get_children(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_parent_child_relationship(db_session: AsyncSession):
|
||||
"""测试父子关系"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="reluser", password="password", group_id=group.id)
|
||||
user = User(email="reluser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -407,12 +403,12 @@ async def test_object_parent_child_relationship(db_session: AsyncSession):
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_unique_constraint(db_session: AsyncSession):
|
||||
"""测试同目录名称唯一约束"""
|
||||
from models.policy import Policy, PolicyType
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(username="uniqueuser", password="password", group_id=group.id)
|
||||
user = User(email="uniqueuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
@@ -450,3 +446,64 @@ async def test_object_unique_constraint(db_session: AsyncSession):
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
await file2.save(db_session)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_get_full_path(db_session: AsyncSession):
|
||||
"""测试 get_full_path() 方法"""
|
||||
from sqlmodels.policy import Policy, PolicyType
|
||||
|
||||
group = Group(name="测试组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(email="pathuser", password="password", group_id=group.id)
|
||||
user = await user.save(db_session)
|
||||
|
||||
policy = Policy(name="本地策略", type=PolicyType.LOCAL, server="/tmp/test")
|
||||
policy = await policy.save(db_session)
|
||||
|
||||
# 创建目录结构: root -> docs -> images -> photo.jpg
|
||||
root = Object(
|
||||
name="/",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=None,
|
||||
owner_id=user.id,
|
||||
policy_id=policy.id
|
||||
)
|
||||
root = await root.save(db_session)
|
||||
|
||||
docs = Object(
|
||||
name="docs",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=root.id,
|
||||
owner_id=user.id,
|
||||
policy_id=policy.id
|
||||
)
|
||||
docs = await docs.save(db_session)
|
||||
|
||||
images = Object(
|
||||
name="images",
|
||||
type=ObjectType.FOLDER,
|
||||
parent_id=docs.id,
|
||||
owner_id=user.id,
|
||||
policy_id=policy.id
|
||||
)
|
||||
images = await images.save(db_session)
|
||||
|
||||
photo = Object(
|
||||
name="photo.jpg",
|
||||
type=ObjectType.FILE,
|
||||
parent_id=images.id,
|
||||
owner_id=user.id,
|
||||
policy_id=policy.id,
|
||||
size=2048
|
||||
)
|
||||
photo = await photo.save(db_session)
|
||||
|
||||
# 测试完整路径
|
||||
full_path = await photo.get_full_path(db_session)
|
||||
assert full_path == "/docs/images/photo.jpg"
|
||||
|
||||
# 测试根目录的 full_path
|
||||
root_path = await root.get_full_path(db_session)
|
||||
assert root_path == "/"
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.setting import Setting, SettingsType
|
||||
from sqlmodels.setting import Setting, SettingsType
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -113,7 +113,7 @@ async def test_setting_update_value(db_session: AsyncSession):
|
||||
setting = await setting.save(db_session)
|
||||
|
||||
# 更新值
|
||||
from models.base import SQLModelBase
|
||||
from sqlmodels.base import SQLModelBase
|
||||
|
||||
class SettingUpdate(SQLModelBase):
|
||||
value: str | None = None
|
||||
|
||||
273
tests/unit/models/test_uri.py
Normal file
273
tests/unit/models/test_uri.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
DiskNextURI 模型的单元测试
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from sqlmodels.uri import DiskNextURI, FileSystemNamespace
|
||||
|
||||
|
||||
class TestDiskNextURIParse:
|
||||
"""测试 URI 解析"""
|
||||
|
||||
def test_parse_my_root(self):
|
||||
"""测试解析个人空间根目录"""
|
||||
uri = DiskNextURI.parse("disknext://my/")
|
||||
assert uri.namespace == FileSystemNamespace.MY
|
||||
assert uri.path == "/"
|
||||
assert uri.fs_id is None
|
||||
assert uri.password is None
|
||||
assert uri.is_root is True
|
||||
|
||||
def test_parse_my_with_path(self):
|
||||
"""测试解析个人空间带路径"""
|
||||
uri = DiskNextURI.parse("disknext://my/docs/readme.md")
|
||||
assert uri.namespace == FileSystemNamespace.MY
|
||||
assert uri.path == "/docs/readme.md"
|
||||
assert uri.fs_id is None
|
||||
assert uri.path_parts == ["docs", "readme.md"]
|
||||
assert uri.is_root is False
|
||||
|
||||
def test_parse_my_with_fs_id(self):
|
||||
"""测试解析带 fs_id 的个人空间"""
|
||||
uri = DiskNextURI.parse("disknext://some-uuid@my/docs")
|
||||
assert uri.namespace == FileSystemNamespace.MY
|
||||
assert uri.fs_id == "some-uuid"
|
||||
assert uri.path == "/docs"
|
||||
|
||||
def test_parse_share_with_code(self):
|
||||
"""测试解析分享链接"""
|
||||
uri = DiskNextURI.parse("disknext://abc123@share/")
|
||||
assert uri.namespace == FileSystemNamespace.SHARE
|
||||
assert uri.fs_id == "abc123"
|
||||
assert uri.path == "/"
|
||||
assert uri.password is None
|
||||
|
||||
def test_parse_share_with_password(self):
|
||||
"""测试解析带密码的分享链接"""
|
||||
uri = DiskNextURI.parse("disknext://abc123:mypass@share/sub/dir")
|
||||
assert uri.namespace == FileSystemNamespace.SHARE
|
||||
assert uri.fs_id == "abc123"
|
||||
assert uri.password == "mypass"
|
||||
assert uri.path == "/sub/dir"
|
||||
|
||||
def test_parse_trash(self):
|
||||
"""测试解析回收站"""
|
||||
uri = DiskNextURI.parse("disknext://trash/")
|
||||
assert uri.namespace == FileSystemNamespace.TRASH
|
||||
assert uri.is_root is True
|
||||
|
||||
def test_parse_with_query(self):
|
||||
"""测试解析带查询参数的 URI"""
|
||||
uri = DiskNextURI.parse("disknext://my/?name=report&type=file")
|
||||
assert uri.namespace == FileSystemNamespace.MY
|
||||
assert uri.query is not None
|
||||
assert uri.query["name"] == "report"
|
||||
assert uri.query["type"] == "file"
|
||||
|
||||
def test_parse_invalid_scheme(self):
|
||||
"""测试无效的协议前缀"""
|
||||
with pytest.raises(ValueError, match="disknext://"):
|
||||
DiskNextURI.parse("http://my/docs")
|
||||
|
||||
def test_parse_invalid_namespace(self):
|
||||
"""测试无效的命名空间"""
|
||||
with pytest.raises(ValueError, match="无效的命名空间"):
|
||||
DiskNextURI.parse("disknext://invalid/docs")
|
||||
|
||||
def test_parse_no_namespace(self):
|
||||
"""测试缺少命名空间"""
|
||||
with pytest.raises(ValueError):
|
||||
DiskNextURI.parse("disknext://")
|
||||
|
||||
|
||||
class TestDiskNextURIBuild:
|
||||
"""测试 URI 构建"""
|
||||
|
||||
def test_build_simple(self):
|
||||
"""测试简单构建"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY)
|
||||
assert uri.namespace == FileSystemNamespace.MY
|
||||
assert uri.path == "/"
|
||||
assert uri.fs_id is None
|
||||
|
||||
def test_build_with_path(self):
|
||||
"""测试带路径构建"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/readme.md")
|
||||
assert uri.path == "/docs/readme.md"
|
||||
|
||||
def test_build_path_auto_prefix(self):
|
||||
"""测试路径自动添加 / 前缀"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="docs/readme.md")
|
||||
assert uri.path == "/docs/readme.md"
|
||||
|
||||
def test_build_with_fs_id(self):
|
||||
"""测试带 fs_id 构建"""
|
||||
uri = DiskNextURI.build(
|
||||
FileSystemNamespace.SHARE,
|
||||
fs_id="abc123",
|
||||
password="secret",
|
||||
)
|
||||
assert uri.fs_id == "abc123"
|
||||
assert uri.password == "secret"
|
||||
|
||||
|
||||
class TestDiskNextURIToString:
|
||||
"""测试 URI 序列化"""
|
||||
|
||||
def test_to_string_simple(self):
|
||||
"""测试简单序列化"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY)
|
||||
assert uri.to_string() == "disknext://my/"
|
||||
|
||||
def test_to_string_with_path(self):
|
||||
"""测试带路径序列化"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/readme.md")
|
||||
assert uri.to_string() == "disknext://my/docs/readme.md"
|
||||
|
||||
def test_to_string_with_fs_id(self):
|
||||
"""测试带 fs_id 序列化"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, fs_id="uuid-123")
|
||||
assert uri.to_string() == "disknext://uuid-123@my/"
|
||||
|
||||
def test_to_string_with_password(self):
|
||||
"""测试带密码序列化"""
|
||||
uri = DiskNextURI.build(
|
||||
FileSystemNamespace.SHARE,
|
||||
fs_id="code",
|
||||
password="pass",
|
||||
)
|
||||
assert uri.to_string() == "disknext://code:pass@share/"
|
||||
|
||||
def test_to_string_roundtrip(self):
|
||||
"""测试序列化-反序列化往返"""
|
||||
original = "disknext://abc123:pass@share/sub/dir"
|
||||
uri = DiskNextURI.parse(original)
|
||||
result = uri.to_string()
|
||||
assert result == original
|
||||
|
||||
|
||||
class TestDiskNextURIId:
|
||||
"""测试 id() 方法"""
|
||||
|
||||
def test_id_with_fs_id(self):
|
||||
"""测试有 fs_id 时返回 fs_id"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, fs_id="my-uuid")
|
||||
assert uri.id("default") == "my-uuid"
|
||||
|
||||
def test_id_without_fs_id(self):
|
||||
"""测试无 fs_id 时返回默认值"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY)
|
||||
assert uri.id("default-uuid") == "default-uuid"
|
||||
|
||||
def test_id_without_fs_id_no_default(self):
|
||||
"""测试无 fs_id 且无默认值时返回 None"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY)
|
||||
assert uri.id() is None
|
||||
|
||||
|
||||
class TestDiskNextURIJoin:
|
||||
"""测试 join() 方法"""
|
||||
|
||||
def test_join_single(self):
|
||||
"""测试拼接单个路径元素"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs")
|
||||
joined = uri.join("readme.md")
|
||||
assert joined.path == "/docs/readme.md"
|
||||
|
||||
def test_join_multiple(self):
|
||||
"""测试拼接多个路径元素"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY)
|
||||
joined = uri.join("docs", "work", "report.pdf")
|
||||
assert joined.path == "/docs/work/report.pdf"
|
||||
|
||||
def test_join_preserves_metadata(self):
|
||||
"""测试 join 保留 namespace 和 fs_id"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.SHARE, fs_id="code123")
|
||||
joined = uri.join("sub")
|
||||
assert joined.namespace == FileSystemNamespace.SHARE
|
||||
assert joined.fs_id == "code123"
|
||||
|
||||
|
||||
class TestDiskNextURIDirUri:
|
||||
"""测试 dir_uri() 方法"""
|
||||
|
||||
def test_dir_uri_file(self):
|
||||
"""测试获取文件的父目录 URI"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/readme.md")
|
||||
parent = uri.dir_uri()
|
||||
assert parent.path == "/docs/"
|
||||
|
||||
def test_dir_uri_root(self):
|
||||
"""测试根目录的 dir_uri 返回自身"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/")
|
||||
parent = uri.dir_uri()
|
||||
assert parent.path == "/"
|
||||
|
||||
|
||||
class TestDiskNextURIRoot:
|
||||
"""测试 root() 方法"""
|
||||
|
||||
def test_root_resets_path(self):
|
||||
"""测试 root 重置路径"""
|
||||
uri = DiskNextURI.build(
|
||||
FileSystemNamespace.MY,
|
||||
path="/docs/work/report.pdf",
|
||||
fs_id="uuid-123",
|
||||
)
|
||||
root = uri.root()
|
||||
assert root.path == "/"
|
||||
assert root.fs_id == "uuid-123"
|
||||
assert root.namespace == FileSystemNamespace.MY
|
||||
|
||||
|
||||
class TestDiskNextURIName:
|
||||
"""测试 name() 方法"""
|
||||
|
||||
def test_name_file(self):
|
||||
"""测试获取文件名"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/readme.md")
|
||||
assert uri.name() == "readme.md"
|
||||
|
||||
def test_name_directory(self):
|
||||
"""测试获取目录名"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/work")
|
||||
assert uri.name() == "work"
|
||||
|
||||
def test_name_root(self):
|
||||
"""测试根目录的 name 返回空字符串"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/")
|
||||
assert uri.name() == ""
|
||||
|
||||
|
||||
class TestDiskNextURIProperties:
|
||||
"""测试属性方法"""
|
||||
|
||||
def test_path_parts(self):
|
||||
"""测试路径分割"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs/work/report.pdf")
|
||||
assert uri.path_parts == ["docs", "work", "report.pdf"]
|
||||
|
||||
def test_path_parts_root(self):
|
||||
"""测试根路径分割"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/")
|
||||
assert uri.path_parts == []
|
||||
|
||||
def test_is_root_true(self):
|
||||
"""测试 is_root 为真"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/")
|
||||
assert uri.is_root is True
|
||||
|
||||
def test_is_root_false(self):
|
||||
"""测试 is_root 为假"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs")
|
||||
assert uri.is_root is False
|
||||
|
||||
def test_str_representation(self):
|
||||
"""测试字符串表示"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs")
|
||||
assert str(uri) == "disknext://my/docs"
|
||||
|
||||
def test_repr(self):
|
||||
"""测试 repr"""
|
||||
uri = DiskNextURI.build(FileSystemNamespace.MY, path="/docs")
|
||||
assert "disknext://my/docs" in repr(uri)
|
||||
@@ -5,8 +5,8 @@ import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.user import User, ThemeType, UserPublic
|
||||
from models.group import Group
|
||||
from sqlmodels.user import User, ThemeType, UserPublic
|
||||
from sqlmodels.group import Group
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -18,7 +18,7 @@ async def test_user_create(db_session: AsyncSession):
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="testuser@test.local",
|
||||
nickname="测试用户",
|
||||
password="hashed_password",
|
||||
group_id=group.id
|
||||
@@ -26,7 +26,7 @@ async def test_user_create(db_session: AsyncSession):
|
||||
user = await user.save(db_session)
|
||||
|
||||
assert user.id is not None
|
||||
assert user.username == "testuser"
|
||||
assert user.email == "testuser@test.local"
|
||||
assert user.nickname == "测试用户"
|
||||
assert user.status is True
|
||||
assert user.storage == 0
|
||||
@@ -34,15 +34,15 @@ async def test_user_create(db_session: AsyncSession):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_unique_username(db_session: AsyncSession):
|
||||
"""测试用户名唯一约束"""
|
||||
async def test_user_unique_email(db_session: AsyncSession):
|
||||
"""测试邮箱唯一约束"""
|
||||
# 创建用户组
|
||||
group = Group(name="默认组")
|
||||
group = await group.save(db_session)
|
||||
|
||||
# 创建第一个用户
|
||||
user1 = User(
|
||||
username="duplicate",
|
||||
email="duplicate@test.local",
|
||||
password="password1",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -50,7 +50,7 @@ async def test_user_unique_username(db_session: AsyncSession):
|
||||
|
||||
# 尝试创建同名用户
|
||||
user2 = User(
|
||||
username="duplicate",
|
||||
email="duplicate@test.local",
|
||||
password="password2",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -68,7 +68,7 @@ async def test_user_to_public(db_session: AsyncSession):
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
username="publicuser",
|
||||
email="publicuser@test.local",
|
||||
nickname="公开用户",
|
||||
password="secret_password",
|
||||
storage=1024,
|
||||
@@ -82,7 +82,7 @@ async def test_user_to_public(db_session: AsyncSession):
|
||||
|
||||
assert isinstance(public_user, UserPublic)
|
||||
assert public_user.id == user.id
|
||||
assert public_user.username == "publicuser"
|
||||
assert public_user.email == "publicuser@test.local"
|
||||
# 注意: UserPublic.nick 字段名与 User.nickname 不同,
|
||||
# model_validate 不会自动映射,所以 nick 为 None
|
||||
# 这是已知的设计问题,需要在 UserPublic 中添加别名或重命名字段
|
||||
@@ -101,7 +101,7 @@ async def test_user_group_relationship(db_session: AsyncSession):
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
username="vipuser",
|
||||
email="vipuser@test.local",
|
||||
password="password",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -125,7 +125,7 @@ async def test_user_status_default(db_session: AsyncSession):
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(
|
||||
username="defaultuser",
|
||||
email="defaultuser@test.local",
|
||||
password="password",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -141,7 +141,7 @@ async def test_user_storage_default(db_session: AsyncSession):
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(
|
||||
username="storageuser",
|
||||
email="storageuser@test.local",
|
||||
password="password",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -158,7 +158,7 @@ async def test_user_theme_enum(db_session: AsyncSession):
|
||||
|
||||
# 测试默认值
|
||||
user1 = User(
|
||||
username="user1",
|
||||
email="user1@test.local",
|
||||
password="password",
|
||||
group_id=group.id
|
||||
)
|
||||
@@ -167,7 +167,7 @@ async def test_user_theme_enum(db_session: AsyncSession):
|
||||
|
||||
# 测试设置为 LIGHT
|
||||
user2 = User(
|
||||
username="user2",
|
||||
email="user2@test.local",
|
||||
password="password",
|
||||
theme=ThemeType.LIGHT,
|
||||
group_id=group.id
|
||||
@@ -177,7 +177,7 @@ async def test_user_theme_enum(db_session: AsyncSession):
|
||||
|
||||
# 测试设置为 DARK
|
||||
user3 = User(
|
||||
username="user3",
|
||||
email="user3@test.local",
|
||||
password="password",
|
||||
theme=ThemeType.DARK,
|
||||
group_id=group.id
|
||||
|
||||
@@ -4,8 +4,8 @@ Login 服务的单元测试
|
||||
import pytest
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from models.user import User, LoginRequest, TokenResponse
|
||||
from models.group import Group
|
||||
from sqlmodels.user import User, LoginRequest, TokenResponse
|
||||
from sqlmodels.group import Group
|
||||
from service.user.login import login
|
||||
from utils.password.pwd import Password
|
||||
|
||||
@@ -20,7 +20,7 @@ async def setup_user(db_session: AsyncSession):
|
||||
# 创建正常用户
|
||||
plain_password = "secure_password_123"
|
||||
user = User(
|
||||
username="loginuser",
|
||||
email="loginuser@test.local",
|
||||
password=Password.hash(plain_password),
|
||||
status=True,
|
||||
group_id=group.id
|
||||
@@ -41,7 +41,7 @@ async def setup_banned_user(db_session: AsyncSession):
|
||||
group = await group.save(db_session)
|
||||
|
||||
user = User(
|
||||
username="banneduser",
|
||||
email="banneduser@test.local",
|
||||
password=Password.hash("password"),
|
||||
status=False, # 封禁状态
|
||||
group_id=group.id
|
||||
@@ -61,7 +61,7 @@ async def setup_2fa_user(db_session: AsyncSession):
|
||||
|
||||
secret = pyotp.random_base32()
|
||||
user = User(
|
||||
username="2fauser",
|
||||
email="2fauser@test.local",
|
||||
password=Password.hash("password"),
|
||||
status=True,
|
||||
two_factor=secret,
|
||||
@@ -82,7 +82,7 @@ async def test_login_success(db_session: AsyncSession, setup_user):
|
||||
user_data = setup_user
|
||||
|
||||
login_request = LoginRequest(
|
||||
username="loginuser",
|
||||
email="loginuser@test.local",
|
||||
password=user_data["password"]
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ async def test_login_success(db_session: AsyncSession, setup_user):
|
||||
async def test_login_user_not_found(db_session: AsyncSession):
|
||||
"""测试用户不存在"""
|
||||
login_request = LoginRequest(
|
||||
username="nonexistent_user",
|
||||
email="nonexistent@test.local",
|
||||
password="any_password"
|
||||
)
|
||||
|
||||
@@ -112,7 +112,7 @@ async def test_login_user_not_found(db_session: AsyncSession):
|
||||
async def test_login_wrong_password(db_session: AsyncSession, setup_user):
|
||||
"""测试密码错误"""
|
||||
login_request = LoginRequest(
|
||||
username="loginuser",
|
||||
email="loginuser@test.local",
|
||||
password="wrong_password"
|
||||
)
|
||||
|
||||
@@ -125,7 +125,7 @@ async def test_login_wrong_password(db_session: AsyncSession, setup_user):
|
||||
async def test_login_user_banned(db_session: AsyncSession, setup_banned_user):
|
||||
"""测试用户被封禁"""
|
||||
login_request = LoginRequest(
|
||||
username="banneduser",
|
||||
email="banneduser@test.local",
|
||||
password="password"
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ async def test_login_2fa_required(db_session: AsyncSession, setup_2fa_user):
|
||||
user_data = setup_2fa_user
|
||||
|
||||
login_request = LoginRequest(
|
||||
username="2fauser",
|
||||
email="2fauser@test.local",
|
||||
password=user_data["password"]
|
||||
# 未提供 two_fa_code
|
||||
)
|
||||
@@ -156,7 +156,7 @@ async def test_login_2fa_invalid(db_session: AsyncSession, setup_2fa_user):
|
||||
user_data = setup_2fa_user
|
||||
|
||||
login_request = LoginRequest(
|
||||
username="2fauser",
|
||||
email="2fauser@test.local",
|
||||
password=user_data["password"],
|
||||
two_fa_code="000000" # 错误的验证码
|
||||
)
|
||||
@@ -179,7 +179,7 @@ async def test_login_2fa_success(db_session: AsyncSession, setup_2fa_user):
|
||||
valid_code = totp.now()
|
||||
|
||||
login_request = LoginRequest(
|
||||
username="2fauser",
|
||||
email="2fauser@test.local",
|
||||
password=user_data["password"],
|
||||
two_fa_code=valid_code
|
||||
)
|
||||
@@ -198,7 +198,7 @@ async def test_login_returns_valid_tokens(db_session: AsyncSession, setup_user):
|
||||
user_data = setup_user
|
||||
|
||||
login_request = LoginRequest(
|
||||
username="loginuser",
|
||||
email="loginuser@test.local",
|
||||
password=user_data["password"]
|
||||
)
|
||||
|
||||
@@ -217,17 +217,17 @@ async def test_login_returns_valid_tokens(db_session: AsyncSession, setup_user):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_case_sensitive_username(db_session: AsyncSession, setup_user):
|
||||
"""测试用户名大小写敏感"""
|
||||
async def test_login_case_sensitive_email(db_session: AsyncSession, setup_user):
|
||||
"""测试邮箱大小写敏感"""
|
||||
user_data = setup_user
|
||||
|
||||
# 使用大写用户名登录(如果数据库是 loginuser)
|
||||
# 使用大写邮箱登录
|
||||
login_request = LoginRequest(
|
||||
username="LOGINUSER",
|
||||
email="LOGINUSER@TEST.LOCAL",
|
||||
password=user_data["password"]
|
||||
)
|
||||
|
||||
result = await login(db_session, login_request)
|
||||
|
||||
# 应该失败,因为用户名大小写不匹配
|
||||
# 应该失败,因为邮箱大小写不匹配
|
||||
assert result is None
|
||||
|
||||
@@ -72,9 +72,9 @@ def test_password_verify_expired():
|
||||
@pytest.mark.asyncio
|
||||
async def test_totp_generate():
|
||||
"""测试 TOTP 密钥生成"""
|
||||
username = "testuser"
|
||||
email = "testuser@test.local"
|
||||
|
||||
response = await Password.generate_totp(username)
|
||||
response = await Password.generate_totp(email)
|
||||
|
||||
assert response.setup_token is not None
|
||||
assert response.uri is not None
|
||||
@@ -82,7 +82,7 @@ async def test_totp_generate():
|
||||
assert isinstance(response.uri, str)
|
||||
# TOTP URI 格式: otpauth://totp/...
|
||||
assert response.uri.startswith("otpauth://totp/")
|
||||
assert username in response.uri
|
||||
assert email in response.uri
|
||||
|
||||
|
||||
def test_totp_verify_valid():
|
||||
|
||||
Reference in New Issue
Block a user