feat(system): 完成后端系统管理(审计/角色/操作日志/日志查询)
- 操作日志审计机制:audit 上下文(成功与失败都记、独立提交、操作人标量 固化、commit 后查询前置约定),用户/角色全部写操作接入;request_data 按键名的结构化脱敏 + UTF-8 字节边界截断 - 角色管理:CRUD 与启停;admin 禁删禁停、有绑定禁删(FOR UPDATE 锁行)、 编码创建后不可改、停用角色不可再分配 - 日志查询:操作/登录两端点,列表与详情两段式(详情含已脱敏请求参数); 时间参数统一转本地 naive(修混用时区 500 与偏移丢弃) - 用户管理:补齐 reset_password 操作人、编辑空串归一 NULL、并发唯一键兜底 400 - 测试 301 例全绿(单元 223 / 集成 78) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
e22c4bcdc9
commit
c83287819a
32 files changed
+3158
-137
No files matched your search
@@ -0,0 +1,98 @@
|
||||
"""角色请求模型单元测试。
|
||||
|
||||
守的是**角色编码的格式红线**:编码会进入授权判断(`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 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)
|
||||
Reference in new issue
Block a user