按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""角色请求模型单元测试。
|
||
|
||
守的是**角色编码的格式红线**:编码会进入授权判断(`ROLE_ADMIN` 按编码
|
||
比对)与操作日志,必须限定为小写字母开头的小写/数字/下划线组合 ——
|
||
放开大写或中文会让对照与检索困难,超长则与列宽(32)不符。
|
||
"""
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from app.schemas.role import RoleCreateIn, RoleQuery, RoleStatusIn, RoleUpdateIn
|
||
|
||
|
||
def _create(**overrides: object) -> RoleCreateIn:
|
||
base: dict[str, object] = {"role_code": "finance", "role_name": "财务"}
|
||
base.update(overrides)
|
||
return RoleCreateIn(**base)
|
||
|
||
|
||
class TestRoleCodePattern:
|
||
"""编码规则:^[a-z][a-z0-9_]{1,31}$"""
|
||
|
||
def test_合法编码通过(self) -> None:
|
||
assert _create().role_code == "finance"
|
||
|
||
def test_下划线与数字通过(self) -> None:
|
||
assert _create(role_code="sales_01").role_code == "sales_01"
|
||
|
||
def test_大写字母被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="Finance")
|
||
|
||
def test_中文被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="财务")
|
||
|
||
def test_数字开头被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="1abc")
|
||
|
||
def test_下划线开头被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="_abc")
|
||
|
||
def test_单字符被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="a")
|
||
|
||
def test_超长被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_create(role_code="a" * 33)
|
||
|
||
def test_首尾空白被自动去除(self) -> None:
|
||
assert _create(role_code=" finance ").role_code == "finance"
|
||
|
||
|
||
class TestInitialPermissions:
|
||
def test_旧创建请求兼容且默认不授权(self):
|
||
assert _create().permission_codes is None
|
||
|
||
def test_初始授权无需手动填写修改原因(self):
|
||
payload = _create(permission_codes=["base:personnel:view"])
|
||
assert payload.reason is None
|
||
|
||
@pytest.mark.parametrize("reason", ["", " "])
|
||
def test_显式提供的原因不能是空白(self, reason):
|
||
with pytest.raises(ValidationError):
|
||
_create(permission_codes=["base:personnel:view"], reason=reason)
|
||
|
||
def test_初始权限和原因保留(self):
|
||
payload = _create(permission_codes=["base:personnel:view"], reason=" 增加资料查看职责 ")
|
||
assert payload.permission_codes == ["base:personnel:view"]
|
||
assert payload.reason == "增加资料查看职责"
|
||
|
||
|
||
class TestRoleUpdate:
|
||
"""编辑模型:不含 role_code。"""
|
||
|
||
def test_编辑不接受role_code(self) -> None:
|
||
"""编码是授权判定锚点,编辑接口有意不接收;多余字段被忽略而非报错
|
||
(前端表单可能整份回填提交)。"""
|
||
payload = RoleUpdateIn(role_name="财务二组", role_code="hacked")
|
||
assert not hasattr(payload, "role_code")
|
||
|
||
def test_名称为空被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
RoleUpdateIn(role_name="")
|
||
|
||
def test_排序负数被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
RoleUpdateIn(role_name="财务", sort_order=-1)
|
||
|
||
|
||
class TestRoleStatusAndQuery:
|
||
"""状态与查询模型。"""
|
||
|
||
def test_状态只能为0或1(self) -> None:
|
||
assert RoleStatusIn(status=0).status == 0
|
||
assert RoleStatusIn(status=1).status == 1
|
||
with pytest.raises(ValidationError):
|
||
RoleStatusIn(status=2)
|
||
|
||
def test_查询默认分页(self) -> None:
|
||
query = RoleQuery()
|
||
assert query.page_num == 1
|
||
assert query.page_size == 10
|
||
assert query.keyword is None
|
||
assert query.status is None
|
||
|
||
def test_分页别名照常工作(self) -> None:
|
||
"""pageNum/pageSize 是前端契约(PageQuery 的 alias)。"""
|
||
query = RoleQuery(pageNum=3, pageSize=5)
|
||
assert query.offset == 10
|
||
|
||
def test_关键词超长被拒(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
RoleQuery(keyword="a" * 65)
|