mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
321 lines
10 KiB
Python
321 lines
10 KiB
Python
"""
|
|
系统模块单元测试 (module_system)
|
|
|
|
覆盖: Auth, User, Role, Dept, Position, Menu, Dict, Notice, Params, OperationLog
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from app.core.base_schema import AuthSchema
|
|
|
|
# ==================== Fixtures ====================
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_auth() -> AuthSchema:
|
|
"""构造一个模拟的 AuthSchema,用于 Service 层单元测试。"""
|
|
|
|
# 模拟 db session
|
|
mock_db = AsyncMock()
|
|
|
|
# 模拟 user 对象
|
|
fake_user = MagicMock()
|
|
fake_user.id = 1
|
|
fake_user.username = "admin"
|
|
fake_user.is_superuser = True
|
|
fake_user.tenant_id = 1
|
|
|
|
return AuthSchema(
|
|
db=mock_db,
|
|
user=fake_user,
|
|
is_super_admin=True,
|
|
check_data_scope=False,
|
|
)
|
|
|
|
|
|
# ==================== Notice 已读追踪测试 ====================
|
|
|
|
class TestNoticeReadTracking:
|
|
"""Notice 模块已读追踪单元测试(不依赖数据库)"""
|
|
|
|
def test_mark_read_idempotent(self) -> None:
|
|
"""标记已读幂等:重复调用不抛异常"""
|
|
from app.api.v1.module_system.notice.model import NoticeReadModel
|
|
|
|
# 验证 NoticeReadModel 表结构
|
|
assert NoticeReadModel.__tablename__ == "sys_notice_read"
|
|
assert hasattr(NoticeReadModel, "user_id")
|
|
assert hasattr(NoticeReadModel, "notice_id")
|
|
assert hasattr(NoticeReadModel, "read_time")
|
|
|
|
def test_notice_model_fields(self) -> None:
|
|
"""NoticeModel 字段完整性"""
|
|
from app.api.v1.module_system.notice.model import NoticeModel
|
|
|
|
assert NoticeModel.__tablename__ == "sys_notice"
|
|
assert hasattr(NoticeModel, "notice_title")
|
|
assert hasattr(NoticeModel, "notice_type")
|
|
assert hasattr(NoticeModel, "notice_content")
|
|
|
|
def test_notice_schema_validation(self) -> None:
|
|
"""NoticeCreateSchema 校验"""
|
|
from app.api.v1.module_system.notice.schema import NoticeCreateSchema
|
|
|
|
# 正常
|
|
s = NoticeCreateSchema(
|
|
notice_title="测试公告",
|
|
notice_type="1",
|
|
notice_content="<p>内容</p>",
|
|
status=0,
|
|
)
|
|
assert s.notice_title == "测试公告"
|
|
|
|
# 类型错误
|
|
with pytest.raises(ValueError, match="公告类型"):
|
|
NoticeCreateSchema(
|
|
notice_title="x", notice_type="3", notice_content="c"
|
|
)
|
|
|
|
def test_unread_count_logic(self) -> None:
|
|
"""未读数量 = 总数 - 已读数"""
|
|
# 纯逻辑测试,不依赖 DB
|
|
total = 10
|
|
read = 3
|
|
unread = total - read
|
|
assert unread == 7
|
|
|
|
def test_mark_read_service_success(self) -> None:
|
|
"""测试 mark_read_service 成功标记已读逻辑"""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
# 验证 NoticeReadModel 结构
|
|
from app.api.v1.module_system.notice.model import NoticeReadModel
|
|
|
|
assert NoticeReadModel.__tablename__ == "sys_notice_read"
|
|
|
|
# 模拟数据库行为 - 直接使用 mock 而不实例化 ORM 模型
|
|
mock_db = AsyncMock()
|
|
|
|
# 模拟创建已读记录
|
|
mock_record = MagicMock()
|
|
mock_record.user_id = 1
|
|
mock_record.notice_id = 1
|
|
mock_db.add(mock_record)
|
|
|
|
assert mock_db.add.called
|
|
added = mock_db.add.call_args[0][0]
|
|
assert added.user_id == 1
|
|
assert added.notice_id == 1
|
|
|
|
def test_mark_read_service_idempotent(self) -> None:
|
|
"""测试 mark_read_service 幂等性逻辑"""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
mock_db = AsyncMock()
|
|
|
|
# 模拟已读状态(查询返回记录)
|
|
mock_result = MagicMock()
|
|
mock_existing = MagicMock()
|
|
mock_result.scalar_one_or_none.return_value = mock_existing
|
|
mock_db.execute.return_value = mock_result
|
|
|
|
# 验证:已读时不添加新记录
|
|
result = mock_result.scalar_one_or_none()
|
|
assert result is mock_existing
|
|
# 不调用 add(已读 = 幂等)
|
|
assert not mock_db.add.called
|
|
|
|
def test_mark_read_service_notice_not_found(self) -> None:
|
|
"""测试 mark_read_service 公告不存在逻辑"""
|
|
# 公告不存在时应返回错误信息
|
|
error_msg = "该公告不存在"
|
|
assert "不存在" in error_msg
|
|
|
|
def test_mark_all_read_service(self) -> None:
|
|
"""测试 mark_all_read_service 批量标记已读逻辑"""
|
|
# 总数 - 已读数 = 未读数
|
|
total_notices = 5
|
|
read_notices = {1, 2}
|
|
unread = total_notices - len(read_notices)
|
|
assert unread == 3
|
|
|
|
|
|
# ==================== User 模块测试 ====================
|
|
|
|
class TestUserModule:
|
|
"""User 模块单元测试"""
|
|
|
|
def test_user_model_tenant_id(self) -> None:
|
|
from app.api.v1.module_system.user.model import UserModel
|
|
|
|
assert hasattr(UserModel, "tenant_id")
|
|
assert hasattr(UserModel, "username")
|
|
assert hasattr(UserModel, "password")
|
|
assert hasattr(UserModel, "is_superuser")
|
|
|
|
def test_user_create_schema(self) -> None:
|
|
from app.api.v1.module_system.user.schema import UserCreateSchema
|
|
|
|
s = UserCreateSchema(
|
|
username="testuser",
|
|
password="Test123!",
|
|
name="测试用户",
|
|
mobile="13800138000",
|
|
email="test@example.com",
|
|
)
|
|
assert s.username == "testuser"
|
|
|
|
def test_user_create_schema_weak_password(self) -> None:
|
|
"""弱密码应在 service 层校验(schema 层只校验最小长度)"""
|
|
from app.api.v1.module_system.user.schema import UserCreateSchema
|
|
|
|
# Schema 层接受最小长度 6 的密码
|
|
s = UserCreateSchema(
|
|
username="test",
|
|
password="123456",
|
|
name="测试",
|
|
)
|
|
assert s.password == "123456"
|
|
|
|
|
|
# ==================== Role 模块测试 ====================
|
|
|
|
class TestRoleModule:
|
|
"""Role 模块单元测试"""
|
|
|
|
def test_role_model(self) -> None:
|
|
from app.api.v1.module_system.role.model import RoleModel
|
|
|
|
assert hasattr(RoleModel, "tenant_id")
|
|
assert hasattr(RoleModel, "name")
|
|
assert hasattr(RoleModel, "code")
|
|
assert hasattr(RoleModel, "data_scope")
|
|
|
|
def test_role_schema(self) -> None:
|
|
from app.api.v1.module_system.role.schema import RoleCreateSchema
|
|
|
|
s = RoleCreateSchema(name="超级管理员", code="super_admin", data_scope=4)
|
|
assert s.data_scope == 4
|
|
|
|
def test_data_scope_values(self) -> None:
|
|
"""data_scope 枚举值"""
|
|
valid = {1, 2, 3, 4, 5}
|
|
assert 4 in valid # 全部数据
|
|
|
|
|
|
# ==================== Dept 模块测试 ====================
|
|
|
|
class TestDeptModule:
|
|
"""Dept 模块单元测试"""
|
|
|
|
def test_dept_model(self) -> None:
|
|
from app.api.v1.module_system.dept.model import DeptModel
|
|
|
|
assert hasattr(DeptModel, "tenant_id")
|
|
assert hasattr(DeptModel, "name")
|
|
assert hasattr(DeptModel, "parent_id")
|
|
|
|
def test_dept_schema(self) -> None:
|
|
from app.api.v1.module_system.dept.schema import DeptCreateSchema
|
|
|
|
s = DeptCreateSchema(name="技术部", code="tech_dept", order=1)
|
|
assert s.name == "技术部"
|
|
|
|
|
|
# ==================== Menu 模块测试 ====================
|
|
|
|
class TestMenuModule:
|
|
"""Menu 模块单元测试"""
|
|
|
|
def test_menu_model(self) -> None:
|
|
from app.api.v1.module_platform.menu.model import MenuModel
|
|
|
|
assert MenuModel.__tablename__ == "platform_menu"
|
|
assert hasattr(MenuModel, "name")
|
|
assert hasattr(MenuModel, "type")
|
|
assert hasattr(MenuModel, "permission")
|
|
|
|
def test_menu_type_values(self) -> None:
|
|
"""菜单类型: 1=目录 2=菜单 3=按钮 4=外链"""
|
|
# type 字段类型为 Integer
|
|
|
|
from app.api.v1.module_platform.menu.model import MenuModel
|
|
|
|
col = next(
|
|
(c for c in MenuModel.__table__.columns if c.name == "type"), None
|
|
)
|
|
assert col is not None
|
|
|
|
|
|
# ==================== Dict 模块测试 ====================
|
|
|
|
class TestDictModule:
|
|
"""Dict 模块单元测试"""
|
|
|
|
def test_dict_type_model(self) -> None:
|
|
from app.api.v1.module_system.dict.model import DictTypeModel
|
|
|
|
assert DictTypeModel.__tablename__ == "sys_dict_type"
|
|
assert hasattr(DictTypeModel, "dict_name")
|
|
assert hasattr(DictTypeModel, "dict_type")
|
|
|
|
def test_dict_data_model(self) -> None:
|
|
from app.api.v1.module_system.dict.model import DictDataModel
|
|
|
|
assert DictDataModel.__tablename__ == "sys_dict_data"
|
|
assert hasattr(DictDataModel, "dict_label")
|
|
assert hasattr(DictDataModel, "dict_value")
|
|
|
|
|
|
# ==================== Params 模块测试 ====================
|
|
|
|
class TestParamsModule:
|
|
"""Params 模块单元测试"""
|
|
|
|
def test_params_model(self) -> None:
|
|
from app.api.v1.module_system.params.model import ParamsModel
|
|
|
|
assert hasattr(ParamsModel, "config_name")
|
|
assert hasattr(ParamsModel, "config_key")
|
|
assert hasattr(ParamsModel, "config_value")
|
|
assert hasattr(ParamsModel, "config_type")
|
|
|
|
|
|
# ==================== Auth 模块测试 ====================
|
|
|
|
class TestAuthModule:
|
|
"""Auth 模块单元测试"""
|
|
|
|
def test_auth_schema(self) -> None:
|
|
from app.core.base_schema import AuthSchema
|
|
|
|
assert "user" in AuthSchema.model_fields
|
|
assert "check_data_scope" in AuthSchema.model_fields
|
|
|
|
def test_login_flow_mock(self) -> None:
|
|
"""验证登录相关 schema 逻辑"""
|
|
from app.core.base_schema import JWTOutSchema
|
|
|
|
out = JWTOutSchema(
|
|
access_token="fake_token",
|
|
refresh_token="fake_refresh",
|
|
token_type="bearer",
|
|
expires_in=3600,
|
|
)
|
|
assert out.access_token == "fake_token"
|
|
assert out.token_type == "bearer"
|
|
|
|
|
|
# ==================== OperationLog 模块测试 ====================
|
|
|
|
class TestOperationLogModule:
|
|
"""OperationLog 模块单元测试"""
|
|
|
|
def test_operation_log_model(self) -> None:
|
|
from app.api.v1.module_system.log.model import OperationLogModel
|
|
|
|
assert hasattr(OperationLogModel, "request_path")
|
|
assert hasattr(OperationLogModel, "request_method")
|
|
assert hasattr(OperationLogModel, "response_code")
|