Files
FastapiAdmin/backend/tests/test_unit.py
T
zhangtao 73f2823692 refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码
2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑
3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件
4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
2026-06-17 01:56:31 +08:00

424 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""多租户多模块纯单元测试(不依赖数据库连接)。"""
import pytest
# ==================== 01. 租户 ContextVar ====================
class TestTenantContextVar:
"""租户上下文变量读写与清理。"""
def test_initially_none(self) -> None:
from app.core.tenant import get_current_tenant_id, get_is_super_admin
assert get_current_tenant_id() is None
assert get_is_super_admin() is False
def test_set_and_get(self) -> None:
from app.core.tenant import (
clear_current_tenant,
get_current_tenant_id,
get_is_super_admin,
set_current_tenant,
)
set_current_tenant(42, True)
assert get_current_tenant_id() == 42
assert get_is_super_admin() is True
clear_current_tenant()
assert get_current_tenant_id() is None
# ==================== 02. ORM 模型定义 ====================
class TestModels:
"""核心业务模型表名与字段完整性。"""
def test_tenant_model(self) -> None:
from app.api.v1.module_platform.tenant.model import TenantModel
assert TenantModel.__tablename__ == "platform_tenant"
assert hasattr(TenantModel, "name")
assert hasattr(TenantModel, "code")
assert hasattr(TenantModel, "status")
assert hasattr(TenantModel, "end_time")
def test_tenant_quota_fields(self) -> None:
"""测试租户模型中的配额字段(单一大表设计)"""
from app.api.v1.module_platform.tenant.model import TenantModel
assert hasattr(TenantModel, "package_id")
assert hasattr(TenantModel, "start_time")
def test_tenant_config_model(self) -> None:
"""TenantModel 包含配置字段(单一大表设计)"""
from app.api.v1.module_platform.tenant.model import TenantModel
# 配置字段已合并到 TenantModel
assert hasattr(TenantModel, "name")
assert hasattr(TenantModel, "code")
assert hasattr(TenantModel, "status")
def test_package_model_quota(self) -> None:
from app.api.v1.module_platform.package.model import PackageModel
assert hasattr(PackageModel, "max_users")
assert hasattr(PackageModel, "max_roles")
assert hasattr(PackageModel, "max_depts")
assert hasattr(PackageModel, "max_storage_mb")
def test_tenant_user_model(self) -> None:
from app.api.v1.module_platform.tenant.model import TenantUserModel
assert TenantUserModel.__tablename__ == "platform_user_tenant"
assert hasattr(TenantUserModel, "role")
assert hasattr(TenantUserModel, "is_default")
def test_user_model_has_tenant_id(self) -> None:
from app.api.v1.module_system.user.model import UserModel
assert hasattr(UserModel, "tenant_id")
assert hasattr(UserModel, "is_superuser")
def test_role_model_has_tenant_id(self) -> None:
from app.api.v1.module_system.role.model import RoleModel
assert hasattr(RoleModel, "tenant_id")
assert hasattr(RoleModel, "menus")
def test_menu_model_has_type(self) -> None:
from app.api.v1.module_platform.menu.model import MenuModel
assert hasattr(MenuModel, "type")
assert hasattr(MenuModel, "name")
assert hasattr(MenuModel, "permission")
def test_notice_model(self) -> None:
from app.api.v1.module_system.notice.model import NoticeModel
assert NoticeModel.__tablename__ == "sys_notice"
assert hasattr(NoticeModel, "notice_title")
assert hasattr(NoticeModel, "status")
def test_plugin_model(self) -> None:
from app.api.v1.module_platform.plugin.model import PluginModel
assert PluginModel.__tablename__ == "platform_plugin"
assert hasattr(PluginModel, "code")
def test_tenant_plugin_model(self) -> None:
from app.api.v1.module_platform.plugin.model import TenantPluginModel
assert TenantPluginModel.__tablename__ == "platform_tenant_plugin"
def test_ticket_model(self) -> None:
from app.api.v1.module_system.ticket.model import TicketModel
assert TicketModel.__tablename__ == "sys_ticket"
assert hasattr(TicketModel, "ticket_type")
assert hasattr(TicketModel, "reply")
# ==================== 03. Pydantic Schema ====================
class TestSchemas:
"""请求/响应Schema字段验证。"""
def test_tenant_create_schema(self) -> None:
from app.api.v1.module_platform.tenant.schema import TenantCreateSchema
s = TenantCreateSchema(name="test", code="test001", status=0)
assert s.name == "test"
assert s.code == "test001"
def test_tenant_update_schema_partial(self) -> None:
from app.api.v1.module_platform.tenant.schema import TenantUpdateSchema
s = TenantUpdateSchema(name="renamed")
assert s.name == "renamed"
assert s.code is None # 未传则为 None
def test_package_schema_quota(self) -> None:
from app.api.v1.module_platform.package.schema import PackageCreateSchema
s = PackageCreateSchema(name="test", code="test", max_users=100, max_roles=30)
assert s.max_users == 100
assert s.max_roles == 30
# 负数应被拒绝
with pytest.raises(ValueError):
PackageCreateSchema(name="test", code="test", max_users=-1)
def test_ticket_schema(self) -> None:
from app.api.v1.module_system.ticket.schema import TicketCreateSchema
s = TicketCreateSchema(title="test", ticket_type="suggestion")
assert s.title == "test"
assert s.ticket_type == "suggestion"
def test_menu_type_enum(self) -> None:
"""菜单类型: 1=目录 2=菜单 3=按钮 4=链接"""
from app.api.v1.module_platform.menu.model import MenuModel
assert hasattr(MenuModel, "type")
# type 是 int 字段
# ==================== 04. 配置类 ====================
class TestSettings:
"""配置默认值与环境变量覆盖。"""
def test_settings_defaults(self) -> None:
from app.config.setting import settings
assert settings.DATABASE_HOST
assert settings.DATABASE_PORT > 0
assert settings.SERVER_PORT > 0
def test_redis_config(self) -> None:
from app.config.setting import settings
assert isinstance(settings.REDIS_ENABLE, bool)
assert settings.REDIS_PORT == 6379
def test_database_password_empty_by_default(self) -> None:
"""密码默认值为空,需通过 .env 或环境变量提供。"""
from app.config.setting import Settings
# 检查字段默认值(不依赖运行时环境变量)
assert Settings.model_fields["DATABASE_PASSWORD"].default == ""
# ==================== 05. 常量与枚举 ====================
class TestEnums:
"""业务枚举值完整性。"""
def test_status_operate_enum(self) -> None:
from app.common.enums import BusinessType
assert hasattr(BusinessType, "OTHER")
def test_menu_type_values(self) -> None:
"""type: 1=目录 2=菜单 3=按钮 4=链接"""
from app.api.v1.module_platform.menu.model import MenuModel
assert MenuModel.__tablename__ == "platform_menu"
# type 字段是 int 类型
col = next((c for c in MenuModel.__table__.columns if c.name == "type"), None)
assert col is not None
# ==================== 06. 租户自助注册(PRD §4.5====================
class TestTenantRegister:
"""TenantRegisterSchema / TenantRegisterOutSchema 校验。"""
def test_register_schema_valid(self) -> None:
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
s = TenantRegisterSchema(
username="testowner",
password="TestPass123",
email="test@example.com",
tenant_name="测试企业",
)
assert s.username == "testowner"
assert s.email == "test@example.com"
assert s.tenant_name == "测试企业"
def test_register_schema_minimal(self) -> None:
"""仅必填字段。"""
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
s = TenantRegisterSchema(
username="owner2",
password="secret123",
email="owner2@example.com",
)
assert s.tenant_name is None
def test_register_schema_short_password(self) -> None:
"""密码少于 6 位应拒绝。"""
from pydantic import ValidationError
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
with pytest.raises(ValidationError):
TenantRegisterSchema(
username="x",
password="123",
email="a@b.com",
)
def test_register_schema_bad_email(self) -> None:
"""非法邮箱应拒绝。"""
from pydantic import ValidationError
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
with pytest.raises(ValidationError):
TenantRegisterSchema(
username="x",
password="123456",
email="not-an-email",
)
def test_register_out_schema_fields(self) -> None:
"""注册响应 Schema 包含所有必要字段。"""
from app.api.v1.module_system.auth.schema import TenantRegisterOutSchema
fields = set(TenantRegisterOutSchema.model_fields.keys())
for f in ("user_id", "tenant_id", "tenant_name", "tenant_code", "package", "trial_end", "message"):
assert f in fields, f"{f} 未在 TenantRegisterOutSchema 中找到"
# ==================== 07. 支付回调套餐激活 ====================
class TestActivateTenantPackage:
"""PaymentService._activate_tenant_package 日期计算逻辑。"""
def test_new_calculation(self) -> None:
"""order_type=new:设置 start_time=now, end_time=now+period。"""
from datetime import datetime, timedelta
now = datetime(2025, 1, 1, 12, 0, 0)
period_months = 1
duration = timedelta(days=30 * period_months)
# 模拟 _activate_tenant_package 中 new 分支的日期计算
fake = {"package_id": None, "start_time": None, "end_time": None, "status": 1}
fake["package_id"] = 1
fake["start_time"] = now
fake["end_time"] = now + duration
fake["status"] = 0
assert fake["package_id"] == 1
assert fake["start_time"] == now
assert fake["end_time"] == now + timedelta(days=30)
assert fake["status"] == 0
def test_renew_extends_end_time(self) -> None:
"""order_type=renew:在原有 end_time 基础上顺延。"""
from datetime import datetime, timedelta
now = datetime(2025, 1, 1)
original_end = now + timedelta(days=30)
fake = {"end_time": original_end, "status": 1}
# 模拟 renew 分支:base = max(end_time, now) + duration
base = max(fake["end_time"], now)
fake["end_time"] = base + timedelta(days=30)
fake["status"] = 0
assert fake["end_time"] == original_end + timedelta(days=30)
assert fake["status"] == 0
def test_upgrade_changes_package(self) -> None:
"""order_type=upgrade:更换 package_id,状态置为 active。"""
fake = {"package_id": 1, "status": 1}
fake["package_id"] = 2
fake["status"] = 0
assert fake["package_id"] == 2
assert fake["status"] == 0
# ==================== 08. 插件热重载 ====================
class TestPluginReload:
"""discover.reload_dynamic_router 可正常导入且签名正确。"""
def test_reload_function_exists(self) -> None:
from app.core.discover import reload_dynamic_router
assert callable(reload_dynamic_router)
def test_set_app_ref_exists(self) -> None:
from app.core.discover import set_app_ref
assert callable(set_app_ref)
def test_reload_service_exists(self) -> None:
"""PluginService.reload_service 方法存在且为类方法。"""
import inspect
from app.api.v1.module_platform.plugin.service import PluginService
assert hasattr(PluginService, "reload_service")
assert isinstance(
inspect.getattr_static(PluginService, "reload_service"),
(classmethod, classmethod),
)
# ==================== 18. status 字段类型一致性 ====================
class TestStatusFieldType:
"""验证 status 字段从 String(10) 迁移为 SmallInteger 后的一致性"""
def test_model_mixin_status_is_integer(self) -> None:
"""验证 ModelMixin 子类的 status 字段已转为 SmallInteger"""
from app.api.v1.module_system.user.model import UserModel
col = UserModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper(), (
f"UserModel.status 应为 SmallInteger,当前为 {col.type}"
)
def test_tenant_status_is_integer(self) -> None:
from app.api.v1.module_platform.tenant.model import TenantModel
col = TenantModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper(), (
f"TenantModel.status 应为 SmallInteger,当前为 {col.type}"
)
def test_user_status_is_integer(self) -> None:
from app.api.v1.module_system.user.model import UserModel
col = UserModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper()
def test_role_status_is_integer(self) -> None:
from app.api.v1.module_system.role.model import RoleModel
col = RoleModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper()
def test_dept_status_is_integer(self) -> None:
from app.api.v1.module_system.dept.model import DeptModel
col = DeptModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper()
def test_menu_status_is_integer(self) -> None:
from app.api.v1.module_platform.menu.model import MenuModel
col = MenuModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper()
def test_package_status_is_integer(self) -> None:
from app.api.v1.module_platform.package.model import PackageModel
col = PackageModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper(), (
f"PackageModel.status 应为 SmallInteger,当前为 {col.type}"
)
def test_ticket_status_is_integer(self) -> None:
from app.api.v1.module_system.ticket.model import TicketModel
col = TicketModel.__table__.c["status"]
assert "SMALLINT" in str(col.type).upper()