权限管理系统 (perms-system-server) — 全路径覆盖测试设计
测试范围: API (go-zero REST, 全 POST) + gRPC (status codes) + Model 层 (_gen.go 模板生成 + 自定义方法) + Logic 单元测试 + util 层 + 访问控制 + UserDetailsLoader
测试报告与代码审计详见 test-report.md
一、系统架构与逻辑链路
1.1 整体调用链路
HTTP Client gRPC Client
│ │
▼ ▼
rest.Server (go-zero) zrpc.Server (go-zero)
│ (全部 POST 路由) │
▼ ▼
Handler 层 (JSON Body 解析) PermServer (permserver.go)
│ (status.Error + codes.Xxx)
▼ │
JwtAuth Middleware (鉴权/上下文注入) │
│ │
▼ ▼
Logic 层 (业务逻辑) ◄────── 共享 ────► authHelper (jwt.go / perms.go)
│ │
▼ ▼
util 层 (NormalizePage / IsValidEmail / IsValidPhone)
│
▼
Model 层 (go-zero sqlc + cache + TransactCtx + 批量查询)
│ ├── _gen.go (自定义模板生成: CRUD/Batch/WithTx/缓存管理)
│ └── 自定义方法 (分页/按条件查询/级联删除等)
▼
MySQL (InnoDB) + Redis Cache
1.2 Model 层接口全景
共 9 个 Model,每个包含:
| 层级 |
方法类别 |
数量/模型 |
来源 |
| _gen.go 基础 CRUD |
Insert, InsertWithTx, FindOne, FindOneWithTx, Update, UpdateWithTx, Delete, DeleteWithTx |
8 |
自定义模板 |
| _gen.go 批量操作 |
BatchInsert, BatchInsertWithTx, BatchUpdate, BatchUpdateWithTx, BatchDelete, BatchDeleteWithTx |
6 |
自定义模板 |
| _gen.go 唯一索引查询 |
FindOneBy{UniqueField}, FindOneBy{UniqueField}WithTx (因表而异) |
0~2 组 |
自定义模板 |
| _gen.go 内部辅助 |
TransactCtx, TableName, findListByPrimaryKeys, getPrimaryKeyValue, buildBatchUpdateQuery, formatPrimary, queryPrimary |
7 |
自定义模板 |
| 自定义方法 |
分页查询/按条件查询/级联删除/批量ID查询等 |
3~7 |
手写 |
1.3 权限计算逻辑链路
输入: userId + deptId + productCode + isSuperAdmin(bool)
│
├─ isSuperAdmin=true → 产品全部启用权限 + "SUPER_ADMIN"
├─ 非产品成员 → nil + ""
├─ DEVELOPER/ADMIN → 产品全部启用权限 + memberType
├─ MEMBER + deptId>0 + dept.DeptType="DEV" → 产品全部启用权限 + "MEMBER"
├─ MEMBER + deptId>0 + dept查询失败/DeptType≠"DEV" → 继续走角色权限流程
└─ MEMBER → (角色权限 ∪ ALLOW) - DENY → 过滤 status=1
二、REST API 测试用例
注意: 所有路由统一为 POST 方法,请求参数均通过 JSON Body 传递。
2.1 产品端登录 POST /api/auth/login
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0001 |
POST /api/auth/login |
正常登录(普通用户+productCode) |
{"username":"user1","password":"123456","productCode":"test"} |
code=0, accessToken/refreshToken/userInfo |
正常路径 |
P0 |
loginLogic全路径 |
| TC-0002 |
POST /api/auth/login |
正常登录-带productCode+ADMIN成员 |
{"username":"user1","password":"123456","productCode":"test"} |
code=0, perms含用户可用权限, memberType="ADMIN" |
正常路径 |
P0 |
GetUserPerms(false) MEMBER分支 |
| TC-0003 |
POST /api/auth/login |
超管通过产品端登录被拒绝 |
{"username":"super","password":"x","productCode":"p1"} |
code=403, "超级管理员不允许通过产品端登录,请使用管理后台" |
安全 |
P0 |
IsSuperAdmin==1 → ErrForbidden |
| TC-0004 |
POST /api/auth/login |
超管无productCode被拒绝 |
{"username":"super","password":"x"} |
code=403, "超级管理员不允许通过产品端登录,请使用管理后台" |
安全 |
P0 |
IsSuperAdmin==1 → ErrForbidden |
| TC-0005 |
POST /api/auth/login |
用户不存在 |
{"username":"notexist","password":"x"} |
code=401, "用户名或密码错误" |
异常路径 |
P0 |
ErrNotFound分支 |
| TC-0006 |
POST /api/auth/login |
DB异常(非ErrNotFound) |
FindOneByUsername连接失败 |
code=500, "服务器内部错误" |
异常路径 |
P1 |
透传err→Setup兜底 |
| TC-0007 |
POST /api/auth/login |
密码错误 |
{"username":"admin","password":"wrong"} |
code=401 |
异常路径 |
P0 |
bcrypt比对失败 |
| TC-0008 |
POST /api/auth/login |
账号冻结 |
status=2用户 |
code=403, "账号已被冻结" |
分支覆盖 |
P0 |
u.Status!=1 |
| TC-0009 |
POST /api/auth/login |
非产品成员 |
productCode指向用户不属于的产品 |
code=403, "您不是该产品的成员" |
安全 |
P0 |
非成员禁止登录 |
| TC-0010 |
POST /api/auth/login |
DEVELOPER成员 |
DEVELOPER类型成员 |
perms全量, memberType="DEVELOPER" |
分支覆盖 |
P1 |
perms.go DEVELOPER分支 |
| TC-0011 |
POST /api/auth/login |
SQL注入 |
{"username":"' OR 1=1 --","password":"x"} |
code=401 |
安全 |
P0 |
参数化查询 |
| TC-0012 |
POST /api/auth/login |
缺少必填字段 |
{} |
HTTP 400 |
边界 |
P1 |
httpx.Parse校验(productCode现为必填) |
| TC-0013 |
POST /api/auth/login |
产品成员被禁用时拒绝登录 |
member.status=Disabled |
403 "您的产品成员资格已被禁用" |
安全 |
P0 |
H-3: loginService |
| TC-0014 |
POST /api/auth/login |
产品被禁用时拒绝登录 |
product.status=Disabled |
403 "该产品已被禁用" |
安全 |
P0 |
H-3: loginService |
2.1b 管理后台登录 POST /api/auth/adminLogin
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0015 |
POST /api/auth/adminLogin |
超管正常登录 |
{"username":"super","password":"x","managementKey":"valid"} |
code=0, accessToken/refreshToken/userInfo, isSuperAdmin=1, memberType="SUPER_ADMIN", perms为空 |
正常路径 |
P0 |
adminLoginLogic全路径 |
| TC-0016 |
POST /api/auth/adminLogin |
普通用户被拒绝 |
{"username":"user1","password":"x","managementKey":"valid"} |
code=401, "用户名或密码错误" |
安全 |
P0 |
审计M-7修复: 统一错误消息防用户枚举 |
| TC-0017 |
POST /api/auth/adminLogin |
managementKey无效 |
{"username":"user1","password":"x","managementKey":"wrong"} |
code=401, "managementKey无效" |
安全 |
P0 |
第一个校验点 |
| TC-0018 |
POST /api/auth/adminLogin |
managementKey为空 |
{"username":"user1","password":"x","managementKey":""} |
code=401, "managementKey无效" |
安全 |
P0 |
空字符串≠config值 |
| TC-0019 |
POST /api/auth/adminLogin |
用户不存在 |
{"username":"notexist","password":"x","managementKey":"valid"} |
code=401, "用户名或密码错误" |
异常路径 |
P0 |
ErrNotFound分支 |
| TC-0020 |
POST /api/auth/adminLogin |
密码错误 |
{"username":"user1","password":"wrong","managementKey":"valid"} |
code=401, "用户名或密码错误" |
异常路径 |
P0 |
bcrypt比对失败 |
| TC-0021 |
POST /api/auth/adminLogin |
账号冻结 |
status=2用户 |
code=401, "用户名或密码错误" |
安全 |
P0 |
审计M-7修复: 冻结/非超管统一返回同一错误防枚举 |
| TC-0022 |
POST /api/auth/adminLogin |
不带productCode时perms为空 |
管理后台登录超管 |
userInfo.perms为空, memberType="SUPER_ADMIN"(超管标记由Loader自动填充) |
功能验证 |
P0 |
Load(ctx, uid, "") |
| TC-0023 |
POST /api/auth/adminLogin |
缺少必填字段 |
{} |
HTTP 400 |
边界 |
P1 |
httpx.Parse校验 |
| TC-0024 |
POST /api/auth/adminLogin |
SQL注入username |
{"username":"' OR 1=1 --","password":"x","managementKey":"valid"} |
code=401 |
安全 |
P0 |
参数化查询 |
| TC-0025 |
POST /api/auth/adminLogin |
adminLogin 用户名限流 |
对同一用户名连续多次失败登录 |
触发后返回 429 "请求过于频繁" |
安全 |
P0 |
H-2: 防用户名枚举爆破 |
2.2 刷新Token POST /api/auth/refreshToken
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0026 |
POST /api/auth/refreshToken |
正常刷新 |
Header Authorization: Bearer <refreshToken> |
code=0, 新accessToken, 新refreshToken(token轮转,保留原始过期时间) |
正常路径 |
P0 |
refreshTokenLogic全路径 |
| TC-0027 |
POST /api/auth/refreshToken |
不带productCode(回退) |
Header Authorization, 无productCode |
使用claims.ProductCode |
分支覆盖 |
P1 |
productCode=""回退 |
| TC-0028 |
POST /api/auth/refreshToken |
token无效 |
Header Authorization: Bearer invalid |
code=401 |
异常路径 |
P0 |
ParseRefreshToken失败 |
| TC-0029 |
POST /api/auth/refreshToken |
用户已删除 |
token中userId不存在 |
code=403, "账号已被冻结" |
异常路径 |
P1 |
UserDetailsLoader返回Status=0 |
| TC-0030 |
POST /api/auth/refreshToken |
账号冻结 |
冻结用户 |
code=403 |
分支覆盖 |
P0 |
Status!=1 |
| TC-0031 |
POST /api/auth/refreshToken |
超管+productCode(token中已含相同pc) |
isSuperAdmin=1, token中productCode=pc, req.ProductCode=pc |
refreshToken原样返回, SUPER_ADMIN权限 |
分支覆盖 |
P1 |
isSuperAdmin分支+productCode不变 |
| TC-0032 |
POST /api/auth/refreshToken |
尝试切换产品被拒绝 |
token中productCode="p1", req.ProductCode="p2" |
code=400, "刷新令牌不允许切换产品" |
安全 |
P0 |
H-02修复: 禁止跨产品切换 |
| TC-0033 |
POST /api/auth/refreshToken |
TokenVersion不匹配时拒绝刷新 |
refreshToken含tokenVersion=999, DB中tokenVersion=0 |
401 "登录状态已失效,请重新登录" |
安全 |
P0 |
claims.TokenVersion != ud.TokenVersion |
| TC-0034 |
POST /api/auth/refreshToken |
使用accessToken作为refreshToken被拒绝 |
用accessSecret签发的accessToken作为refreshToken传入 |
401 "refreshToken无效或已过期" |
安全 |
P0 |
ParseRefreshToken校验TokenType!=refresh |
| TC-0035 |
POST /api/auth/refreshToken |
产品成员已移除时拒绝刷新 |
refreshToken含productCode, 但用户已从该产品移除 |
403 "您已不是该产品的成员" |
安全 |
P0 |
ud.MemberType=="" && !ud.IsSuperAdmin |
2.3 同步权限 POST /api/perm/sync
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0036 |
POST /api/perm/sync |
全部新增 |
{"appKey":"ak","appSecret":"as","perms":[{"code":"x","name":"y"}]} |
code=0, added=1, updated=0, disabled=0 |
正常路径 |
P0 |
toInsert→BatchInsert |
| TC-0037 |
POST /api/perm/sync |
更新已有(名称变更) |
已存在code但name不同 |
updated=1 |
正常路径 |
P0 |
toUpdate→BatchUpdate |
| TC-0038 |
POST /api/perm/sync |
无变化 |
已存在且name/remark/status均相同 |
added=0, updated=0 |
分支覆盖 |
P1 |
跳过更新 |
| TC-0039 |
POST /api/perm/sync |
禁用权限重启 |
已status=2的权限在列表中 |
updated=1, status恢复1 |
分支覆盖 |
P1 |
Status!=1条件 |
| TC-0040 |
POST /api/perm/sync |
移除不在列表的权限 |
DB有多余权限 |
disabled>0 |
正常路径 |
P0 |
DisableNotInCodes |
| TC-0041 |
POST /api/perm/sync |
空perms数组被拒绝 |
{"...","perms":[]} |
code=400, "权限列表不能为空" |
输入校验 |
P0 |
空列表校验,防止意外批量禁用 |
| TC-0042 |
POST /api/perm/sync |
验证disabled返回值 |
已知DB有5条,perms仅含2条 |
disabled=3 |
功能验证 |
P0 |
RowsAffected() |
| TC-0043 |
POST /api/perm/sync |
appKey无效 |
{"appKey":"invalid"} |
code=401 |
异常路径 |
P0 |
FindOneByAppKey失败 |
| TC-0044 |
POST /api/perm/sync |
appSecret错误 |
secret不匹配 |
code=401 |
异常路径 |
P0 |
AppSecret比对 |
| TC-0045 |
POST /api/perm/sync |
产品已禁用 |
product.Status!=1 |
code=403 |
分支覆盖 |
P0 |
Status!=1 |
| TC-0046 |
POST /api/perm/sync |
大批量(1000条) |
1000条perms |
added=1000 |
性能 |
P2 |
BatchInsert性能 |
| TC-0047 |
POST /api/perm/sync |
重复code去重 |
perms中包含两个相同code |
仅处理一次, added=1(而非2) |
分支覆盖 |
P0 |
M-09修复: seen去重 |
| TC-0048 |
POST /api/perm/sync |
事务保护-中途失败回滚 |
模拟BatchUpdate失败 |
全部操作回滚, 返回SyncPermsError(500,"同步权限事务失败"), 不透传DB错误 |
事务验证 |
P0 |
H-05/H-03 修复:LockByCodeTx→FindMapByProductCodeWithTx→BatchInsert→BatchUpdate 任一失败统一 500 |
2.4 获取用户信息 POST /api/auth/userInfo
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0049 |
POST /api/auth/userInfo |
正常获取-含productCode |
Bearer token (含productCode) |
code=0, 完整UserInfo+实时perms |
正常路径 |
P0 |
userInfoLogic全路径 |
| TC-0050 |
POST /api/auth/userInfo |
不含productCode |
Bearer token (无productCode) |
perms为空 |
分支覆盖 |
P1 |
productCode="" |
| TC-0051 |
POST /api/auth/userInfo |
未登录 |
无Authorization头 |
code=401, "未登录" |
异常路径 |
P0 |
middleware拦截 |
| TC-0052 |
POST /api/auth/userInfo |
token过期 |
过期token |
code=401 |
异常路径 |
P0 |
middleware |
| TC-0053 |
POST /api/auth/userInfo |
userId=0 |
伪造claims |
code=401, "未登录" |
分支覆盖 |
P1 |
userId==0 |
2.5 修改密码 POST /api/auth/changePassword
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0054 |
POST /api/auth/changePassword |
正常修改 |
{"oldPassword":"123456","newPassword":"654321"} |
code=0 |
正常路径 |
P0 |
changePasswordLogic全路径 |
| TC-0055 |
POST /api/auth/changePassword |
mustChangePassword重置 |
正常修改后 |
DB中mustChangePassword=2 |
功能验证 |
P0 |
user.MustChangePassword=2 |
| TC-0056 |
POST /api/auth/changePassword |
原密码错误 |
{"oldPassword":"wrong","newPassword":"newpwd"} |
code=400, "原密码错误" |
异常路径 |
P0 |
bcrypt失败 |
| TC-0057 |
POST /api/auth/changePassword |
新密码少于8字符 |
{"oldPassword":"old","newPassword":"Pas1234"} |
code=400, "密码长度不能少于8个字符" |
输入校验 |
P0 |
len<8 |
| TC-0058 |
POST /api/auth/changePassword |
新密码恰好8字符(含大小写+数字) |
{"oldPassword":"old","newPassword":"Abcdef1x"} |
code=0 |
边界 |
P1 |
len==8,含大小写+数字 |
| TC-0059 |
POST /api/auth/changePassword |
新密码空字符串 |
{"oldPassword":"old","newPassword":""} |
code=400 |
边界 |
P0 |
len("")=0<8 |
| TC-0060 |
POST /api/auth/changePassword |
新密码超过72字符 |
{"oldPassword":"old","newPassword":"a*73"} |
code=400, "密码长度不能超过72个字符" |
输入校验 |
P0 |
len>72 |
| TC-0061 |
POST /api/auth/changePassword |
新密码恰好72字符 |
{"oldPassword":"old","newPassword":"a*72"} |
code=0 |
边界 |
P1 |
len==72 |
| TC-0062 |
POST /api/auth/changePassword |
新旧密码相同 |
{"oldPassword":"123456","newPassword":"123456"} |
code=400, "新密码不能与原密码相同" |
输入校验 |
P0 |
OldPassword==NewPassword |
| TC-0063 |
POST /api/auth/changePassword |
用户不存在 |
token中userId已删除 |
code=404 |
异常路径 |
P1 |
FindOne失败 |
2.6 创建产品 POST /api/product/create
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0064 |
POST /api/product/create |
正常创建 |
{"code":"new","name":"新产品"} |
code=0, id/appKey/appSecret/adminUser/adminPassword |
正常路径 |
P0 |
TransactCtx全路径 |
| TC-0065 |
POST /api/product/create |
事务回滚-用户创建失败 |
模拟InsertWithTx User失败 |
返回错误, DB无新产品 |
事务验证 |
P0 |
TransactCtx回滚 |
| TC-0066 |
POST /api/product/create |
事务回滚-成员创建失败 |
模拟InsertWithTx Member失败 |
产品和用户均回滚 |
事务验证 |
P0 |
TransactCtx回滚 |
| TC-0067 |
POST /api/product/create |
编码已存在 |
{"code":"existing","name":"x"} |
code=409 |
异常路径 |
P0 |
FindOneByCode成功 |
| TC-0068 |
POST /api/product/create |
并发创建同编码 |
两请求同时 |
一成功一冲突 |
并发 |
P1 |
uk_code |
| TC-0069 |
POST /api/product/create |
createProduct 含空格被拒绝 |
code="abc def" |
400 "产品编码格式不合法" |
输入校验 |
P0 |
productCodeRegexp |
| TC-0070 |
POST /api/product/create |
createProduct 含特殊字符被拒绝 |
code="abc@def" |
400 |
输入校验 |
P0 |
productCodeRegexp |
| TC-0071 |
POST /api/product/create |
createProduct 全中文被拒绝 |
code="产品一" |
400 |
输入校验 |
P0 |
productCodeRegexp |
| TC-0072 |
POST /api/product/create |
createProduct 纯数字开头被拒绝 |
code="1abc" |
400 |
输入校验 |
P0 |
productCodeRegexp 首字符限定 |
| TC-0073 |
POST /api/product/create |
createProduct 空字符串被拒绝 |
code="" |
400 |
边界 |
P0 |
|
| TC-0074 |
POST /api/product/create |
createProduct 长度>64 被拒绝 |
code="a"*65 |
400 "产品编码长度不能超过64个字符" |
边界 |
P0 |
len>64 |
| TC-0075 |
POST /api/product/create |
createProduct 合法编码(含下划线/中划线/数字) |
code="pc_01-test" |
创建成功 |
正常路径 |
P0 |
Regexp 正向匹配 |
2.7 产品更新/列表/详情
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0076 |
POST /api/product/update |
正常更新 |
{"id":1,"name":"新名","status":1} |
code=0 |
正常路径 |
P0 |
updateProductLogic |
| TC-0077 |
POST /api/product/update |
不存在 |
{"id":9999,"name":"x"} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0078 |
POST /api/product/update |
不传status |
{"id":1,"name":"x"} |
status不变 |
分支覆盖 |
P1 |
Status>0 |
TC-0079~0083 |
POST /api/product/list 分页边界 |
正常/默认/超限/0/负值 |
— |
— |
— |
— |
已删除:分页边界由 util.TestNormalizePage 单元测试覆盖;列表语义被 M-2 拆分为"超管走 FindList / 非超管只看自己"两条独立契约(TC-0850、TC-0871) |
TC-0084~0085 |
POST /api/product/detail 正常/不存在 |
— |
— |
— |
— |
— |
已删除:被 M-2 契约合并改写为 TC-0852(他人产品 → 404)、TC-0853(自己产品 AppKey 脱敏)、TC-0872(FindOne 错误 → 404 无差别响应) |
TC-0086 / TC-0088 |
非超管 AppKey 隐藏 |
— |
— |
— |
— |
— |
已删除:由 TC-0850(list)+ TC-0853(detail)覆盖 |
TC-0087 / TC-0089 |
超管可见 AppKey |
— |
— |
— |
— |
— |
已删除:由 TC-0854(detail)+ TC-0871(list)覆盖 |
| TC-0090 |
POST /api/product/update |
updateProduct 非法状态值被拒绝 |
status=99 |
400 "产品状态值无效" |
输入校验 |
P0 |
H-4: 仅允许 1/2 |
2.8 创建部门 POST /api/dept/create
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0091 |
POST /api/dept/create |
创建顶级部门 |
{"parentId":0,"name":"总部"} |
code=0, path="/{id}/" |
正常路径 |
P0 |
TransactCtx, parentPath="/" |
| TC-0092 |
POST /api/dept/create |
创建子部门 |
{"parentId":1,"name":"技术部"} |
code=0, path=parent.path+id+"/" |
正常路径 |
P0 |
parentId>0分支 |
| TC-0093 |
POST /api/dept/create |
父部门不存在 |
{"parentId":9999,"name":"x"} |
code=404, "父部门不存在" |
异常路径 |
P0 |
FindOneWithTx失败 |
| TC-0094 |
POST /api/dept/create |
不传DeptType默认NORMAL |
{"parentId":0,"name":"x"} |
DB deptType="NORMAL" |
分支覆盖 |
P0 |
deptType=""→DeptTypeNormal |
| TC-0095 |
POST /api/dept/create |
传DeptType=DEV |
{"parentId":0,"name":"x","deptType":"DEV"} |
DB deptType="DEV" |
正常路径 |
P0 |
req.DeptType赋值 |
| TC-0096 |
POST /api/dept/create |
事务内FindOneWithTx可见性 |
TransactCtx内InsertWithTx后FindOneWithTx |
事务内可读到未提交数据 |
事务验证 |
P0 |
FindOneWithTx(session) |
| TC-0097 |
POST /api/dept/create |
事务回滚-Insert失败 |
模拟InsertWithTx失败 |
DB无新记录 |
事务验证 |
P0 |
TransactCtx回滚 |
| TC-0098 |
POST /api/dept/create |
事务回滚-UpdateWithTx失败 |
模拟UpdateWithTx失败 |
Insert也回滚 |
事务验证 |
P1 |
TransactCtx回滚 |
| TC-0099 |
POST /api/dept/create |
多层嵌套(5层) |
递归创建5层 |
path正确拼接 |
深度测试 |
P2 |
path逻辑 |
| TC-0100 |
POST /api/dept/create |
通过Logic创建+验证Path |
CreateDeptLogic.CreateDept→FindOne |
path包含/{id}/ |
集成验证 |
P0 |
FindOneWithTx修复后端到端 |
2.9 部门更新/删除/树
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0101 |
POST /api/dept/update |
正常更新 |
{"id":1,"name":"新名","sort":5} |
code=0 |
正常路径 |
P0 |
updateDeptLogic |
| TC-0102 |
POST /api/dept/update |
不存在 |
{"id":9999,"name":"x"} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0103 |
POST /api/dept/update |
DeptType NORMAL→DEV |
{"id":1,"deptType":"DEV"} |
DB deptType="DEV" |
正常路径 |
P0 |
DeptType合法值更新 |
| TC-0104 |
POST /api/dept/update |
DeptType无效值返回错误 |
{"id":1,"deptType":"INVALID"} |
code=400, "部门类型无效", DB deptType不变 |
输入校验 |
P0 |
DeptType校验,仅NORMAL/DEV |
| TC-0105 |
POST /api/dept/update |
DeptType变更时级联清除子部门用户缓存 |
部门从NORMAL改为DEV,有子部门含用户 |
code=0, 子部门下用户缓存被清除 |
缓存验证 |
P0 |
M-10修复: 级联缓存失效 |
| TC-0106 |
POST /api/dept/delete |
正常删除(无子部门) |
{"id":5} |
code=0 |
正常路径 |
P0 |
deleteDeptLogic |
| TC-0107 |
POST /api/dept/delete |
有子部门 |
{"id":1} |
code=400, "存在子部门" |
业务约束 |
P0 |
len(children)>0 |
| TC-0108 |
POST /api/dept/delete |
不存在的部门 |
{"id":9999} |
code=0(Delete对不存在行不报错) |
边界 |
P1 |
FindByParentId空+Delete |
| TC-0109 |
POST /api/dept/delete |
部门下有关联用户 |
部门id指向含用户的部门 |
code=400, "该部门下仍有关联用户,无法删除" |
业务约束 |
P0 |
H-07修复: 检查关联用户 |
TC-0110~0112 |
POST /api/dept/tree 正常/空/孤儿 |
— |
— |
— |
— |
— |
已删除:M-2 后 DeptTree 按 caller 身份剪枝,旧测试假定任何身份可拿全树已不成立;新契约由 TC-0855/0856/0857(deptTreeAccessControl_audit_test.go)覆盖,"孤儿→根"行为隐含在 fullAccess 路径中 |
2.10 权限列表 POST /api/perm/list
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0113 |
POST /api/perm/list |
正常查询 |
{"productCode":"p1","page":1,"pageSize":10} |
code=0, total/list |
正常路径 |
P0 |
permListLogic |
| TC-0114 |
POST /api/perm/list |
默认分页 |
{"productCode":"p1"} |
page=1, pageSize=20 |
分支覆盖 |
P1 |
NormalizePage |
| TC-0115 |
POST /api/perm/list |
pageSize超过上限 |
{"productCode":"p1","pageSize":200} |
实际pageSize=100 |
边界 |
P0 |
NormalizePage cap |
| TC-0116 |
POST /api/perm/list |
不存在的productCode |
{"productCode":"notexist"} |
total=0, list=[] |
边界 |
P1 |
空结果 |
2.11 角色管理
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0117 |
POST /api/role/create |
正常创建 |
{"productCode":"p1","name":"管理员","permsLevel":1} |
code=0, id>0 |
正常路径 |
P0 |
createRoleLogic |
| TC-0118 |
POST /api/role/create |
重复角色名 |
同产品同名 |
code=409, "该产品下角色名已存在" |
业务约束 |
P0 |
Duplicate entry→ErrConflict |
| TC-0119 |
POST /api/role/create |
并发同名创建 |
两请求同时 |
一成功一冲突409 |
并发 |
P1 |
唯一索引+1062捕获 |
| TC-0120 |
POST /api/role/update |
正常更新 |
{"id":1,"name":"新名","permsLevel":2} |
code=0 |
正常路径 |
P0 |
updateRoleLogic |
| TC-0121 |
POST /api/role/update |
不存在 |
{"id":9999,...} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0122 |
POST /api/role/list |
正常查询 |
{"productCode":"p1","page":1,"pageSize":10} |
code=0 |
正常路径 |
P0 |
roleListLogic |
| TC-0123 |
POST /api/role/list |
pageSize超过上限 |
{"productCode":"p1","pageSize":200} |
实际pageSize=100 |
边界 |
P0 |
NormalizePage cap |
| TC-0124 |
POST /api/role/detail |
正常查询 |
{"id":1} |
code=0, 含permIds |
正常路径 |
P0 |
roleDetailLogic |
| TC-0125 |
POST /api/role/detail |
不存在 |
{"id":9999} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
2.12 删除角色 POST /api/role/delete
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0126 |
POST /api/role/delete |
正常删除+级联 |
{"id":5} (含权限/用户绑定) |
code=0, role_perm/user_role同步清理 |
正常+事务 |
P0 |
TransactCtx全路径 |
| TC-0127 |
POST /api/role/delete |
事务回滚 |
模拟DeleteWithTx失败 |
级联删除回滚 |
事务验证 |
P0 |
TransactCtx |
| TC-0128 |
POST /api/role/delete |
无关联数据 |
新角色无绑定 |
code=0 |
分支覆盖 |
P1 |
删0条 |
2.13 绑定角色权限 POST /api/role/bindPerms
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0129 |
POST /api/role/bindPerms |
正常绑定 |
{"roleId":1,"permIds":[1,2,3]} |
code=0 |
正常路径 |
P0 |
TransactCtx |
| TC-0130 |
POST /api/role/bindPerms |
角色不存在 |
{"roleId":9999,"permIds":[1]} |
code=404, "角色不存在" |
存在性校验 |
P0 |
FindOne预检 |
| TC-0131 |
POST /api/role/bindPerms |
清空权限 |
{"roleId":1,"permIds":[]} |
code=0, 全清空 |
分支覆盖 |
P1 |
len==0→return |
| TC-0132 |
POST /api/role/bindPerms |
重复permId |
{"roleId":1,"permIds":[1,1]} |
DB唯一索引→事务回滚 |
边界 |
P1 |
uk_role_perm |
| TC-0133 |
POST /api/role/bindPerms |
事务回滚 |
模拟BatchInsertWithTx失败 |
旧数据回滚还原 |
事务验证 |
P0 |
TransactCtx |
2.14 创建用户 POST /api/user/create
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0134 |
POST /api/user/create |
正常创建 |
{"username":"new","password":"123456"} |
code=0, id>0 |
正常路径 |
P0 |
createUserLogic |
| TC-0135 |
POST /api/user/create |
用户名已存在(预检) |
{"username":"existing","password":"x"} |
code=409, "用户名已存在" |
异常路径 |
P0 |
FindOneByUsername成功 |
| TC-0136 |
POST /api/user/create |
带完整可选字段 |
含nickname/email/phone/remark/deptId |
code=0 |
正常路径 |
P1 |
各字段赋值 |
| TC-0137 |
POST /api/user/create |
非法email格式 |
{"...","email":"not-an-email"} |
code=400, "邮箱格式不正确" |
输入校验 |
P0 |
util.IsValidEmail |
| TC-0138 |
POST /api/user/create |
合法email |
{"...","email":"[email protected]"} |
code=0 |
正常路径 |
P1 |
IsValidEmail通过 |
| TC-0139 |
POST /api/user/create |
email为空(可选) |
{"...","email":""} |
code=0, 跳过校验 |
分支覆盖 |
P1 |
email!=""判断 |
| TC-0140 |
POST /api/user/create |
非法phone格式 |
{"...","phone":"abc"} |
code=400, "手机号格式不正确" |
输入校验 |
P0 |
util.IsValidPhone |
| TC-0141 |
POST /api/user/create |
合法phone(国际) |
{"...","phone":"+8613800138000"} |
code=0 |
正常路径 |
P1 |
IsValidPhone通过 |
| TC-0142 |
POST /api/user/create |
phone为空(可选) |
{"...","phone":""} |
code=0, 跳过校验 |
分支覆盖 |
P1 |
phone!=""判断 |
| TC-0143 |
POST /api/user/create |
并发同username(TOCTOU) |
两请求同时 |
一成功一冲突(1062) |
并发 |
P0 |
Duplicate entry→ErrConflict |
| TC-0144 |
POST /api/user/create |
唯一索引冲突消息 |
预检通过后DB冲突 |
code=409, "用户名已存在" |
异常路径 |
P0 |
strings.Contains "1062" |
| TC-0145 |
POST /api/user/create |
密码少于8字符 |
{"username":"x","password":"Pas1234"} |
code=400, "密码长度不能少于8个字符" |
输入校验 |
P0 |
H-10修复: 密码强度校验(8+字符,含大小写+数字) |
| TC-0146 |
POST /api/user/create |
密码缺少大写字母 |
{"username":"x","password":"pass123456"} |
code=400, "密码必须包含大写字母、小写字母和数字" |
输入校验 |
P0 |
密码复杂性: 无大写 |
| TC-0147 |
POST /api/user/create |
密码缺少小写字母 |
{"username":"x","password":"PASS123456"} |
code=400, "密码必须包含大写字母、小写字母和数字" |
输入校验 |
P0 |
密码复杂性: 无小写 |
| TC-0148 |
POST /api/user/create |
密码缺少数字 |
{"username":"x","password":"Passpasspass"} |
code=400, "密码必须包含大写字母、小写字母和数字" |
输入校验 |
P0 |
密码复杂性: 无数字 |
| TC-0149 |
POST /api/user/create |
密码超过72字符 |
{"username":"x","password":"a*73"} |
code=400, "密码长度不能超过72个字符" |
输入校验 |
P0 |
H-10修复: 密码强度校验 |
| TC-0150 |
POST /api/user/create |
用户名含特殊字符被拒绝 |
{"username":"user@name!","password":"pass123456"} |
400 "用户名只能包含字母、数字和下划线,长度2-64个字符" |
输入校验 |
P0 |
usernameRegexp不匹配 |
| TC-0151 |
POST /api/user/create |
用户名太短(1字符)被拒绝 |
{"username":"a","password":"pass123456"} |
400 "用户名只能包含字母、数字和下划线,长度2-64个字符" |
边界值 |
P0 |
最小长度2 |
| TC-0152 |
POST /api/user/create |
用户名太长(65字符)被拒绝 |
{"username":"a*65","password":"pass123456"} |
400 "用户名只能包含字母、数字和下划线,长度2-64个字符" |
边界值 |
P0 |
最大长度64 |
| TC-0153 |
POST /api/user/create |
部门不存在被拒绝 |
{"username":"x","password":"pass123456","deptId":999999999} |
400 "部门不存在" |
异常路径 |
P1 |
DeptId>0时校验FindOne |
| TC-0154 |
POST /api/user/create |
昵称超过64字符被拒绝 |
{"username":"x","password":"pass123456","nickname":"n*65"} |
400 "昵称长度不能超过64个字符" |
边界值 |
P1 |
len(Nickname)>64 |
| TC-0155 |
POST /api/user/create |
备注超过255字符被拒绝 |
{"username":"x","password":"pass123456","remark":"r*256"} |
400 "备注长度不能超过255个字符" |
边界值 |
P1 |
len(Remark)>255 |
2.15 用户更新 POST /api/user/update (指针类型+DeptId可清零)
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0156 |
POST /api/user/update |
正常更新 |
{"id":1,"nickname":"n","email":"[email protected]"} |
code=0 |
正常路径 |
P0 |
updateUserLogic |
| TC-0157 |
POST /api/user/update |
不存在 |
{"id":9999} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0158 |
POST /api/user/update |
仅传id |
{"id":1} |
仅updateTime变 |
分支覆盖 |
P1 |
所有指针nil |
| TC-0159 |
POST /api/user/update |
清空nickname |
{"id":1,"nickname":""} |
DB nickname→空字符串 |
功能 |
P0 |
*string非nil+空值 |
| TC-0160 |
POST /api/user/update |
清空email |
{"id":1,"email":""} |
DB email→空字符串(跳过校验) |
功能 |
P0 |
*email!=""判断 |
| TC-0161 |
POST /api/user/update |
清空remark |
{"id":1,"remark":""} |
DB remark清空 |
功能 |
P1 |
*string→"" |
| TC-0162 |
POST /api/user/update |
非法email格式 |
{"id":1,"email":"bad-email"} |
code=400, "邮箱格式不正确" |
输入校验 |
P0 |
util.IsValidEmail |
| TC-0163 |
POST /api/user/update |
非法phone格式 |
{"id":1,"phone":"12345"} |
code=400, "手机号格式不正确" |
输入校验 |
P0 |
util.IsValidPhone |
| TC-0164 |
POST /api/user/update |
合法phone |
{"id":1,"phone":"+8613800138000"} |
code=0 |
正常路径 |
P1 |
IsValidPhone通过 |
| TC-0165 |
POST /api/user/update |
不传email(nil) |
{"id":1,"nickname":"x"} |
email不变 |
分支覆盖 |
P1 |
req.Email==nil |
| TC-0166 |
POST /api/user/update |
DeptId设为0(取消部门) |
{"id":1,"deptId":0} |
DB deptId→0 |
功能 |
P0 |
*int64, *req.DeptId=0 |
| TC-0167 |
POST /api/user/update |
DeptId设为正值 |
{"id":1,"deptId":5} |
DB deptId→5 |
正常路径 |
P0 |
*int64指针 |
| TC-0168 |
POST /api/user/update |
DeptId不传(nil) |
{"id":1,"nickname":"x"} |
deptId不变 |
分支覆盖 |
P1 |
req.DeptId==nil |
| TC-0169 |
POST /api/user/update |
超管不能冻结另一超管 |
caller=超管A, target=超管B, status=2 |
403 "不能通过此接口修改超级管理员的状态" |
安全 |
P0 |
H-2: IsSuperAdmin==Yes 保护 |
| TC-0170 |
POST /api/user/update |
updateUser-产品管理员可管理范围内用户 |
ctx=ADMIN, target在管理范围内 |
更新成功 |
正常路径 |
P0 |
Audit#4修复: CheckManageAccess允许产品管理员 |
| TC-0171 |
POST /api/user/update |
updateUser-昵称超长拒绝 |
nickname=65字符 |
400 "昵称长度不能超过64个字符" |
边界 |
P1 |
输入校验 |
| TC-0172 |
POST /api/user/update |
updateUser-部门不存在 |
deptId=999999 |
400 "部门不存在" |
异常路径 |
P1 |
关联对象不存在校验 |
| TC-0173 |
POST /api/user/update |
updateUser 修改状态时递增 tokenVersion |
req.Status=Disabled, 原Status=Enabled |
更新成功, tokenVersion+1 |
正常路径 |
P0 |
H-1: 状态变更强制下线 |
| TC-0174 |
POST /api/user/update |
updateUser 仅改 profile 不递增 tokenVersion |
req.Nickname+Email |
更新成功, tokenVersion不变 |
正常路径 |
P0 |
H-1: 非状态字段不影响会话 |
| TC-0175 |
POST /api/user/update |
updateUser 乐观锁冲突 -> 409 |
基于过期 updateTime 更新 |
返回 CodeError(409, "数据已被其他操作修改...") |
并发/异常 |
P0 |
H-1: ErrUpdateConflict 透传 |
2.16 用户列表/详情/状态 及其他用户操作
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0176 |
POST /api/user/list |
含productCode |
{"productCode":"p1","page":1,"pageSize":10} |
每用户含memberType(批量查) |
正常路径 |
P0 |
FindMapByProductCodeUserIds |
| TC-0177 |
POST /api/user/list |
不含productCode |
{"page":1} |
memberType全空,不调批量查 |
分支覆盖 |
P1 |
productCode="" |
| TC-0178 |
POST /api/user/list |
pageSize超过上限 |
{"pageSize":500} |
实际pageSize=100 |
边界 |
P0 |
NormalizePage cap |
| TC-0179 |
POST /api/user/list |
用户不在产品中 |
productCode指定,部分用户不是成员 |
memberType为空 |
分支覆盖 |
P1 |
memberMap无对应key |
| TC-0180 |
POST /api/user/list |
批量查询DB异常 |
FindMapByProductCodeUserIds失败 |
code=500 |
异常路径 |
P1 |
err→透传 |
| TC-0181 |
POST /api/user/detail |
正常查询 |
{"id":1} |
含roleIds |
正常路径 |
P0 |
userDetailLogic |
| TC-0182 |
POST /api/user/detail |
正常查询-含Avatar |
有Avatar用户 |
avatar字段非空 |
分支覆盖 |
P1 |
Avatar.Valid=true |
| TC-0183 |
POST /api/user/detail |
不存在 |
{"id":9999} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0184 |
POST /api/user/bindRoles |
正常绑定 |
{"userId":1,"roleIds":[1,2]} |
code=0 |
正常路径 |
P0 |
TransactCtx |
| TC-0185 |
POST /api/user/bindRoles |
用户不存在 |
{"userId":9999,"roleIds":[1]} |
code=404, "用户不存在" |
存在性校验 |
P0 |
FindOne预检 |
| TC-0186 |
POST /api/user/bindRoles |
清空角色 |
{"userId":1,"roleIds":[]} |
code=0 |
分支覆盖 |
P1 |
len==0 |
| TC-0187 |
POST /api/user/bindRoles |
事务回滚 |
模拟失败 |
旧数据还原 |
事务验证 |
P0 |
TransactCtx |
| TC-0188 |
POST /api/user/bindRoles |
角色不属于当前产品 |
roleId属于其他产品 |
code=400, "角色不属于当前产品" |
安全 |
P0 |
H-03修复: 校验角色归属 |
| TC-0189 |
POST /api/user/bindRoles |
角色已禁用 |
roleId状态为禁用 |
code=400, "角色已禁用" |
安全 |
P0 |
H-03修复: 校验角色状态 |
| TC-0190 |
POST /api/user/bindRoles |
角色不存在 |
roleId不存在 |
code=400, "角色不存在" |
安全 |
P0 |
H-03修复: 校验角色存在 |
| TC-0191 |
POST /api/user/bindRoles |
非产品成员绑定角色被拒绝 |
目标用户非当前产品成员 |
400 "不是当前产品的成员" |
安全 |
P0 |
L-4: BindRoles |
| TC-0192 |
POST /api/user/setPerms |
正常ALLOW |
{"userId":1,"perms":[{"permId":1,"effect":"ALLOW"}]} |
code=0 |
正常路径 |
P0 |
TransactCtx |
| TC-0193 |
POST /api/user/setPerms |
用户不存在 |
{"userId":9999,"perms":[...]} |
code=404, "用户不存在" |
存在性校验 |
P0 |
FindOne预检 |
| TC-0194 |
POST /api/user/setPerms |
DENY权限 |
effect="DENY" |
code=0 |
正常路径 |
P0 |
effect="DENY" |
| TC-0195 |
POST /api/user/setPerms |
清空权限 |
{"userId":1,"perms":[]} |
code=0 |
分支覆盖 |
P1 |
len==0 |
| TC-0196 |
POST /api/user/setPerms |
无效Effect值 |
effect="INVALID" |
code=400, "无效的权限效果" |
安全 |
P0 |
H-04修复: Effect白名单 |
| TC-0197 |
POST /api/user/setPerms |
PermId不存在 |
permId=99999 |
code=400, "权限不存在" |
安全 |
P0 |
H-04修复: 校验PermId |
| TC-0198 |
POST /api/user/setPerms |
权限不属于当前产品 |
permId属于其他产品 |
code=400, "权限不属于当前产品" |
安全 |
P0 |
H-04修复: 校验权限归属 |
| TC-0199 |
POST /api/user/setPerms |
非产品成员设置权限被拒绝 |
目标用户非当前产品成员 |
400 "不是当前产品的成员" |
安全 |
P0 |
L-5: SetUserPerms |
| TC-0200 |
POST /api/user/updateStatus |
正常冻结 |
{"id":普通用户,"status":2} |
code=0 |
正常路径 |
P0 |
updateUserStatusLogic |
| TC-0201 |
POST /api/user/updateStatus |
正常解冻 |
{"id":普通用户,"status":1} |
code=0 |
正常路径 |
P0 |
status=1 |
| TC-0202 |
POST /api/user/updateStatus |
非法status(0) |
{"id":1,"status":0} |
code=400, "状态值无效" |
输入校验 |
P0 |
status!=1&&!=2 |
| TC-0203 |
POST /api/user/updateStatus |
冻结自己 |
id=当前登录userId |
code=400, "不能修改自己的状态" |
自我保护 |
P0 |
callerId==req.Id |
| TC-0204 |
POST /api/user/updateStatus |
冻结超管 |
id=超管 |
code=403, "不能修改超级管理员的状态" |
超管保护 |
P0 |
IsSuperAdmin==1 |
| TC-0205 |
POST /api/user/list |
userList-非超管仅可见产品成员 |
ctx=ADMIN(非超管), productCode指定 |
仅返回该产品成员, 不返回非成员 |
安全 |
P0 |
Audit#1修复: FindListByProductMembers数据隔离 |
| TC-0206 |
POST /api/user/list |
userList-非超管未指定productCode被拒绝 |
ctx=ADMIN(非超管), productCode="" |
403 "非超管用户必须指定产品编码" |
安全 |
P0 |
Audit#1修复: 强制productCode |
| TC-0207 |
POST /api/user/list |
userList-非超管使用错误productCode被拒绝 |
ctx=ADMIN, productCode!=ctx.ProductCode |
403 |
安全 |
P0 |
Audit#1修复: productCode一致性校验 |
| TC-0208 |
POST /api/user/bindRoles |
bindRoles-permsLevel越权拒绝 |
ctx=ADMIN(MinPermsLevel=50), role.permsLevel=1 |
403 "不能分配权限级别高于自身的角色" |
安全 |
P0 |
Audit#2修复: 角色权限级别越权防护 |
| TC-0209 |
POST /api/user/bindRoles |
bindRoles-超管可分配任意级别角色 |
ctx=SuperAdmin, role.permsLevel=1 |
绑定成功 |
正常路径 |
P0 |
Audit#2修复: 超管无permsLevel限制 |
| TC-0210 |
POST /api/user/setPerms |
同一权限ID冲突Effect被拒绝 |
perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"DENY"}] |
400 "同一权限ID不能同时为 ALLOW 和 DENY" |
业务约束 |
P0 |
seen[permId]冲突检测 |
| TC-0211 |
POST /api/user/setPerms |
重复权限ID相同Effect去重 |
perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"ALLOW"}] |
成功, DB仅1条记录 |
数据鲁棒性 |
P1 |
seen去重,uniquePerms |
| TC-0212 |
POST /api/user/setPerms |
已禁用权限不能被设置 |
perm.Status=2(Disabled) |
400 "权限 xxx 已被禁用,无法设置" |
业务约束 |
P0 |
p.Status != StatusEnabled |
2.17 成员管理
| TC编号 |
接口/方法 |
测试场景 |
输入参数 (JSON) |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0213 |
POST /api/member/add |
正常添加 |
{"productCode":"p1","userId":1,"memberType":"MEMBER"} |
code=0, id>0 |
正常路径 |
P0 |
addMemberLogic |
| TC-0214 |
POST /api/member/add |
产品不存在 |
{"productCode":"notexist",...} |
code=404, "产品不存在" |
存在性校验 |
P0 |
FindOneByCode预检 |
| TC-0215 |
POST /api/member/add |
用户不存在 |
{"userId":9999,...} |
code=404, "用户不存在" |
存在性校验 |
P0 |
FindOne预检 |
| TC-0216 |
POST /api/member/add |
已是成员 |
重复添加 |
code=409, "已是成员" |
异常路径 |
P0 |
FindOneByProductCodeUserId成功 |
| TC-0217 |
POST /api/member/add |
并发添加 |
两请求同时 |
一成功一冲突 |
并发 |
P1 |
uk_product_user |
| TC-0218 |
POST /api/member/add |
无效MemberType |
{"memberType":"INVALID"} |
code=400, "无效的成员类型" |
输入校验 |
P0 |
M-06修复: MemberType白名单 |
| TC-0219 |
POST /api/member/update |
正常更新 |
{"id":1,"memberType":"ADMIN"} |
code=0 |
正常路径 |
P0 |
updateMemberLogic |
| TC-0220 |
POST /api/member/update |
不存在 |
{"id":9999,...} |
code=404 |
异常路径 |
P0 |
FindOne失败 |
| TC-0221 |
POST /api/member/update |
无效MemberType |
{"id":1,"memberType":"INVALID"} |
code=400, "无效的成员类型" |
输入校验 |
P0 |
M-06修复: MemberType白名单 |
| TC-0222 |
POST /api/member/list |
正常查询(批量查用户) |
{"productCode":"p1","page":1,"pageSize":10} |
含username/nickname |
正常路径 |
P0 |
FindByIds批量 |
| TC-0223 |
POST /api/member/list |
成员用户已删除 |
userId不存在于FindByIds结果 |
username/nickname为空 |
分支覆盖 |
P1 |
userMap无对应key |
| TC-0224 |
POST /api/member/list |
pageSize超过上限 |
{"productCode":"p1","pageSize":200} |
实际pageSize=100 |
边界 |
P0 |
NormalizePage cap |
| TC-0225 |
POST /api/member/list |
空成员列表 |
productCode下无成员 |
total=0, list=[], 不调FindByIds |
分支覆盖 |
P1 |
userIds空 |
| TC-0226 |
POST /api/member/remove |
正常移除+级联(事务内) |
{"id":1} (含角色/权限) |
code=0, user_role+user_perm同步清理 |
正常+事务 |
P0 |
TransactCtx全路径 |
| TC-0227 |
POST /api/member/remove |
跨产品隔离 |
用户在多产品有角色 |
仅清理该产品的 |
深度业务 |
P0 |
ForProductTx子查询 |
| TC-0228 |
POST /api/member/remove |
成员不存在 |
{"id":9999} |
code=404, "成员不存在" |
异常路径 |
P0 |
FindOne失败 |
| TC-0229 |
POST /api/member/remove |
事务回滚 |
模拟DeleteWithTx失败 |
级联删除全部回滚 |
事务验证 |
P0 |
TransactCtx |
三、gRPC 接口测试用例
3.1 gRPC SyncPermissions
| TC编号 |
接口/方法 |
测试场景 |
输入 |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0230 |
SyncPermissions |
正常同步 |
valid req |
added/updated/disabled计数正确 |
正常路径 |
P0 |
permserver.go SyncPermissions |
| TC-0231 |
SyncPermissions |
appKey无效 |
invalid appKey |
codes.Unauthenticated |
异常路径 |
P0 |
status.Error |
| TC-0232 |
SyncPermissions |
appSecret错误 |
wrong secret |
codes.Unauthenticated |
异常路径 |
P0 |
status.Error |
| TC-0233 |
SyncPermissions |
产品已禁用 |
disabled product |
codes.PermissionDenied |
分支覆盖 |
P0 |
status.Error |
| TC-0234 |
SyncPermissions |
验证disabled计数 |
DB有5条,perms含2条 |
disabled=3 |
功能验证 |
P0 |
RowsAffected |
3.2 gRPC Login / RefreshToken / VerifyToken / GetUserPerms
| TC编号 |
接口/方法 |
测试场景 |
输入 |
预期结果 |
测试类型 |
优先级 |
覆盖说明 |
| TC-0235 |
Login |
正常登录(普通用户+productCode) |
valid credentials + productCode |
token对+userInfo(含nickname) |
正常路径 |
P0 |
permserver.go Login; resp.Nickname应返回用户昵称 |
| TC-0236 |
Login |
用户不存在 |
wrong username |
codes.Unauthenticated |
异常路径 |
P0 |
status.Error |
| TC-0237 |
Login |
密码错误 |
wrong password |
codes.Unauthenticated |
异常路径 |
P0 |
status.Error |
| TC-0238 |
Login |
账号冻结 |
frozen user |
codes.PermissionDenied |
分支覆盖 |
P0 |
status.Error |
| TC-0239 |
Login |
超管被拒绝 |
isSuperAdmin=1+productCode |
codes.PermissionDenied, "超级管理员不允许通过产品端登录" |
安全 |
P0 |
IsSuperAdmin==1 → 拒绝 |
| TC-0240 |
Login |
普通用户+productCode |
普通MEMBER+productCode |
perms含角色权限, memberType="MEMBER" |
分支覆盖 |
P0 |
!isSuperAdmin && productCode!="" |
| TC-0241 |
Login |
产品成员被禁用时拒绝登录 |
member.status=Disabled |
PermissionDenied |
安全 |
P0 |
H-3: permserver Login |
| TC-0242 |
Login |
productCode为空 |
productCode="" |
codes.InvalidArgument, "productCode不能为空" |
输入校验 |
P0 |
第一个校验点 |
| TC-0243 |
RefreshToken |
正常刷新 |
valid token |
新token对 |
正常路径 |
P0 |
RefreshToken |
| TC-0244 |
RefreshToken |
token无效 |
invalid token |
codes.Unauthenticated |
异常路径 |
P0 |
status.Error |
| TC-0245 |
RefreshToken |
账号冻结 |
frozen |
codes.PermissionDenied |
分支覆盖 |
P0 |
status.Error |
| TC-0246 |
RefreshToken |
productCode回退到claims |
req.ProductCode="", claims含productCode |
使用claims.ProductCode |
分支覆盖 |
P0 |
productCode==""回退 |
| TC-0247 |
RefreshToken |
超管+productCode |
isSuperAdmin=1+productCode |
memberType="SUPER_ADMIN", perms全量 |
分支覆盖 |
P0 |
isSuperAdmin && productCode!="" |
| TC-0248 |
RefreshToken |
普通用户+productCode |
普通MEMBER+productCode |
perms含角色权限 |
分支覆盖 |
P0 |
!isSuperAdmin && productCode!="" |
| TC-0249 |
VerifyToken |
有效token |
valid |
valid=true, userId/perms/productCode正确 |
正常路径 |
P0 |
VerifyToken; resp.ProductCode应返回产品编码 |
| TC-0250 |
VerifyToken |
无效token |
invalid |
valid=false |
异常路径 |
P0 |
err或!Valid |
| TC-0251 |
VerifyToken |
缺少userId |
伪造claims |
valid=false |
安全 |
P0 |
!ok断言保护 |
| TC-0252 |
VerifyToken |
冻结用户token返回Invalid |
user.status=Disabled |
Valid=false |
安全 |
P0 |
H-4: 实时查DB |
| TC-0253 |
VerifyToken |
非成员token返回Invalid |
user非产品成员 |
Valid=false |
安全 |
P0 |
H-4: 实时查成员状态 |
| TC-0254 |
VerifyToken |
返回实时MemberType和Perms |
DB中ADMIN+自定义权限 |
返回实时数据而非token中旧数据 |
安全 |
P0 |
H-4: 实时数据 |
| TC-0255 |
GetUserPerms |
用户不存在(需先通过AppKey/Secret认证) |
userId=9999, 合法AppKey/AppSecret |
codes.NotFound |
异常路径 |
P0 |
status.Error; 先认证再查用户 |
| TC-0256 |
GetUserPerms |
超管(需先通过AppKey/Secret认证) |
isSuperAdmin, 合法AppKey/AppSecret |
perms全量, "SUPER_ADMIN" |
正常路径 |
P0 |
GetUserPerms(true); AppKey认证前置 |
| TC-0257 |
GetUserPerms |
MEMBER-DENY覆盖(需先通过AppKey/Secret认证) |
角色有permA, DENY permA, 合法AppKey/AppSecret |
perms不含permA |
深度业务 |
P0 |
denySet过滤; AppKey认证前置 |
四、JWT中间件 / 统一响应测试用例
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0258 |
正常Bearer token |
Authorization: Bearer {valid} |
通过, ctx注入5个值 |
正常路径 |
P0 |
middleware全路径 |
| TC-0259 |
无Authorization头 |
无Header |
code=401, "未登录" |
异常 |
P0 |
authHeader=="" |
| TC-0260 |
无Bearer前缀 |
Authorization: xxx |
code=401, "token格式错误" |
异常 |
P0 |
TrimPrefix相等 |
| TC-0261 |
token签名错误 |
错误secret |
code=401, "token无效或已过期" |
异常 |
P0 |
!Valid |
| TC-0262 |
token过期 |
expired |
code=401 |
异常 |
P0 |
jwt过期 |
| TC-0263 |
claims类型断言失败 |
非标准claims |
code=401, "token无效或类型错误" |
异常 |
P1 |
!ok 防御性分支,jwt.ParseWithClaims(&Claims{}) 下不可达;TokenType 检查由 TC-0264 覆盖 |
| TC-0264 |
refresh token被拒绝 |
用refresh token访问API |
code=401, "token无效或类型错误" |
安全 |
P0 |
TokenType="refresh"时拒绝 |
| TC-0265 |
业务错误(CodeError) |
触发404等 |
{code:业务码, msg:业务消息} |
正常 |
P0 |
errors.As成功 |
| TC-0266 |
内部错误 |
DB异常 |
{code:500, msg:"服务器内部错误"} |
安全 |
P0 |
logx.Errorf+兜底 |
| TC-0267 |
成功(有data) |
正常请求 |
{code:0, msg:"ok", data:{...}} |
正常 |
P0 |
v!=nil |
| TC-0268 |
成功(无data) |
返回nil |
{code:0, msg:"ok"} |
正常 |
P0 |
v==nil |
五、util 层测试用例
5.1 NormalizePage
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0269 |
正常值 |
page=2, pageSize=10 |
(2, 10) |
正常路径 |
P0 |
无修正 |
| TC-0270 |
page<=0 |
page=0, pageSize=10 |
(1, 10) |
边界 |
P0 |
page<=0→1 |
| TC-0271 |
page=-1 |
page=-1, pageSize=10 |
(1, 10) |
边界 |
P0 |
page<=0→1 |
| TC-0272 |
pageSize<=0 |
page=1, pageSize=0 |
(1, 20) |
边界 |
P0 |
pageSize<=0→20 |
| TC-0273 |
pageSize>100 |
page=1, pageSize=500 |
(1, 100) |
边界-上限 |
P0 |
pageSize>100→100 |
| TC-0274 |
pageSize=100 |
page=1, pageSize=100 |
(1, 100) |
边界 |
P1 |
恰好不触发 |
| TC-0275 |
pageSize=101 |
page=1, pageSize=101 |
(1, 100) |
边界 |
P1 |
恰好触发 |
| TC-0276 |
双零 |
page=0, pageSize=0 |
(1, 20) |
边界 |
P1 |
两条件同时 |
5.2 IsValidEmail
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0277 |
正常邮箱 |
[email protected] |
true |
正常路径 |
P0 |
标准格式 |
| TC-0278 |
含点号 |
[email protected] |
true |
正常路径 |
P1 |
允许点号 |
| TC-0279 |
含加号 |
[email protected] |
true |
正常路径 |
P1 |
允许加号 |
| TC-0280 |
缺少@ |
userexample.com |
false |
异常路径 |
P0 |
无@ |
| TC-0281 |
缺少域名 |
user@ |
false |
异常路径 |
P0 |
无域名 |
| TC-0282 |
缺少TLD |
user@example |
false |
异常路径 |
P0 |
TLD<2字符 |
| TC-0283 |
空字符串 |
"" |
false |
边界 |
P1 |
空 |
5.3 IsValidPhone
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0284 |
国内手机号 |
13800138000 |
true |
正常路径 |
P0 |
11位数字 |
| TC-0285 |
带+国际码 |
+8613800138000 |
true |
正常路径 |
P0 |
+前缀 |
| TC-0286 |
太短(6位) |
123456 |
false |
边界 |
P0 |
<7位 |
| TC-0287 |
恰好7位 |
1234567 |
true |
边界 |
P1 |
最小长度 |
| TC-0288 |
最长15位 |
+123456789012345 |
true |
边界 |
P1 |
最大长度 |
| TC-0289 |
超长16位 |
1234567890123456 |
false |
边界 |
P1 |
超限 |
| TC-0290 |
包含字母 |
1380013abc |
false |
异常路径 |
P0 |
非数字 |
| TC-0291 |
空字符串 |
"" |
false |
边界 |
P1 |
空 |
六、Logic 层单元测试用例
以下针对 Logic 层中的核心共享函数,使用 mock Model 接口进行纯单元测试。
6.1 auth/jwt.go — GenerateAccessToken
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0292 |
正常生成 |
secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"] |
返回非空token, err=nil |
正常路径 |
P0 |
jwt.NewWithClaims(HS256) |
| TC-0293 |
解析token验证claims |
上述token |
ParseWithClaims可解析出正确userId/username/productCode/memberType/perms |
功能验证 |
P0 |
claims完整性 |
| TC-0294 |
空secret |
secret="" |
仍能生成token(空key签名) |
边界 |
P2 |
HS256 允许空key |
| TC-0295 |
空perms |
perms=nil |
token生成成功, 解析后perms=nil |
边界 |
P1 |
nil slice |
| TC-0296 |
过期时间验证 |
expireSeconds=1, sleep 2s |
ParseWithClaims返回过期错误 |
功能验证 |
P0 |
ExpiresAt |
6.2 auth/jwt.go — GenerateRefreshToken
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0297 |
正常生成 |
secret="s", expire=86400, userId=1, productCode="p" |
返回非空token |
正常路径 |
P0 |
RefreshClaims |
| TC-0298 |
解析验证 |
上述token |
ParseRefreshToken解析出userId=1, productCode="p" |
功能验证 |
P0 |
往返一致 |
| TC-0299 |
productCode为空 |
productCode="" |
生成成功, 解析后productCode="" |
边界 |
P1 |
空字符串 |
6.3 auth/jwt.go — ParseRefreshToken
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0300 |
正常解析 |
有效token+正确secret |
返回RefreshClaims, err=nil |
正常路径 |
P0 |
token.Valid |
| TC-0301 |
错误secret |
有效token+错误secret |
err!=nil |
异常路径 |
P0 |
签名验证失败 |
| TC-0302 |
无效token字符串 |
"invalid-token" |
err!=nil |
异常路径 |
P0 |
解析失败 |
| TC-0303 |
空token |
"" |
err!=nil |
边界 |
P1 |
空字符串 |
| TC-0304 |
过期token |
已过期的token |
err!=nil (token expired) |
异常路径 |
P0 |
ExpiresAt已过 |
| TC-0305 |
AccessToken误用 |
用AccessToken当RefreshToken解析 |
err!=nil (TokenType="access"≠"refresh") |
安全 |
P0 |
TokenType字段校验 |
6.4 middleware — 辅助函数单元测试
M-5/M-6重构:GetUserPerms (auth/perms.go) 以及 GetUsername / GetMemberType / IsSuperAdmin 等 context helper 作为死代码被移除;统一使用 GetUserDetails 读取完整 UserDetails 后访问字段。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0306 |
GetUserId-正常 |
ctx含userId=100 |
100 |
正常路径 |
P0 |
类型断言成功 |
| TC-0307 |
GetUserId-空ctx |
空ctx |
0 |
边界 |
P0 |
断言失败→零值 |
| TC-0308 |
GetProductCode-正常 |
ctx含productCode="p1" |
"p1" |
正常路径 |
P0 |
类型断言 |
| TC-0309 |
GetUserDetails 返回完整字段 |
ctx含UserDetails{UserId,Username,ProductCode,MemberType,IsSuperAdmin} |
读出字段全部一致, 空ctx返回nil |
正常路径 |
P0 |
替代已移除的 GetUsername/GetMemberType/IsSuperAdmin |
七、Model 层 _gen.go 模板生成方法测试用例
所有 9 个 Model 的 _gen.go 均由自定义模板 (cli/goctl/model/) 生成,包含非标准方法(批量操作、事务变体、buildBatchUpdateQuery 等)。
以下以 通用测试模式 列出,适用于全部 9 个 Model(注明差异部分)。
7.1 通用 CRUD 方法 (每个 Model 均需测试)
适用: SysUser, SysProduct, SysPerm, SysDept, SysRole, SysRolePerm, SysUserPerm, SysUserRole, SysProductMember
| TC编号 |
方法 |
测试场景 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0310 |
Insert |
正常插入 |
返回Result+nil, DB有新记录 |
正常路径 |
P0 |
ExecCtx+缓存key清理 |
| TC-0311 |
Insert |
正常插入含TokenVersion |
err=nil, DB中tokenVersion=0(默认) |
正常路径 |
P0 |
验证Insert包含tokenVersion |
| TC-0312 |
Insert |
唯一索引冲突 |
返回DB错误(1062) |
异常路径 |
P0 |
MySQL uk |
| TC-0313 |
Insert |
缓存key生成正确 |
验证清理的缓存key包含主键和唯一索引 |
功能验证 |
P0 |
cacheSys*Prefix |
| TC-0314 |
InsertWithTx |
事务内插入 |
使用session执行, 返回Result |
正常路径 |
P0 |
session.ExecCtx |
| TC-0315 |
InsertWithTx |
事务内插入含TokenVersion |
err=nil, 事务内可读到正确tokenVersion |
正常路径 |
P0 |
验证InsertWithTx包含tokenVersion |
| TC-0316 |
InsertWithTx |
事务回滚后无数据 |
事务内Insert+外部回滚→DB无记录 |
事务验证 |
P0 |
TransactCtx |
| TC-0317 |
FindOne |
正常查询(缓存未命中) |
返回记录, 缓存已写入 |
正常路径 |
P0 |
QueryRowCtx→DB |
| TC-0318 |
FindOne |
正常查询(缓存命中) |
不触发DB查询, 返回缓存数据 |
正常路径 |
P0 |
QueryRowCtx→cache |
| TC-0319 |
FindOne |
记录不存在 |
返回ErrNotFound |
异常路径 |
P0 |
sqlc.ErrNotFound→ErrNotFound |
| TC-0320 |
FindOne |
DB异常(非ErrNotFound) |
返回原始error |
异常路径 |
P1 |
default分支 |
| TC-0321 |
FindOneWithTx |
事务内正常查询 |
使用session.QueryRowCtx, 返回记录 |
正常路径 |
P0 |
session直查无缓存 |
| TC-0322 |
FindOneWithTx |
事务内记录不存在 |
返回ErrNotFound |
异常路径 |
P0 |
sqlx.ErrNotFound |
| TC-0323 |
FindOneWithTx |
事务内可见性 |
InsertWithTx后FindOneWithTx可读到 |
事务验证 |
P0 |
同session内可见 |
| TC-0324 |
Update |
正常更新 |
旧缓存key+新缓存key均被清理 |
正常路径 |
P0 |
FindOne→ExecCtx |
| TC-0325 |
Update |
正常更新含TokenVersion |
err=nil, DB中tokenVersion正确更新 |
正常路径 |
P0 |
验证Update包含tokenVersion |
| TC-0326 |
Update |
记录不存在 |
FindOne失败→返回ErrNotFound |
异常路径 |
P0 |
FindOne err |
| TC-0327 |
UpdateWithTx |
事务内更新 |
使用session, 缓存被清理 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0328 |
Delete |
正常删除 |
记录被删, 缓存key被清理 |
正常路径 |
P0 |
FindOne→ExecCtx DELETE |
| TC-0329 |
Delete |
记录不存在 |
FindOne失败→返回ErrNotFound |
异常路径 |
P0 |
FindOne err |
| TC-0330 |
DeleteWithTx |
事务内删除 |
使用session, 缓存被清理 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0331 |
TransactCtx |
正常事务 |
fn执行成功→提交 |
正常路径 |
P0 |
conn.TransactCtx |
| TC-0332 |
TransactCtx |
fn返回错误 |
自动回滚 |
异常路径 |
P0 |
回滚 |
| TC-0333 |
TableName |
获取表名 |
返回正确表名(如 `sys_user`) |
正常路径 |
P0 |
m.table |
7.2 批量插入方法
| TC编号 |
方法 |
测试场景 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0334 |
BatchInsert |
空列表 |
直接返回nil, 不执行SQL |
边界 |
P0 |
len==0 early return |
| TC-0335 |
BatchInsert |
单条记录 |
生成1组VALUES, 执行成功 |
正常路径 |
P0 |
单条 |
| TC-0336 |
BatchInsert |
多条记录(3条) |
生成3组VALUES, SQL正确, 缓存key全清理 |
正常路径 |
P0 |
多条+缓存 |
| TC-0337 |
BatchInsert |
批量插入含TokenVersion |
err=nil, 所有记录tokenVersion正确 |
正常路径 |
P0 |
验证BatchInsert包含tokenVersion |
| TC-0338 |
BatchInsert |
唯一索引冲突 |
全部失败, 返回DB错误 |
异常路径 |
P0 |
MySQL uk |
| TC-0339 |
BatchInsert |
大批量(1000条) |
SQL长度合理, 执行成功 |
性能 |
P2 |
拼接性能 |
| TC-0340 |
BatchInsertWithTx |
空列表 |
直接返回nil |
边界 |
P0 |
len==0 |
| TC-0341 |
BatchInsertWithTx |
正常多条 |
使用session执行 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0342 |
BatchInsertWithTx |
事务回滚 |
外部回滚→无新记录 |
事务验证 |
P0 |
TransactCtx |
7.3 批量更新方法
| TC编号 |
方法 |
测试场景 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0343 |
BatchUpdate |
空列表 |
直接返回nil |
边界 |
P0 |
len==0 early return |
| TC-0344 |
BatchUpdate |
单条记录 |
CASE-WHEN SQL正确, 更新成功 |
正常路径 |
P0 |
buildBatchUpdateQuery 单条 |
| TC-0345 |
BatchUpdate |
多条记录(3条) |
CASE-WHEN生成3个WHEN子句, 旧缓存key全清理 |
正常路径 |
P0 |
buildBatchUpdateQuery 多条 |
| TC-0346 |
BatchUpdate |
批量更新不污染数据 |
err=nil, tokenVersion/createTime/updateTime均正确 |
正常路径 |
P0 |
验证buildBatchUpdateQuery值对齐 |
| TC-0347 |
BatchUpdate |
部分id不存在 |
findListByPrimaryKeys返回部分→仅清理存在的缓存 |
边界 |
P1 |
oldList可能少于dataList |
| TC-0348 |
BatchUpdateWithTx |
空列表 |
直接返回nil |
边界 |
P0 |
len==0 |
| TC-0349 |
BatchUpdateWithTx |
正常多条 |
使用session执行 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0350 |
buildBatchUpdateQuery |
单条 |
SQL: UPDATE SET field=CASE WHEN id=? THEN ? ELSE field END WHERE id IN (?) |
功能验证 |
P0 |
SQL结构 |
| TC-0351 |
buildBatchUpdateQuery |
多条 |
每个字段均有多个WHEN子句, WHERE IN含全部id |
功能验证 |
P0 |
SQL正确性 |
| TC-0352 |
buildBatchUpdateQuery |
vals数量正确 |
vals = N*(fields*2) + N (WHERE IN) |
功能验证 |
P0 |
参数计数 |
7.4 批量删除方法
| TC编号 |
方法 |
测试场景 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0353 |
BatchDelete |
空ids |
直接返回nil |
边界 |
P0 |
len==0 early return |
| TC-0354 |
BatchDelete |
单个id |
DELETE WHERE id IN (?), 缓存清理 |
正常路径 |
P0 |
单条 |
| TC-0355 |
BatchDelete |
多个id(3个) |
3个占位符, 旧数据查询→缓存key全清理 |
正常路径 |
P0 |
findListByPrimaryKeys |
| TC-0356 |
BatchDelete |
包含不存在id |
findListByPrimaryKeys返回部分, 不报错 |
边界 |
P1 |
部分存在 |
| TC-0357 |
BatchDeleteWithTx |
空ids |
直接返回nil |
边界 |
P0 |
len==0 |
| TC-0358 |
BatchDeleteWithTx |
正常多条 |
使用session执行 |
正常路径 |
P0 |
session.ExecCtx |
7.5 唯一索引查询方法 (按 Model 差异)
| TC编号 |
Model |
方法 |
测试场景 |
预期结果 |
优先级 |
覆盖说明 |
| TC-0359 |
SysUser |
FindOneByUsername |
正常查询 |
返回用户, 缓存写入 (索引缓存→主键缓存双层) |
P0 |
QueryRowIndexCtx |
| TC-0360 |
SysUser |
FindOneByUsername |
不存在 |
返回ErrNotFound |
P0 |
sqlc.ErrNotFound |
| TC-0361 |
SysUser |
FindOneByUsernameWithTx |
事务内正常查询 |
返回用户, 使用session直查 |
P0 |
session.QueryRowCtx |
| TC-0362 |
SysUser |
FindOneByUsernameWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
sqlx.ErrNotFound |
| TC-0363 |
SysProduct |
FindOneByAppKey |
正常查询 |
返回产品 |
P0 |
appKey唯一索引 |
| TC-0364 |
SysProduct |
FindOneByAppKey |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0365 |
SysProduct |
FindOneByAppKeyWithTx |
事务内正常查询 |
返回产品 |
P0 |
session直查 |
| TC-0366 |
SysProduct |
FindOneByAppKeyWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0367 |
SysProduct |
FindOneByCode |
正常查询 |
返回产品 |
P0 |
code唯一索引 |
| TC-0368 |
SysProduct |
FindOneByCode |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0369 |
SysProduct |
FindOneByCodeWithTx |
事务内正常查询 |
返回产品 |
P0 |
session直查 |
| TC-0370 |
SysProduct |
FindOneByCodeWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0371 |
SysPerm |
FindOneByProductCodeCode |
正常查询 |
返回权限(复合唯一索引) |
P0 |
productCode+code |
| TC-0372 |
SysPerm |
FindOneByProductCodeCode |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0373 |
SysPerm |
FindOneByProductCodeCodeWithTx |
事务内正常查询 |
返回权限 |
P0 |
session直查 |
| TC-0374 |
SysPerm |
FindOneByProductCodeCodeWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0375 |
SysRole |
FindOneByProductCodeName |
正常查询 |
返回角色(复合唯一索引) |
P0 |
productCode+name |
| TC-0376 |
SysRole |
FindOneByProductCodeName |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0377 |
SysRole |
FindOneByProductCodeNameWithTx |
事务内正常查询 |
返回角色 |
P0 |
session直查 |
| TC-0378 |
SysRole |
FindOneByProductCodeNameWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0379 |
SysRolePerm |
FindOneByRoleIdPermId |
正常查询 |
返回关联记录 |
P0 |
roleId+permId |
| TC-0380 |
SysRolePerm |
FindOneByRoleIdPermId |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0381 |
SysRolePerm |
FindOneByRoleIdPermIdWithTx |
事务内正常查询 |
返回关联记录 |
P0 |
session直查 |
| TC-0382 |
SysRolePerm |
FindOneByRoleIdPermIdWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0383 |
SysUserPerm |
FindOneByUserIdPermId |
正常查询 |
返回关联记录 |
P0 |
userId+permId |
| TC-0384 |
SysUserPerm |
FindOneByUserIdPermId |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0385 |
SysUserPerm |
FindOneByUserIdPermIdWithTx |
事务内正常查询 |
返回关联记录 |
P0 |
session直查 |
| TC-0386 |
SysUserPerm |
FindOneByUserIdPermIdWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0387 |
SysUserRole |
FindOneByUserIdRoleId |
正常查询 |
返回关联记录 |
P0 |
userId+roleId |
| TC-0388 |
SysUserRole |
FindOneByUserIdRoleId |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0389 |
SysUserRole |
FindOneByUserIdRoleIdWithTx |
事务内正常查询 |
返回关联记录 |
P0 |
session直查 |
| TC-0390 |
SysUserRole |
FindOneByUserIdRoleIdWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
| TC-0391 |
SysProductMember |
FindOneByProductCodeUserId |
正常查询 |
返回成员记录 |
P0 |
productCode+userId |
| TC-0392 |
SysProductMember |
FindOneByProductCodeUserId |
不存在 |
返回ErrNotFound |
P0 |
|
| TC-0393 |
SysProductMember |
FindOneByProductCodeUserIdWithTx |
事务内正常查询 |
返回成员记录 |
P0 |
session直查 |
| TC-0394 |
SysProductMember |
FindOneByProductCodeUserIdWithTx |
事务内不存在 |
返回ErrNotFound |
P0 |
|
7.6 内部辅助方法
| TC编号 |
方法 |
测试场景 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0395 |
findListByPrimaryKeys |
空ids |
返回空slice, 不执行SQL |
边界 |
P0 |
len==0 |
| TC-0396 |
findListByPrimaryKeys |
正常ids |
返回匹配记录(无缓存) |
正常路径 |
P0 |
QueryRowsNoCacheCtx |
| TC-0397 |
findListByPrimaryKeys |
部分不存在 |
仅返回存在的记录 |
边界 |
P1 |
IN查询 |
| TC-0398 |
findListByPrimaryKeys |
DB异常 |
返回nil, err |
异常路径 |
P1 |
err透传 |
| TC-0399 |
getPrimaryKeyValue |
正常 |
返回data.Id |
功能验证 |
P0 |
interface{} |
| TC-0400 |
formatPrimary |
正常 |
返回 "cache:sysXxx:id:{id}" |
功能验证 |
P0 |
缓存key格式 |
| TC-0401 |
queryPrimary |
正常 |
执行 SELECT WHERE id=? |
功能验证 |
P0 |
SQL |
7.7 缓存key与前缀初始化
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0402 |
cachePrefix为空 |
cachePrefix="" |
使用默认前缀 (如 "cache:sysUser:id:") |
分支覆盖 |
P0 |
if cachePrefix!="" 未进入 |
| TC-0403 |
cachePrefix非空 |
cachePrefix="test" |
前缀变为 "test:cache:sysUser:id:" |
分支覆盖 |
P0 |
if cachePrefix!="" 进入 |
| TC-0404 |
多唯一索引前缀(SysProduct) |
cachePrefix="test" |
3个缓存前缀均更新: id/appKey/code |
功能验证 |
P0 |
3个变量均修改 |
八、Model 层自定义方法测试用例
8.1 SysUserModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0405 |
FindListByPage |
正常分页 |
page=1, pageSize=10, DB有20条 |
返回10条+total=20 |
正常路径 |
P0 |
count+limit offset |
| TC-0406 |
FindListByPage |
第二页 |
page=2, pageSize=10 |
offset=10, 返回后10条 |
正常路径 |
P0 |
(page-1)*pageSize |
| TC-0407 |
FindListByPage |
空表 |
无数据 |
total=0, list为空 |
边界 |
P0 |
count=0 |
| TC-0408 |
FindListByPage |
count查询失败 |
DB异常 |
返回0,0,err |
异常路径 |
P1 |
第一个err |
| TC-0409 |
FindListByPage |
list查询失败 |
DB异常 |
返回0,total,err |
异常路径 |
P1 |
第二个err |
| TC-0410 |
FindListByProductMembers |
正常查询 |
productCode="p1", page=1, pageSize=10 |
返回该产品所有成员用户, total正确 |
正常路径 |
P0 |
替代FindListByDeptIds: INNER JOIN sys_product_member |
| TC-0411 |
FindListByProductMembers |
productCode不存在 |
productCode="no_such_pc" |
total=0, list空 |
边界 |
P1 |
JOIN 无匹配 |
| TC-0412 |
FindByIds |
正常批量查询 |
ids=[1,2,3] |
返回3条 |
正常路径 |
P0 |
IN查询 |
| TC-0413 |
FindByIds |
空ids |
ids=[] |
返回nil,nil |
边界 |
P0 |
len==0 |
| TC-0414 |
FindByIds |
部分id不存在 |
ids=[1,9999] |
仅返回存在的 |
边界 |
P1 |
IN不报错 |
| TC-0415 |
FindByIds |
DB异常 |
连接失败 |
返回nil,err |
异常路径 |
P1 |
err透传 |
| TC-0416 |
FindIdsByDeptId |
有用户的部门 |
deptId=1(有用户) |
返回id列表 |
正常路径 |
P0 |
WHERE deptId=? |
| TC-0417 |
FindIdsByDeptId |
无用户部门 |
deptId=999 |
空slice |
边界 |
P1 |
|
| TC-0418 |
UpdateProfile |
状态未变-不递增tokenVersion |
statusChanged=false |
成功, tokenVersion不变 |
正常路径 |
P0 |
H-1修复: 非状态字段更新不影响会话 |
| TC-0419 |
UpdateProfile |
状态变更-tokenVersion+1 |
statusChanged=true |
成功, tokenVersion+1 |
正常路径 |
P0 |
H-1修复: 状态变更使会话失效 |
| TC-0420 |
UpdateProfile |
乐观锁冲突 |
expectedUpdateTime 与DB不符 |
返回ErrUpdateConflict |
异常路径 |
P0 |
H-1修复: WHERE updateTime=? |
| TC-0421 |
UpdateProfile |
并发场景 |
两个 goroutine 基于同一 updateTime 并发更新 |
仅一方成功, 另一方得到 ErrUpdateConflict |
并发 |
P0 |
H-1修复: 乐观锁仅允许一个成功 |
| TC-0422 |
UpdateProfile |
userId不存在 |
id=9999999 |
返回 ErrUpdateConflict (affected=0) |
异常路径 |
P1 |
WHERE 不匹配 |
8.2 SysProductModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0423 |
FindList |
正常分页 |
page=1, pageSize=10 |
返回list+total |
正常路径 |
P0 |
count+limit |
| TC-0424 |
FindList |
空表 |
无数据 |
total=0, list空 |
边界 |
P0 |
|
| TC-0425 |
FindList |
count失败 |
DB异常 |
返回err |
异常路径 |
P1 |
第一个err |
8.3 SysPermModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0426 |
FindListByProductCode |
正常分页 |
productCode="p1", page=1, pageSize=10 |
list+total |
正常路径 |
P0 |
WHERE productCode=? |
| TC-0427 |
FindListByProductCode |
不存在的productCode |
"notexist" |
total=0, list空 |
边界 |
P1 |
|
| TC-0428 |
FindAllCodesByProductCode |
正常查询 |
DB有3条启用权限 |
返回3个code |
正常路径 |
P0 |
SELECT code WHERE status=1 |
| TC-0429 |
FindAllCodesByProductCode |
空结果 |
无匹配 |
空slice |
边界 |
P1 |
|
| TC-0430 |
FindByIds |
正常 |
ids=[1,2] |
返回2条 |
正常路径 |
P0 |
IN查询 |
| TC-0431 |
FindByIds |
空ids |
[] |
返回nil,nil |
边界 |
P0 |
len==0 |
| TC-0432 |
FindMapByProductCode |
正常查询 |
productCode="p1" |
map[code]*SysPerm, key为code |
正常路径 |
P0 |
result[p.Code]=p |
| TC-0433 |
FindMapByProductCode |
空结果 |
无匹配 |
空map |
边界 |
P1 |
|
| TC-0434 |
FindMapByProductCode |
key唯一性 |
同productCode有3条 |
map长度=3 |
功能验证 |
P0 |
code唯一 |
| TC-0435 |
DisableNotInCodesWithTx |
codes非空-正常 |
session+productCode="p1", codes=["a","b"], DB有a/b/c |
禁用c, 返回affected=1 |
正常路径 |
P0 |
事务内 NOT IN |
| TC-0436 |
DisableNotInCodesWithTx |
codes为空-全部禁用 |
codes=[] |
全部已启用的被禁用 |
分支覆盖 |
P0 |
len==0分支 |
| TC-0437 |
DisableNotInCodesWithTx |
无需禁用 |
codes包含所有已启用 |
affected=0 |
边界 |
P1 |
0行更新 |
| TC-0438 |
DisableNotInCodesWithTx |
DB异常 |
session.Exec报错 |
返回0,err |
异常路径 |
P1 |
err |
| TC-0439 |
FindAllCodesByProductCode |
有权限产品 |
productCode="p1" |
返回code列表(仅status=1) |
正常路径 |
P0 |
WHERE status=1 |
| TC-0440 |
FindAllCodesByProductCode |
无权限产品 |
productCode="notexist" |
空slice |
边界 |
P1 |
|
| TC-0441 |
FindAllCodesByProductCode |
全部已禁用 |
所有perm.status=2 |
空slice |
边界 |
P1 |
WHERE status=1 过滤, 全禁用时返回空 |
8.4 SysDeptModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0442 |
FindAll |
正常查询 |
DB有5条 |
返回5条, 按sort asc排序 |
正常路径 |
P0 |
ORDER BY sort, id |
| TC-0443 |
FindAll |
空表 |
无数据 |
空slice |
边界 |
P0 |
|
| TC-0444 |
FindByParentId |
正常查询 |
parentId=1 |
返回子部门列表 |
正常路径 |
P0 |
WHERE parentId=? |
| TC-0445 |
FindByParentId |
无子部门 |
parentId=999 |
空slice |
边界 |
P1 |
|
| TC-0446 |
FindByPathPrefix |
正常查询 |
pathPrefix="/1/" |
返回路径以/1/开头的部门 |
正常路径 |
P0 |
LIKE pathPrefix% |
| TC-0447 |
FindByPathPrefix |
LIKE注入已阻止 |
pathPrefix含% |
空slice(%和_已转义,不作为通配符) |
安全 |
P1 |
NewReplacer转义 |
| TC-0448 |
FindByPathPrefix |
无匹配 |
"/999/" |
空slice |
边界 |
P1 |
|
8.5 SysRoleModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0449 |
FindListByProductCode |
正常分页 |
productCode="p1" |
按permsLevel asc排序 |
正常路径 |
P0 |
ORDER BY permsLevel, id |
| TC-0450 |
FindListByProductCode |
空结果 |
无匹配 |
total=0 |
边界 |
P1 |
|
| TC-0451 |
FindByIds |
正常 |
ids=[1,2] |
返回2条 |
正常路径 |
P0 |
IN查询 |
| TC-0452 |
FindByIds |
空ids |
[] |
返回nil,nil |
边界 |
P0 |
len==0 |
| TC-0453 |
FindMinPermsLevelByUserIdAndProductCode |
有角色用户 |
userId=1, productCode="p1" |
返回最小permsLevel |
正常路径 |
P0 |
MIN聚合 |
| TC-0454 |
FindMinPermsLevelByUserIdAndProductCode |
无角色用户 |
userId=无角色用户 |
error(ErrNotFound) |
边界 |
P0 |
IFNULL返回-1→level<0→ErrNotFound |
8.6 SysRolePermModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0455 |
FindPermIdsByRoleId |
正常查询 |
roleId=1, DB有3条 |
返回3个permId |
正常路径 |
P0 |
SELECT permId WHERE roleId=? |
| TC-0456 |
FindPermIdsByRoleId |
无绑定 |
roleId=999 |
空slice |
边界 |
P1 |
|
| TC-0457 |
FindPermIdsByRoleIds |
正常查询 |
roleIds=[1,2] |
返回去重后的permId |
正常路径 |
P0 |
DISTINCT + IN |
| TC-0458 |
FindPermIdsByRoleIds |
空roleIds |
[] |
返回nil,nil |
边界 |
P0 |
len==0 |
| TC-0459 |
FindPermIdsByRoleIds |
去重验证 |
两角色有相同permId |
结果中permId不重复 |
功能验证 |
P0 |
DISTINCT |
| TC-0460 |
DeleteByRoleIdTx |
正常事务内删除 |
session+roleId |
使用session执行 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0461 |
DeleteByRoleIdTx |
无绑定 |
session+roleId=999 |
删0行, 不报错 |
边界 |
P1 |
session.ExecCtx, affected=0 不报错 |
8.7 SysUserPermModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0462 |
FindPermIdsByUserIdAndEffectForProduct |
ALLOW-指定产品 |
userId=1, effect="ALLOW", productCode="p1" |
返回该用户在该产品下ALLOW的permIds |
正常路径 |
P0 |
H-1修复: 跨产品权限隔离 |
| TC-0463 |
FindPermIdsByUserIdAndEffectForProduct |
DENY-指定产品 |
userId=1, effect="DENY", productCode="p1" |
返回DENY的permIds |
正常路径 |
P0 |
H-1修复: 跨产品权限隔离 |
| TC-0464 |
FindPermIdsByUserIdAndEffectForProduct |
无记录/其他产品 |
productCode不匹配 |
空slice |
边界 |
P1 |
WHERE 子句过滤 productCode 后无匹配 |
| TC-0465 |
DeleteByUserIdForProductTx |
事务内跨产品删除 |
session+userId+productCode |
使用session |
正常路径 |
P0 |
session.ExecCtx, 仅删该产品下配置 |
| TC-0466 |
DeleteByUserIdForProductTx |
跨产品隔离 |
用户在多产品有配置 |
仅删目标产品的 |
深度业务 |
P0 |
WHERE userId=? AND productCode=? 隔离其他产品 |
8.8 SysUserRoleModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0467 |
FindRoleIdsByUserId |
正常查询 |
userId=1, DB有3条 |
返回3个roleId |
正常路径 |
P0 |
SELECT roleId |
| TC-0468 |
FindRoleIdsByUserId |
无绑定 |
userId=999 |
空slice |
边界 |
P1 |
|
| TC-0469 |
DeleteByRoleIdTx |
正常删除 |
session+roleId |
删除该角色的所有用户绑定 |
正常路径 |
P0 |
session.ExecCtx |
| TC-0470 |
DeleteByUserIdForProductTx |
事务内跨产品删除 |
session+userId+productCode |
使用session |
正常路径 |
P0 |
session.ExecCtx |
| TC-0471 |
DeleteByUserIdForProductTx |
跨产品隔离 |
用户在多产品有角色 |
仅删目标产品的 |
深度业务 |
P0 |
WHERE userId=? AND productCode=? 子查询过滤 |
| TC-0472 |
FindUserIdsByRoleId |
有绑定的角色 |
roleId=1 |
返回userId列表 |
正常路径 |
P0 |
WHERE roleId=? |
| TC-0473 |
FindUserIdsByRoleId |
无绑定角色 |
roleId=999 |
空slice |
边界 |
P1 |
|
| TC-0474 |
FindRoleIdsByUserIdForProduct |
跨产品过滤 |
userId=1, productCode="p1" |
仅返回该产品下绑定的roleId |
深度业务 |
P0 |
差量更新依赖此接口 |
8.9 SysProductMemberModel
| TC编号 |
方法 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0475 |
FindListByProductCode |
正常分页 |
productCode="p1" |
list+total |
正常路径 |
P0 |
WHERE productCode=? |
| TC-0476 |
FindListByProductCode |
空结果 |
无匹配 |
total=0 |
边界 |
P1 |
|
| TC-0477 |
FindMapByProductCodeUserIds |
正常批量 |
productCode="p1", userIds=[1,2] |
map key=userId |
正常路径 |
P0 |
IN+productCode |
| TC-0478 |
FindMapByProductCodeUserIds |
空userIds |
[] |
返回空map |
边界 |
P0 |
len==0 |
| TC-0479 |
FindMapByProductCodeUserIds |
部分不是成员 |
userIds含非成员 |
map仅含成员 |
边界 |
P1 |
|
| TC-0480 |
FindMapByProductCodeUserIds |
map key正确 |
查询结果 |
key=userId, val=*SysProductMember |
功能验证 |
P0 |
result[pm.UserId] |
九、访问控制 (auth/access.go)
9.1 RequireSuperAdmin
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0481 |
超管通过 |
ctx含SuperAdmin UserDetails |
nil (允许) |
正常路径 |
P0 |
caller.IsSuperAdmin |
| TC-0482 |
非超管拒绝 |
ctx含ADMIN UserDetails |
403 "仅超级管理员" |
异常路径 |
P0 |
!IsSuperAdmin |
| TC-0483 |
MEMBER拒绝 |
ctx含MEMBER UserDetails |
403 "仅超级管理员" |
异常路径 |
P0 |
|
| TC-0484 |
未登录 |
ctx无UserDetails |
401 "未登录" |
边界 |
P0 |
caller==nil |
9.2 RequireProductAdminFor(ctx, targetProductCode)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0485 |
超管通过 |
ctx含SuperAdmin, productCode="p1" |
nil |
正常路径 |
P0 |
IsSuperAdmin |
| TC-0486 |
ADMIN通过(同产品) |
ctx含ADMIN(p1), productCode="p1" |
nil |
正常路径 |
P0 |
MemberType==ADMIN且productCode匹配 |
| TC-0487 |
DEVELOPER拒绝 |
ctx含DEVELOPER(p1), productCode="p1" |
403 |
异常路径 |
P0 |
非Admin |
| TC-0488 |
MEMBER拒绝 |
ctx含MEMBER(p1), productCode="p1" |
403 |
异常路径 |
P0 |
非Admin |
| TC-0489 |
未登录 |
ctx无UserDetails, productCode="p1" |
401 |
边界 |
P0 |
caller==nil |
| TC-0490 |
ADMIN跨产品拒绝 |
ctx含ADMIN(p1), productCode="other" |
403 |
安全 |
P0 |
productCode不匹配→拒绝 |
9.3 CheckMemberTypeAssignment
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0491 |
超管可分配任何类型 |
caller=SuperAdmin, assigned=ADMIN |
nil |
正常路径 |
P0 |
IsSuperAdmin豁免 |
| TC-0492 |
ADMIN分配DEVELOPER |
caller=ADMIN, assigned=DEVELOPER |
nil |
正常路径 |
P0 |
callerPri(1) < assignPri(2) |
| TC-0493 |
ADMIN分配ADMIN(同级拒绝) |
caller=ADMIN, assigned=ADMIN |
403 |
深度业务 |
P0 |
callerPri >= assignPri |
| TC-0494 |
DEVELOPER分配ADMIN(越级拒绝) |
caller=DEVELOPER, assigned=ADMIN |
403 |
深度业务 |
P0 |
callerPri > assignPri |
| TC-0495 |
MEMBER分配MEMBER(同级拒绝) |
caller=MEMBER, assigned=MEMBER |
403 |
深度业务 |
P0 |
|
| TC-0496 |
未登录 |
ctx无UserDetails |
401 |
边界 |
P0 |
|
9.4 CheckManageAccess
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0497 |
超管可管理任何人 |
caller=SuperAdmin |
nil |
正常路径 |
P0 |
IsSuperAdmin豁免 |
| TC-0498 |
操作自己 |
caller.UserId==targetUserId |
nil |
正常路径 |
P0 |
self豁免 |
| TC-0499 |
ADMIN跳过部门检查 |
caller=ADMIN |
nil (直接比级别) |
深度业务 |
P0 |
checkDeptHierarchy ADMIN豁免 |
| TC-0500 |
非ADMIN无部门拒绝 |
caller.DeptId=0 |
403 "未归属部门" |
边界 |
P0 |
caller.DeptId==0 |
| TC-0501 |
目标用户无部门 |
target.DeptId=0 |
403 "目标用户未归属部门" |
边界 |
P0 |
target.DeptId==0 |
| TC-0502 |
目标在不同部门 |
目标不在caller子部门 |
403 "无权管理其他部门" |
深度业务 |
P0 |
!HasPrefix |
| TC-0503 |
未登录 |
ctx无UserDetails |
401 |
边界 |
P0 |
|
| TC-0504 |
caller.DeptPath为空时拒绝 |
caller有DeptId但DeptPath="" |
403 "无权管理" |
安全 |
P0 |
H-08修复: DeptPath空串保护 |
9.5 memberTypePriority
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0505 |
各类型优先级正确 |
全部4种+未知 |
SA=0,A=1,D=2,M=3,unknown=MaxInt32 |
白盒 |
P0 |
switch分支 |
十、UserDetailsLoader (loaders/userDetailsLoader.go)
10.1 Load / 缓存
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0506 |
DB加载(缓存miss) |
有效userId+productCode |
返回完整UserDetails |
正常路径 |
P0 |
loadFromDB全链路 |
| TC-0507 |
缓存命中 |
第二次Load同key |
从Redis返回,不查DB |
正常路径 |
P0 |
GetCtx hit |
| TC-0508 |
用户不存在 |
userId=999999 |
返回零值UserDetails(Status=0) |
边界 |
P0 |
loadUser失败 |
| TC-0509 |
productCode为空 |
productCode="" |
跳过产品/成员/角色/权限加载 |
边界 |
P1 |
各方法guard |
10.2 缓存失效
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0510 |
Del删除指定缓存 |
Del(uid, pc) |
缓存被删除,下次Load查DB |
正常路径 |
P0 |
DelCtx |
| TC-0511 |
Clean清除用户所有产品缓存 |
Clean(uid) |
该用户所有key被删 |
正常路径 |
P0 |
KEYS pattern |
| TC-0512 |
CleanByProduct清除产品所有用户 |
CleanByProduct(pc) |
该产品所有key被删 |
正常路径 |
P0 |
KEYS pattern |
| TC-0513 |
BatchDel批量删除 |
BatchDel([uid1,uid2], pc) |
多个key被删 |
正常路径 |
P0 |
DelCtx多key |
| TC-0514 |
BatchDel空数组 |
BatchDel([], pc) |
无操作 |
边界 |
P1 |
len==0 guard |
10.3 loadPerms权限计算
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0515 |
超管全量权限 |
IsSuperAdmin=true |
Perms=全部启用的权限码 |
正常路径 |
P0 |
超管分支 |
| TC-0516 |
ADMIN全量权限 |
MemberType=ADMIN |
Perms=全量 |
正常路径 |
P0 |
ADMIN分支 |
| TC-0517 |
DEVELOPER全量权限 |
MemberType=DEVELOPER |
Perms=全量 |
正常路径 |
P0 |
DEVELOPER分支 |
| TC-0518 |
DEV部门全量权限 |
DeptType=DEV |
Perms=全量 |
正常路径 |
P0 |
DeptTypeDev分支 |
| TC-0519 |
MEMBER角色权限+ALLOW-DENY |
有角色+ALLOW+DENY |
正确计算 |
深度业务 |
P0 |
denySet过滤 |
| TC-0520 |
用户ALLOW权限不跨产品泄漏 |
用户在产品A/B各有ALLOW权限 |
加载产品A时仅含A权限,不含B权限 |
安全 |
P0 |
H-1: FindPermIdsByUserIdAndEffectForProduct |
| TC-0521 |
禁用DEV部门成员无全量权限 |
dept.type=DEV, dept.status=Disabled |
ud.Perms为空 |
安全 |
P0 |
M-3: DeptStatus检查 |
10.4 loadRoles + MinPermsLevel
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0522 |
多角色取最小permsLevel |
用户有level=10和level=5的角色 |
MinPermsLevel=5 |
正常路径 |
P0 |
min计算 |
| TC-0523 |
无角色 |
用户无角色 |
MinPermsLevel=MaxInt64 |
边界 |
P0 |
默认值 |
| TC-0524 |
角色跨产品过滤 |
角色在不同产品 |
仅加载当前产品角色 |
深度业务 |
P0 |
productCode过滤 |
| TC-0525 |
禁用角色不计入 |
角色status=2 |
不在Roles列表中 |
深度业务 |
P0 |
Status==Enabled |
10.5 loadMembership
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0526 |
超管自动设置SUPER_ADMIN |
IsSuperAdmin=true |
MemberType=SUPER_ADMIN, 不查DB |
正常路径 |
P0 |
早期return |
| TC-0527 |
非成员MemberType为空 |
用户非该产品成员 |
MemberType="" |
边界 |
P0 |
ErrNotFound |
| TC-0528 |
禁用成员MemberType为空 |
member.status=Disabled |
ud.MemberType="" |
安全 |
P0 |
H-3: loadMembership |
十一、中间件 — 冻结账号拦截
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0529 |
冻结用户被403 |
有效token但Status=2 |
code=403 "账号已被冻结" |
安全 |
P0 |
ud.Status!=Enabled |
| TC-0530 |
用户不存在(Status=0) |
token中userId不存在 |
code=403 "账号已被冻结" |
安全 |
P0 |
loadUser失败→Status=0 |
| TC-0531 |
UserDetails注入context |
正常请求 |
GetUserDetails(ctx)非nil |
正常路径 |
P0 |
WithUserDetails |
十二、Logic层 — 访问控制负面测试
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0532 |
createDept非超管拒绝 |
ctx=ADMIN |
403 "仅超级管理员" |
安全 |
P0 |
RequireSuperAdmin |
| TC-0533 |
updateDept非超管拒绝 |
ctx=ADMIN |
403 "仅超级管理员" |
安全 |
P0 |
RequireSuperAdmin |
| TC-0534 |
deleteDept非超管拒绝 |
ctx=ADMIN |
403 "仅超级管理员" |
安全 |
P0 |
RequireSuperAdmin |
| TC-0535 |
createProduct非超管拒绝 |
ctx=ADMIN |
403 "仅超级管理员" |
安全 |
P0 |
RequireSuperAdmin |
| TC-0536 |
updateProduct非超管拒绝 |
ctx=ADMIN |
403 "仅超级管理员" |
安全 |
P0 |
RequireSuperAdmin |
| TC-0537 |
createUser非产品管理员拒绝 |
ctx=MEMBER |
403 "仅超级管理员或产品管理员" |
安全 |
P0 |
RequireProductAdminFor(ctx, productCode) |
| TC-0538 |
createRole非产品管理员拒绝 |
ctx=MEMBER |
403 |
安全 |
P0 |
RequireProductAdminFor(ctx, productCode) |
| TC-0539 |
updateRole非产品管理员拒绝 |
ctx=MEMBER |
403 |
安全 |
P0 |
RequireProductAdminFor(ctx, productCode) |
| TC-0540 |
deleteRole非产品管理员拒绝 |
ctx=MEMBER |
403 |
安全 |
P0 |
RequireProductAdminFor(ctx, productCode) |
| TC-0541 |
bindRolePerms非产品管理员拒绝 |
ctx=MEMBER |
403 |
安全 |
P0 |
RequireProductAdminFor(ctx, productCode) |
| TC-0542 |
updateUser-MEMBER不能管理他人 |
ctx=MEMBER, id!=self |
403 (CheckManageAccess拒绝) |
安全 |
P0 |
Audit#4修复: CheckManageAccess权限校验 |
| TC-0543 |
updateUser自己修改DeptId被拒绝 |
ctx含userId=X, req.Id=X, req.DeptId!=nil |
403 "不允许修改自己的部门和状态" |
安全 |
P0 |
H-01修复: 自编辑限制DeptId |
| TC-0544 |
updateUser自己修改Status被拒绝 |
ctx含userId=X, req.Id=X, req.Status!=0 |
403 "不允许修改自己的部门和状态" |
安全 |
P0 |
H-01修复: 自编辑限制Status |
| TC-0545 |
updateUser未登录被拒绝 |
ctx无UserDetails |
401 "未登录" |
安全 |
P0 |
H-01修复: caller==nil |
十三、限流中间件 (middleware/ratelimitMiddleware.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0546 |
正常请求(未超限) |
首次请求 |
请求正常通过, next被调用 |
正常路径 |
P0 |
code!=OverQuota→next |
| TC-0547 |
超限请求被拒绝 |
超出配额后的请求 |
code=429, "请求过于频繁,请稍后再试" |
异常路径 |
P0 |
code==OverQuota→ErrTooManyRequests |
| TC-0548 |
behindProxy=false时XFF被忽略 |
behindProxy=false, 不同XFF头+相同RemoteAddr |
仍被限流, nextCount保持为1 |
安全 |
P0 |
behindProxy=false: 仅用RemoteAddr |
| TC-0549 |
behindProxy=false时X-Real-IP被忽略 |
behindProxy=false, 不同XRI头+相同RemoteAddr |
仍被限流, nextCount保持为1 |
安全 |
P0 |
behindProxy=false: 仅用RemoteAddr |
| TC-0550 |
IP从RemoteAddr解析 |
无代理头, RemoteAddr="ip:port" |
使用SplitHostPort解析host作为限流key |
分支覆盖 |
P0 |
SplitHostPort解析host |
| TC-0551 |
不同IP独立限流 |
两个不同IP |
各自独立计数, 互不影响 |
功能验证 |
P0 |
key隔离 |
| TC-0552 |
behindProxy=true时信任X-Real-IP |
behindProxy=true, 不同X-Real-IP头 |
按X-Real-IP独立限流 |
正常路径 |
P0 |
behindProxy=true: X-Real-IP优先 |
| TC-0553 |
behindProxy=true时无X-Real-IP回退RemoteAddr |
behindProxy=true, 无X-Real-IP头 |
使用RemoteAddr作为限流key |
分支覆盖 |
P0 |
X-Real-IP为空→fallback RemoteAddr |
TC-0554 |
behindProxy=true 时 XFF 仍被忽略 |
— |
— |
— |
— |
已删除:M-6 显式反转契约 —— behindProxy=true 时 XFF 首段合法应优先;新契约由 TC-0862~0866(ratelimitMiddlewareXff_audit_test.go)覆盖 |
| TC-0555 |
RemoteAddr无端口格式 |
RemoteAddr="1.2.3.4"(无端口) |
返回原始RemoteAddr "1.2.3.4" |
边界 |
P1 |
SplitHostPort失败→r.RemoteAddr |
十四、审计修复回归测试 (audit-report.md 2026-04 修复集)
新增测试用于验证近期针对 audit-report.md 的高/中/低风险项所提交的修复,确保修复行为严格生效且不回归。
H-1 BindRoles permsLevel 仅对 MEMBER 生效
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0208 |
MEMBER 调用者不能分配权限级别高于自身的角色 |
caller=MEMBER, MinPermsLevel=50, 目标角色 permsLevel=100 |
403 "不能分配权限级别高于自身的角色" |
越权/安全 |
P0 |
BindRoles H-1 修复:permsLevel 校验仍作用于 MEMBER |
| TC-0711 |
ADMIN 调用者豁免 permsLevel 校验 |
caller=ADMIN, MinPermsLevel=math.MaxInt64, 目标角色任意 permsLevel |
成功绑定 |
正常路径 |
P0 |
H-1 修复:ADMIN/DEVELOPER 不再受 permsLevel 约束 |
| TC-0712 |
DEVELOPER 调用者豁免 permsLevel 校验 |
caller=DEVELOPER, 目标角色任意 permsLevel |
成功绑定 |
正常路径 |
P0 |
H-1 修复:DEVELOPER 豁免 |
| TC-0713 |
MinPermsLevel=MaxInt64 的 MEMBER 不被误阻断 |
caller=MEMBER, MinPermsLevel=math.MaxInt64(未持角色) |
不触发 "不能分配权限级别高于自身" 错误 |
分支覆盖 |
P0 |
H-1 修复:sentinel 值语义 |
H-2/H-3 gRPC GetUserPerms 状态+成员校验
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0700 |
冻结用户 (Status=Disabled) |
GetUserPerms 请求冻结账号 |
gRPC PermissionDenied,msg 含"冻结" |
安全 |
P0 |
H-2 修复:对齐 VerifyToken 的 StatusEnabled 判定 |
| TC-0701 |
非产品成员 |
启用用户但非目标产品成员 |
gRPC PermissionDenied,msg 含"成员" |
安全 |
P0 |
H-2 修复:MemberType=="" 拒绝 |
| TC-0702 |
DEV 部门但产品成员被禁用 |
dept.DeptType=DEV & member.Status=Disabled |
gRPC PermissionDenied |
安全 |
P0 |
H-2+H-3 修复:DEV 部门不再旁路已禁用成员校验 |
| TC-0703 |
启用 ADMIN 成员(正向回归) |
正常启用成员,产品存在权限 |
成功,返回 MemberType=ADMIN 且 Perms 含已配置项 |
正常路径 |
P0 |
H-2 修复后正常路径未被误伤 |
| TC-0704 |
Loader 层:DEV 部门 + 产品成员禁用 |
DEV 启用,member.Status=Disabled |
UserDetails.MemberType="",Perms=[] |
安全 |
P0 |
H-3 修复:禁用成员走入 MemberType 清空分支后不再命中全量权限 |
M-2 批量删除回归
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0707 |
DeleteByUserIdAndRoleIdsTx 批量删除 |
插入 3 条 (user, roleX),批量删除其中 2 条 |
仅保留未被删除的 1 条 |
正常路径 |
P0 |
M-2:循环 DELETE → 批量 IN |
| TC-0708 |
批量删除空列表为 no-op |
roleIds=[] |
无任何删除,原记录保留 |
边界 |
P0 |
M-2:空集合保护 |
| TC-0709 |
批量删除仅作用于指定 userId |
同 roleId 下两个 user,仅删 user1 |
user2 的绑定不受影响 |
约束 |
P0 |
M-2:WHERE userId 严格约束 |
M-3 SuperAdmin 在产品上下文只返回该产品角色
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0181 |
超管在产品上下文查 userDetail |
用户同时持有 test_product 与 other_product 角色 |
resp.RoleIds 只含 test_product 的 roleIds |
越权/隔离 |
P0 |
M-3 修复:不再跨产品返回 |
M-4 FindRoleIdsByUserIdForProduct 过滤 r.status=1
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0706 |
同一产品下同时存在启用/禁用角色 |
user 绑定启用+禁用 2 个角色 |
仅返回启用角色的 id |
安全/过滤 |
P0 |
M-4 修复:SQL 加入 r.status=1 |
M-5 UpdateDept 乐观锁 + 精准缓存清理
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0105 |
DeptType/Status 变更仅清自己的成员缓存 |
变更部门类型 |
UpdateWithOptLock 被调用,不再对子部门做 FindIdsByDeptId 级联清理 |
正常路径 |
P1 |
M-5 修复:不再级联 |
| TC-0714 |
DeptType/Status 未变更时不清缓存 |
只改 name |
无 Clean 调用 |
分支覆盖 |
P1 |
M-5:unchanged 分支 |
| TC-0715 |
乐观锁冲突返回 ErrConflict |
UpdateWithOptLock 返回 0 行 |
返回 409/Conflict |
并发 |
P0 |
M-5:版本号冲突 |
M-6 JWT Claims.Perms 已移除
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0716 |
access token payload 中不得含 perms |
生成 access token 后 base64 解码 payload |
JSON 中不存在 "perms" key |
安全 |
P0 |
M-6:Dead field 清理 |
M-11 DeleteDept TOCTOU 修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0108 |
删除不存在的部门 |
任意不存在的 deptId |
返回 404 "部门不存在" |
错误路径 |
P0 |
M-11:事务内 SELECT FOR UPDATE + 不存在显式报错 |
L-2 产品/管后登录限流独立桶
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0710 |
同 IP,产品登录用尽配额后管后登录仍放行 |
productLoginRL 配额=1 打满,再打 adminLoginRL |
adminLoginRL 正常放行 1 次 |
安全 |
P0 |
L-2 修复:keyPrefix 区分 product/admin |
L-5 Loader 不缓存不存在用户
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0705 |
Load 不存在用户 |
userId=999999999 |
返回空 Username;第二次 Load 行为与首次一致(无缓存污染) |
边界/缓存 |
P1 |
L-5 修复:!ok 分支不缓存零值 |
十一、 本轮新增对抗性用例(审计修复回归)
针对 audit-report.md 中 H-4 / M-1 / M-14 / L-3 / L-5 五个关键修复点补充的"攻击性"测试,覆盖:
- 最后 ADMIN 保护(移除 & 降级、活跃/禁用 ADMIN 计数差异)
- Logout 接口 tokenVersion 递增 + loader 缓存清理
- setUserPerms 对产品禁用的拦截
- updateRole 非超管降低
PermsLevel 的禁止 + 超管例外
- addMember 对已禁用产品的拦截
M-1 Logout 接口:tokenVersion 递增 + 缓存失效
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0720 |
正常登出 |
已登录用户 ctx + WithUserDetails |
用户 tokenVersion=0 → 1;重新 Load 时 TokenVersion 已递增 |
正常路径 |
P0 |
M-1:IncrementTokenVersion + UserDetailsLoader.Del |
| TC-0721 |
未登录调用 /auth/logout |
ctx 中无 userDetails |
返回 401 "未登录" |
错误路径 |
P0 |
M-1:未登录兜底 |
| TC-0722 |
同一用户连续两次 logout |
登出两次 |
tokenVersion 累加至 2 |
幂等/累加 |
P1 |
M-1:每次自增,不被覆盖 |
H-4 产品最后一个 ADMIN 保护
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0723 |
移除产品唯一 ADMIN |
1 个启用 ADMIN |
400 "不能移除该产品的最后一个管理员",ADMIN 仍存在 |
安全 |
P0 |
H-4:CountActiveAdmins<=1 拒绝 |
| TC-0724 |
有 2 个 ADMIN 时移除其一 |
2 个 ADMIN |
成功删除 1 个,另一个保留 |
正常路径 |
P0 |
H-4:非 last-admin 场景放行 |
| TC-0725 |
降级产品唯一 ADMIN 为 MEMBER |
1 个启用 ADMIN |
400 "不能降级该产品的最后一个管理员",MemberType 不变 |
安全 |
P0 |
H-4:updateMember 同逻辑 |
| TC-0726 |
有 2 个 ADMIN 时降级其一 |
2 个 ADMIN |
成功降级为 MEMBER |
正常路径 |
P0 |
H-4:非 last-admin 允许 |
| TC-0727 |
2 个 ADMIN 但只有 1 个启用,降级该启用 ADMIN |
ADMIN(status=1)+ADMIN(status=2) |
400 "不能降级该产品的最后一个管理员" |
安全/边界 |
P0 |
H-4:CountActiveAdmins 只计 status=1 |
| TC-0728 |
移除非 ADMIN(MEMBER) |
1 个 MEMBER |
成功删除,不受 last-admin 保护 |
正常路径 |
P1 |
H-4:仅 ADMIN 触发校验 |
| TC-0729 |
对禁用产品 addMember |
product.status=2 |
400 "产品已被禁用,无法添加成员" |
安全 |
P0 |
L-5:addMemberLogic 新增 product.Status 校验 |
L-3 非超管不得降低 PermsLevel
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0730 |
产品 admin 把 roleA.PermsLevel 从 100 降到 10 |
AdminCtx,PermsLevel 100→10 |
403 "非超管不能降低角色的权限级别",DB 保持 100 |
安全 |
P0 |
L-3:caller.IsSuperAdmin=false && new<old |
| TC-0731 |
产品 admin 保持或提升 PermsLevel |
100→100、100→500 |
均允许;DB 最终 PermsLevel=500 |
正常路径 |
P0 |
L-3:new>=old 放行 |
| TC-0732 |
超管降低 PermsLevel |
SuperAdminCtx,500→10 |
成功 |
正常路径 |
P0 |
L-3:IsSuperAdmin 绕开 |
| TC-0733 |
PermsLevel 越界(0/-1/1000/10000) |
任意非法 PermsLevel |
400 "权限级别必须在 1-999 之间" |
边界 |
P0 |
L-3 前置校验 |
M-14 setUserPerms 产品禁用拦截
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0734 |
产品已禁用 |
product.status=2 |
400 "产品已被禁用,无法设置权限" |
安全 |
P0 |
M-14:新增 product.Status 校验 |
| TC-0735 |
产品不存在 |
虚构 productCode |
404 "产品不存在" |
错误路径 |
P0 |
M-14:FindOneByCode ErrNotFound |
十二、 本轮新增对抗性用例(审计修复回归 · 第二批)
针对 audit-report.md 中 H-A / H-B / M-B / M-C / L-B / L-C / L-F 七个关键修复点补充的"攻击性"测试,覆盖:
SetUserPerms 自我提权前置拦截(RequireProductAdminFor)
IncrementTokenVersion 模型层原子递增 + 返回值 = DB 持久化值 + 缓存强制失效 + 并发唯一性
/auth/refreshToken、/auth/logout 接入 TokenOpLimiter,限流后不得再递增 tokenVersion,且按 userId 隔离
jwtauthMiddleware 校验顺序:TokenVersion 失效优先于 ProductStatus / MemberType
- 产品端登录用户名枚举防护(dummy bcrypt + 统一错误文案 +
ip:username 限流 key)
UpdateUser 跨部门转移 DeptPath 前缀校验(DEVELOPER 不得把目标挪出自身子树)
H-A SetUserPerms 调用者必须是同产品 ADMIN(或超管)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0743 |
普通 MEMBER 给自己授权 |
MemberCtx, targetUserId=self |
403 "需要产品管理员权限",DB 中 userperm 无任何写入 |
安全 |
P0 |
H-A:RequireProductAdminFor 前置拦截 self-escalation |
| TC-0744 |
DEVELOPER 调用者(非 ADMIN)操作他人 |
DeveloperCtx, targetUserId=other |
403 "需要产品管理员权限",DB 无写入 |
安全 |
P0 |
H-A:DEVELOPER 也必须被拦截(非仅 self 场景) |
| TC-0745 |
同产品 ADMIN 操作合法 MEMBER 目标 |
AdminCtx, targetUserId=member |
200 OK,userperm 插入成功 |
正常路径 |
P0 |
H-A:修复后 admin 正向通路仍通畅 |
H-B IncrementTokenVersion 原子递增 + 缓存失效 + 并发唯一
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0736 |
单次 Increment 返回值 == DB 持久值 |
初始 tokenVersion=v0 |
返回值 = v0+1 == DB 读取值 |
正常路径 |
P0 |
H-B:LAST_INSERT_ID(tokenVersion+1) 原子自增,不依赖缓存旧值 |
| TC-0737 |
Increment 后缓存必须被主动清理 |
先 Load 预热缓存,再 Increment |
再次 Load 读到的 TokenVersion 为自增后值(非 stale) |
缓存一致性 |
P0 |
H-B:事务成功后 DelCacheCtx |
| TC-0738 |
10 goroutine 并发自增同一用户 |
起始 v0,并发 Increment×10 |
10 次返回值互不重复,最终 DB = v0+10 |
并发/竞态 |
P0 |
H-B:原子 UPDATE,不会丢失更新 |
L-C /auth/logout 接入 TokenOpLimiter
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0739 |
quota=2 的 limiter,第 3 次 logout |
同一 userId 连续 logout 3 次 |
前 2 次成功 + tokenVersion+2;第 3 次 429 "请求过于频繁",tokenVersion 不再递增 |
安全/限流 |
P0 |
L-C:超限请求不得进入业务层,否则攻击者可反复"自增搅乱缓存" |
| TC-0740 |
限流按 userId 隔离 |
userA 打满配额后 userB 登出 |
userB 正常 logout,不受 userA 限流影响 |
安全/隔离 |
P0 |
L-C:限流 key 必须按 userId 分桶 |
M-B /auth/refreshToken 接入 TokenOpLimiter
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0741 |
quota=1 limiter,burst 第 2 次 |
同一 userId 刷新 2 次 |
第 1 次 200;第 2 次 429,tokenVersion 不得递增(避免攻击者持续废除 refresh token) |
安全/限流 |
P0 |
M-B:refresh 同样走 TokenOpLimiter |
| TC-0742 |
限流按 userId 隔离(productCode 无关) |
userA(p1) 满额 → userB(p1) 刷新 |
userB 正常 200 |
安全/隔离 |
P0 |
M-B:key 不含 productCode,不会跨用户误伤 |
L-B jwtauthMiddleware 校验顺序:TokenVersion 优先
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0749 |
token.tokenVersion != DB & 产品被禁用 |
同时具备两个失败条件 |
返回 401 "令牌已失效"(而非 403 "产品已禁用") |
安全/顺序 |
P0 |
L-B:TokenVersion 比 ProductStatus 先判,避免客户端看到"用户被踢出"后误以为是产品被禁用 |
| TC-0750 |
token.tokenVersion == DB,产品被禁用 |
TokenVersion 对齐,product.status=2 |
403 "该产品已被禁用" |
安全 |
P0 |
L-B:TokenVersion 通过后才看 ProductStatus |
M-C 产品端登录用户名枚举防护
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0751 |
用户名不存在 + 任意密码 |
username="__no_such_user__" |
返回与"存在用户但密码错"完全一致的错误文案("用户名或密码错误"),仍会走 dummy bcrypt 耗时 |
安全/枚举 |
P0 |
M-C:用 dummy hash 比对,防止通过响应差异枚举用户名 |
| TC-0752 |
用户名存在但密码错 |
存在用户 + 错误密码 |
同 TC-0751 相同 code、相同文案 |
安全/对照 |
P0 |
M-C:与 TC-0751 联动,断言两条分支对外表现一致 |
| TC-0753 |
登录限流 key 必须基于 ip:username |
同 IP 攻击 userA 耗尽配额 |
userA 被限流 429,但同 IP 登录 userB 仍放行 |
安全/限流 |
P0 |
M-C:UsernameLoginLimit key = ip:username,避免单 IP 单用户暴力破解拖垮同一 IP 其他用户登录 |
L-F UpdateUser 跨部门转移的 DeptPath 前缀校验
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0746 |
DEVELOPER 把成员挪出自己子树 |
Caller.DeptPath=/100/,targetNewDept.DeptPath=/999/ |
403 "无权将用户转移到该部门",DB 保持原 deptId |
安全 |
P0 |
L-F:strings.HasPrefix(newDept.DeptPath, caller.DeptPath) 前缀校验 |
| TC-0747 |
DEVELOPER 在自己子树内移动成员 |
Caller=/100/,newDept=/100/200/ |
200 OK,DB 更新 deptId |
正常路径 |
P0 |
L-F:子树内放行 |
| TC-0748 |
产品 ADMIN 不受子树限制 |
AdminCtx 跨任意部门 |
200 OK |
正常路径 |
P1 |
L-F:ADMIN 只过 CheckManageAccess,不走子树约束 |
十三、审计修复回归 — 第四轮 (audit-report.md 2026-04-19)
覆盖第四轮审计报告修复项:H-1 ~ H-5(P0),M-1/M-2/M-5/M-6/M-7/M-10/M-11/M-13/M-14/M-15(P1/P2),L-1/L-2(P3)。
H-1 UpdateMember 禁用最后 ADMIN 绕过修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0760 |
保持 memberType=ADMIN 但 status 改为 Disabled |
产品唯一 ADMIN,{"memberType":"ADMIN","status":2} |
400 "不能降级或禁用该产品的最后一个管理员" |
安全 |
P0 |
H-1:wasActiveAdmin && !willBeActiveAdmin 覆盖 status 变化 |
| TC-0761 |
同时降级 + 禁用唯一 ADMIN |
{"memberType":"MEMBER","status":2} |
400 同上 |
安全 |
P0 |
H-1:memberType + status 同时变化 |
| TC-0762 |
有 2 个 ADMIN 时禁用其一 |
2 个启用 ADMIN |
成功,目标 status=2 |
正常路径 |
P0 |
H-1:非 last-admin 场景放行 |
H-2 RemoveMember 事务内视图脱钩修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0763 |
移除活跃 ADMIN(事务内用 locked 数据判断) |
唯一 ADMIN |
400 "不能移除该产品的最后一个管理员" |
安全 |
P0 |
H-2:locked.MemberType 替代事务外 member.MemberType |
| TC-0764 |
移除非 ADMIN 不触发 last-admin 校验 |
MEMBER 身份 |
成功移除 |
正常路径 |
P0 |
H-2:locked.MemberType != ADMIN 跳过检查 |
H-3 CountActiveAdminsTx FOR UPDATE 锁定修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0765 |
CountActiveAdminsTx 返回正确计数 |
产品下 2 个启用 ADMIN + 1 个禁用 ADMIN |
返回 2 |
功能验证 |
P0 |
H-3:SELECT id ... FOR UPDATE 仅计活跃行 |
H-4 DeleteDept 子部门/用户 TOCTOU 修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0766 |
删除有子部门的部门(FOR UPDATE 锁定读) |
parentId 指向目标部门的子部门存在 |
400 "该部门下存在子部门,无法删除" |
业务约束 |
P0 |
H-4:SELECT id ... FOR UPDATE 子部门存在性锁定读 |
| TC-0767 |
删除有关联用户的部门(FOR UPDATE 锁定读) |
deptId 指向目标部门的用户存在 |
400 "该部门下仍有关联用户,无法删除" |
业务约束 |
P0 |
H-4:SELECT id ... FOR UPDATE 用户存在性锁定读 |
| TC-0768 |
CreateDept 父部门 FOR SHARE 锁生效 |
parentId > 0 |
事务内对父部门 SELECT FOR SHARE;父不存在则 404 |
安全 |
P0 |
H-4:CreateDept 防并发删除父部门 |
H-5 + M-15 ChangePassword 限流 + 冻结检查
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0769 |
ChangePassword 超过 TokenOpLimiter 配额 |
同一 userId 连续调用超限 |
429 "操作过于频繁,请稍后再试" |
安全/限流 |
P0 |
H-5:TokenOpLimiter.Take("chpwd:%d") |
| TC-0770 |
冻结用户调用 ChangePassword |
user.Status=Disabled |
403 "账号已被冻结" |
安全 |
P0 |
M-15:bcrypt 前检查 user.Status |
| TC-0771 |
原密码错误时记录日志 |
错误密码 |
400 "原密码错误" + 日志含 change-password old-password mismatch |
可观测 |
P1 |
H-5:失败日志可审计 |
M-1 UpdateUserStatus 无变化不递增 tokenVersion
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0772 |
状态无实际变化(已启用→再启用) |
user.Status=1, req.Status=1 |
返回成功,tokenVersion 不变,用户不被踢下线 |
功能 |
P0 |
M-1:user.Status == req.Status 时跳过写操作 |
| TC-0773 |
状态实际变化(启用→冻结) |
user.Status=1, req.Status=2 |
成功,tokenVersion+1,用户被踢下线 |
正常路径 |
P0 |
M-1:真实变更时正常递增 |
M-2 generateRandomHex 熵修复
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0774 |
appKey 长度=32 hex字符 (16字节) |
CreateProduct |
appKey 长度 = 32 |
功能 |
P0 |
M-2:generateRandomHex(16) → 32 hex chars |
| TC-0775 |
appSecret 长度=64 hex字符 (32字节) |
CreateProduct |
appSecret 长度 = 64 |
功能 |
P0 |
M-2:generateRandomHex(32) → 64 hex chars |
| TC-0776 |
初始管理员密码长度=24 hex字符 (12字节) |
CreateProduct |
adminPassword 长度 = 24 |
功能 |
P0 |
M-2:generateRandomHex(12) → 24 hex chars,96 bit 熵 |
M-5 UpdateRole/BindRolePerms 错误处理
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0777 |
FindUserIdsByRoleId 失败时返回 500 |
mock DB 错误 |
500 "角色已更新但缓存刷新失败" |
容错 |
P0 |
M-5:不再忽略错误 |
M-6 乐观锁 — UpdateRole / UpdateProduct / UpdateMember
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0778 |
UpdateRole 乐观锁冲突 |
并发修改同一角色 |
409 "数据已被其他操作修改,请刷新后重试" |
并发安全 |
P0 |
M-6:UpdateWithOptLock WHERE updateTime=? |
| TC-0779 |
UpdateProduct 乐观锁冲突 |
并发修改同一产品 |
409 同上 |
并发安全 |
P0 |
M-6:UpdateWithOptLock WHERE updateTime=? |
| TC-0780 |
UpdateMember 基于事务内 locked 数据更新 |
正常更新 |
成功,使用 locked 行数据组装 UPDATE |
数据一致 |
P0 |
M-6:事务内 FindOneForUpdateTx 结果作为更新基础 |
M-7 AdminLogin 防用户枚举
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0016 |
普通用户(非超管)登录管理后台 |
正确密码 |
code=401, "用户名或密码错误" |
安全 |
P0 |
M-7:统一错误消息,不暴露账号状态 |
| TC-0021 |
冻结用户登录管理后台 |
正确密码 |
code=401, "用户名或密码错误" |
安全 |
P0 |
M-7:冻结状态不暴露 |
| TC-0781 |
不存在用户登录管理后台响应时间恒定 |
不存在的用户名 |
code=401, "用户名或密码错误",执行 dummy bcrypt |
安全 |
P0 |
M-7:dummy bcrypt 恒时对齐 |
M-10 VerifyToken 失败日志
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0782 |
VerifyToken 各失败分支记录日志 |
invalid token / disabled user / version mismatch 等 |
日志含 verifyToken fail reason= |
可观测 |
P1 |
M-10:每个失败分支有 logx.Infof |
M-11 gRPC Login 缺 peer 时不跳过限流
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0783 |
peer.FromContext 失败时仍限流 |
无 peer 的 ctx |
限流 key 使用 "unknown",超限仍返回 ResourceExhausted |
安全 |
P0 |
M-11:fail-closed 不跳过限流 |
M-14 IsDuplicateEntryErr 类型断言
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0784 |
MySQL 1062 错误正确识别 |
mysql.MySQLError{Number: 1062} |
IsDuplicateEntryErr 返回 true |
功能 |
P0 |
M-14:类型断言替代字符串匹配 |
| TC-0785 |
非 1062 错误不误判 |
其他 MySQL 错误或普通 error |
返回 false |
功能 |
P0 |
M-14:只匹配 1062 |
L-1 去重不修改 req 入参
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0786 |
SetUserPerms 调用后 req.Perms 不变 |
带重复 permId 的请求 |
调用后 req.Perms 长度与调用前一致 |
代码质量 |
P1 |
L-1:使用局部变量去重 |
| TC-0787 |
BindRoles 调用后 req.RoleIds 不变 |
带重复 roleId 的请求 |
调用后 req.RoleIds 长度与调用前一致 |
代码质量 |
P1 |
L-1:使用局部变量去重 |
| TC-0788 |
BindRolePerms 调用后 req.PermIds 不变 |
带重复 permId 的请求 |
调用后 req.PermIds 长度与调用前一致 |
代码质量 |
P1 |
L-1:使用局部变量去重 |
L-2 空 memberType 显式拒绝
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0789 |
caller.MemberType="" 调用 CheckMemberTypeAssignment |
空 memberType 的 caller |
403 "缺少产品成员上下文" |
安全 |
P0 |
L-2:显式分支替代 sentinel 值 |
十三、 本轮新增对抗性用例(QA 主动补齐 · 第三批)
响应"后续测试建议不应由用户手写"的要求, QA 自主完成以下 7 类共 18 个测试:
- JWT 鉴权优先级完整矩阵(L-B 延伸, 覆盖 UserDeleted / Frozen / NonMember / SuperAdmin bypass 四维顺序)
- UpdateDept 真实并发乐观锁(M-5 延伸, 用 goroutine 直打 MySQL, 断言恰好 1 胜 9 冲突)
- TokenOpLimiter 滚动窗口恢复 + Redis fail-open(L-C 延伸, 冻结时间窗语义与宕机容错契约)
- Loader singleflight 合并并发 + 缓存命中(L-5 延伸, 用计数包装拦截
FindOne)
- gRPC VerifyToken / GetUserPerms 契约层 fuzz(新增, 覆盖 13+6 个攻击性 payload, 断言 never-panic + 错误码 taxonomy 稳定)
- handler 薄层 HTTP 契约(新增, logout / refreshToken / changePassword, 冻结参数解析 + 透传协议)
JWT 鉴权优先级完整矩阵(jwtauth_checkorder_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0754 |
用户已被删除 + TokenVersion 失配 |
Username="" + tokenVersion mismatch |
401 "用户不存在或已被删除" |
安全/顺序 |
P0 |
L-B 矩阵:Username empty 必须先于 TokenVersion 裁决, 否则软删除语义泄漏成"登录已失效" |
| TC-0755 |
账号冻结 + TokenVersion 失配 + 产品禁用 |
三重 failing 同时命中 |
403 "账号已被冻结"(而非 401/ProductDisabled) |
安全/顺序 |
P0 |
L-B 矩阵:账号级 > 会话级 > 产品级 的优先级契约 |
| TC-0756 |
TokenVersion OK + 产品启用 + 非超管 + MemberType="" |
曾是成员后被移除 |
403 "您已不是该产品的有效成员" |
安全 |
P0 |
L-B 矩阵:成员移除后 old token 必须被识别 |
| TC-0757 |
SuperAdmin + ProductCode + MemberType="" |
超管 claim 携带 productCode |
200 放行到下游 handler |
正常路径 |
P0 |
L-B 矩阵:超管 bypass 成员校验不可被移除 |
| TC-0758 |
Frozen 用户 + TokenVersion 失配(无 ProductCode) |
冻结账号 + stale token |
403 "账号已被冻结" |
安全 |
P0 |
L-B 矩阵:不走产品分支时 Status 仍先于 TokenVersion |
UpdateDept 真实并发乐观锁(internal/model/dept/updateWithOptLock_concurrent_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0759 |
10 goroutine 同时 UpdateWithOptLock 同一行 |
共享 expectedUpdateTime=t0 |
恰好 1 成功 + 9 ErrUpdateConflict; DB 里 UpdateTime 被推进, Remark 非初值 |
并发/竞态 |
P0 |
M-5:WHERE updateTime=? + RowsAffected 判定, 挡"无声覆盖"退化 |
TokenOpLimiter 滚动窗口恢复 + Redis fail-open(internal/logic/auth/logoutRateLimit_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0790 |
period=1s quota=1, 打满后 sleep 1.2s 再调 |
同一 userId 连续 logout |
第 1 次 200; 第 2 次 429; sleep 后第 3 次 200 且 tokenVersion=2 |
安全/限流 |
P0 |
L-C:限流必须是滚动窗口, 不能退化成永久 deny |
| TC-0791 |
Redis 不可达(127.0.0.1:1 + NonBlock) |
limit.Take 返回 err |
logout 仍成功(fail-OPEN), tokenVersion=1 |
容错契约 |
P0 |
L-C:code, _ := 的工程取舍被冻结, 未来改 fail-close 需 code review |
Loader singleflight 合并并发(internal/loaders/userDetailsLoader_singleflight_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0792 |
50 goroutine barrier 同时 Load 同 userId |
缓存已清空 |
每个 goroutine 都拿到完整数据; FindOne 调用次数 ≤ workers/5, >0 |
并发/缓存 |
P0 |
L-5:singleflight.Group.Do 合并冷启动击穿 |
| TC-0793 |
首次 Load 后再 20 次 Load |
缓存已预热 |
首次 DB 命中 1 次, 后续 0 次(全部走 Redis cache) |
缓存命中 |
P0 |
L-5:写 Redis 成功后应走 fast-path 不再打 DB |
gRPC 契约层 Fuzz(internal/server/permserver_fuzz_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0794 |
VerifyToken 对任意畸形 AccessToken |
13 条种子(空串/alg=none/长串/Unicode/控制符)+ 运行期可扩 -fuzz=... |
never-panic, (resp,nil), resp.Valid=false |
协议健壮性 |
P0 |
gRPC 契约:畸形令牌不得使服务端返回 err != nil 或崩溃 |
| TC-0795 |
GetUserPerms 任意 (appKey,appSecret,productCode,userId) |
6 条种子(空/不存在/SQL 注入样本/Unicode) |
错误码只能在 Unauthenticated/PermissionDenied/InvalidArgument/NotFound/Internal 中 |
协议健壮性 |
P0 |
错误码 taxonomy 冻结, 产品侧权限网关依赖此集合 |
Handler 薄层契约(internal/handler/auth/*、internal/handler/pub/*)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0796 |
LogoutHandler 无 userDetails ctx |
空 ctx |
401 "未登录" |
契约 |
P0 |
handler 正确透传 logic 错误, 不吞成 200/5xx |
| TC-0797 |
LogoutHandler 携带合法 ctx |
注入 UserDetails |
200 + DB 中 tokenVersion=1 |
契约 |
P0 |
handler 必须真正触达 logic, 不能 stub 式伪装成功 |
| TC-0798 |
ChangePasswordHandler 非法 JSON body |
{not-json |
400, 文案不含业务词"原密码" |
契约 |
P0 |
httpx.Parse 错误 → 400 而非 500; 不泄露业务语义 |
| TC-0799 |
ChangePasswordHandler 缺必填字段 |
{} |
400, 文案点名 oldPassword/newPassword |
契约 |
P0 |
goctl required/optional 标注防退化 |
| TC-0800 |
RefreshTokenHandler 缺 Authorization |
无 header |
401 或 400, 文案不含 sql/redis |
契约 |
P0 |
handler 错误文案不得泄露实现细节 |
| TC-0801 |
RefreshTokenHandler 非法 bearer |
Bearer garbage.token.value |
401(绝不 500/200/panic) |
契约 |
P0 |
refresh token 畸形时等价于未登录 |
十四、 本轮新增对抗性用例(QA 主动补齐 · 第四批 / 审计报告修复回归)
本批与最新的 audit-report.md 逐项对齐。每条修复的高/中/低风险点都挂一条或一组独立 TC,
断言 修复后的预期行为(不是源码当前的观测),一旦未来有人把修复改回旧路径,本批用例立刻红。
覆盖域:
- H-1
IncrementTokenVersionIfMatch 原子 CAS + logic 层并发刷新胜负 + 缓存一致性
- H-2 / M-7 gRPC Refresh / Verify IP 级限流 +
extractClientIP 端口剥离契约
- H-3
BindRoles 等级 >= 护栏(等级平行时亦拒绝赋权)
- H-4
UpdateUser 将 deptId 设为 0 需 ADMIN / 超管权限
- L-1
CreateUser 未显式指定时 mustChangePassword 默认为 1(强制首次改密)
- L-4
CheckManageAccess / checkPermLevel 面对 DB 瞬时错误必须 fail-close → 500,而非 "没有角色 → 403"
- M-3
UserDetailsLoader 负缓存 sentinel,保证大量携带"已删除用户 token"的请求不再反复击穿 DB
- M-5
CreateProduct 并发冲突(1062 唯一键)必须映射为 409,而非 500 + 脆弱字符串匹配
- M-6
SyncPerms 事务内锁产品 + 事务内读 perm map + 入参去重 + 1062 → 409
- M-B HTTP 路由
/api/auth/refreshToken 必须挂载 RefreshTokenRateLimit(路由静态 wiring + 行为双重验证)
H-1 CAS 会话劫持防线(internal/model/user/incrementTokenVersionIfMatch_audit_test.go + internal/logic/pub/refreshTokenCas_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0802 |
expected == DB.tokenVersion |
expected=5, DB=5 |
返回 6;DB 落盘 6 |
正常路径 |
P0 |
CAS 成功分支 |
| TC-0803 |
expected != DB.tokenVersion |
expected=9, DB=10 |
ErrTokenVersionMismatch;DB 零副作用 |
安全/并发 |
P0 |
会话劫持窗口拦截 |
TC-0804 |
用户不存在 |
— |
— |
— |
— |
已删除:M-8 取消模型内 FindOne 预检,"CAS 未命中 = ErrTokenVersionMismatch"为最新契约;用户状态分支改由上游 UserDetailsLoader.Load 的 status 字段分流 |
| TC-0805 |
8 goroutine 同时 CAS 同 expected |
N=8 |
恰好 1 成功 + 7 ErrTokenVersionMismatch;DB tokenVersion 只递增 1 |
并发/竞态 |
P0 |
原子性外部可观察证据 |
| TC-0806 |
成功后 id-key / username-key 缓存一致性 |
CAS→1 |
再读两路都看到 1(非 stale 0) |
缓存 |
P0 |
防 middleware 读 stale tokenVersion 放行旧 token |
| TC-0812 |
logic 层 6 goroutine 并发 RefreshToken 同一旧 rt |
N=6 |
1 成功 + 5 × 401 "登录状态已失效";DB 递增 1 |
并发/协议 |
P0 |
H-1 纵深,覆盖 logic 层分支到 CodeError |
H-2 + M-7 gRPC 限流 + client IP 剥端口(internal/server/grpc_rate_limit_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0828 |
同 IP 两次 gRPC RefreshToken,quota=1 |
peer 10.1.2.3:11111 → 10.1.2.3:22222 |
第 1 次 Unauthenticated(业务放行);第 2 次 ResourceExhausted + "过于频繁" |
安全/限流 |
P0 |
H-2:端口变化不得绕过限流 |
| TC-0829 |
同 IP 两次 gRPC VerifyToken,quota=1 |
peer 10.9.8.7:30001 → 10.9.8.7:30002 |
第 1 次 Valid=false + nil err;第 2 次 ResourceExhausted |
安全/限流 |
P0 |
VerifyToken 作为 token oracle 必须受限流保护 |
| TC-0830 |
extractClientIP 对 "host:port" 剥离 |
192.168.0.1:54321 |
返回 192.168.0.1;无 peer 时 error |
契约 |
P0 |
M-7:剥端口契约不得回退 |
| TC-0831 |
gRPC refresh 成功后重放旧 rt(换端口) |
quota 宽松 |
第 2 次 Unauthenticated + "登录状态已失效" |
安全/并发 |
P0 |
H-1 + M-7 纵深交叉 |
H-3 BindRoles 等级 >= 护栏(internal/logic/user/bindRolesEqualLevel_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0813 |
MEMBER 调用者给他人赋予 "与自己 permsLevel 相同" 的角色 |
caller level=50, role level=50 |
403(等级不允许);DB 的 sys_user_role 关系无变化 |
安全/越权 |
P0 |
GuardRoleLevelAssignable 的 >= 防自等升权 |
H-4 UpdateUser deptId=0 门禁(internal/logic/user/updateUserDeptZero_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0814 |
DEVELOPER 将他人 deptId 置 0 |
caller=DEVELOPER |
403;目标 deptId 不变 |
安全/越权 |
P0 |
防"把用户挪出部门树以逃出管理视野" |
| TC-0815 |
MEMBER 将他人 deptId 置 0 |
caller=MEMBER |
403;目标 deptId 不变 |
安全/越权 |
P0 |
同上 |
| TC-0816 |
产品 ADMIN 将他人 deptId 置 0 |
caller=ADMIN |
200;目标 deptId=0 |
正常路径 |
P1 |
合法操作不被误伤 |
| TC-0817 |
SuperAdmin 将他人 deptId 置 0 |
caller=SuperAdmin |
200;目标 deptId=0 |
正常路径 |
P1 |
顶级权限链路通畅 |
L-1 CreateUser 默认强制首次改密(internal/logic/user/createUserMustChangePwd_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0818 |
SuperAdmin 创建用户未显式指定 mustChangePassword |
req 缺字段 |
DB 落盘 MustChangePassword=1 |
默认值/安全 |
P1 |
默认 Yes 才能保证账号发出后立刻被改密 |
L-4 checkPermLevel fail-close(internal/logic/auth/checkPermLevelFailClose_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0819 |
DB 层瞬时错误(mock errors.New("boom")) |
mock 抛通用 err |
CodeError.Code == 500,不是 403 |
鲁棒性/安全 |
P0 |
禁止把 DB 故障曲解为"没角色 → 403" |
| TC-0820 |
DB 返回 sqlx.ErrNotFound |
mock sqlx.ErrNotFound |
403(保留原业务语义) |
分支区分 |
P0 |
只有真正"无角色"才 403,保证可审计 |
M-3 UserDetailsLoader 负缓存(internal/loaders/userDetailsLoader_negativeCache_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0821 |
同一"不存在 userId" 第 2 次 Load |
首次写入 sentinel 后再 Load |
第 2 次 0 次 DB 调用;sentinel 持有 negativeCacheTTL |
防 DoS/缓存 |
P0 |
携带已删除用户 token 的请求不再反复击穿 DB |
| TC-0822 |
负缓存不登记到 userIndex/productIndex |
查不存在用户后观察集合 |
sentinel 不进 Clean 索引,避免误伤 |
缓存一致性 |
P0 |
防 Clean 误删合法 key 或污染统计 |
| TC-0823 |
50 并发 Load 同一不存在 userId |
singleflight + 负缓存协同 |
最终 Redis key = 负缓存 sentinel;无 panic |
并发/缓存 |
P0 |
防并发惊群 + 负缓存收敛 |
M-5 CreateProduct 并发唯一键冲突(internal/logic/product/createProductConflict_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0827 |
mock InsertWithTx 返回 mysql error 1062,message 不含 "uk_code" |
&mysql.MySQLError{Number:1062,Message:"generic"} |
返回 response.ErrConflict(409) |
错误映射 |
P0 |
去掉脆弱 strings.Contains 依赖,靠 mysql.MySQLError.Number 判定 |
M-6 SyncPerms 事务一致性 + 409(internal/logic/pub/syncPermsConflict_audit_test.go + 基础设施 internal/model/perm/findMapByProductCodeWithTx_audit_test.go + internal/model/product/lockByCodeTx_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0807 |
FindMapByProductCodeWithTx 与非事务版返回等价 |
Tx + 产品有启/禁用权限 |
两次调用数据一致;仅 status=1 被返回 |
一致性 |
P0 |
基础设施:事务内读 perm map |
| TC-0808 |
产品无权限时返回 |
空集 |
非 nil 空 map(防 upstream NPE) |
健壮性 |
P1 |
空产品语义固定 |
| TC-0809 |
LockByCodeTx 对存在 code 返回完整行 |
Tx 内 SELECT ... FOR UPDATE |
行数据完整 |
正常路径 |
P0 |
基础设施:事务内锁产品行 |
| TC-0810 |
对不存在 code |
Tx |
sqlx.ErrNotFound |
分支 |
P0 |
让 logic 层分辨"产品不存在" vs "DB 错误" |
| TC-0811 |
两个事务同时锁同一行 |
并发 LockByCodeTx |
后者被阻塞,前者 commit 后才继续 |
并发/锁 |
P0 |
实证 FOR UPDATE 的行级锁语义 |
TC-0824 / TC-0825 |
1062 映射成 409 / logic 映射 409→HTTP 409 |
— |
— |
— |
— |
已删除:H-3 的 LockByCodeTx FOR UPDATE 已经串行化并发同步,1062 在新架构下不可达;tx 内任何未命名错误统一为 SyncPermsError{500},409 重试契约随之取消 |
| TC-0826 |
同一 perm code 在 req 中重复 |
perms = [A, A] |
落盘仅 1 条(入参内部去重) |
防自伤 |
P0 |
入参级去重,避免 tx 内自撞 UNIQUE |
M-B HTTP /refreshToken 中间件挂载(internal/handler/refreshTokenRouteWiring_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0832 |
静态 wiring 检查:routes.go 中 /auth/refreshToken 所在 rest.WithMiddlewares(...) 块必须包含 serverCtx.RefreshTokenRateLimit |
正则匹配 |
命中 |
架构/wiring |
P0 |
防有人把中间件从路由剥离而忘了通知 QA |
| TC-0833 |
行为验证:构造等价中间件链(quota=1),同 IP 连打 2 次;再换 IP 打 1 次 |
RemoteAddr 三个样本 |
首次放行(业务层 401);同 IP 第 2 次 Code=429 "过于频繁";不同 IP 不受影响 |
安全/限流 |
P0 |
与 wiring 正交交叉验证,限流真实生效且按 IP 隔离 |
十五、 本轮新增对抗性用例(QA 主动补齐 · 第五批 / 审计报告第 6 轮修复回归)
本批与 2026-04-19 第 6 轮 audit-report.md 逐项对齐。每条修复均挂一条或一组独立 TC,
断言 修复后的预期行为(不是源码当前的观测)。任何人只要把修复改回旧路径,相应用例立刻 FAIL。
覆盖域:
- H-1
AdminLogin 限流 key 使用 admin:<clientIP>:<username> 双维,换 IP 不得被同一用户名上一次的配额连带锁死
- H-2
ValidateProductLogin 无条件 bcrypt + 冻结/超管状态只在密码正确后才披露(消除账号存在性 & 冻结状态 oracle)
- H-3
SyncPermsService 事务内 LockByCodeTx + FindMapByProductCodeWithTx 串行化同 product 并发同步
- M-1
UpdateDeptLogic 改用 CleanByUserIds 批量清缓存;FindIdsByDeptId 失败仅 Errorf 不再吞错且不返回 500
- M-2
ProductList / ProductDetail / DeptTree 三个接口对非超管实施行/资源级访问控制
- M-4
BindRolePerms / UpdateRole post-commit 缓存获取失败不再映射 500(degraded 成功)
- M-5
CheckManageAccess 支持 WithPrefetchedTarget option,避免单次请求内重复 FindOne
- M-6
ExtractClientIP 解析 X-Forwarded-For 首段 + net.ParseIP 合法性校验 + behindProxy=false 忽略请求头
- M-8
IncrementTokenVersionIfMatch 新签名 (ctx, id, username, expected),调用方透传 username 省掉多一次 FindOne
- L-5
CountOtherActiveAdminsTx 新方法:排除目标自己后计数,正/反向用例均匹配
- L-4 (复盘第 5 轮)
checkPermLevel 对 DB 瞬时错误 fail-close 500(已验证)
- 新增负面契约 H-1/H-2 回归:frozen + wrong password 不得返回 403、远端换 IP 不得被上一次 per-username 计数锁死
H-1 AdminLogin 限流按 IP+username 双维(internal/logic/pub/adminLoginIpLimit_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0834 |
相同 IP + 相同 username 连续打满 quota |
clientIP=1.2.3.4, username=superA,连打 >quota 次错误密码 |
超限后返回 429 "登录尝试过于频繁,请5分钟后再试",同 IP 下同 username 不再放行 |
安全/限流 |
P0 |
H-1:修复后 key=admin:<ip>:<u>;按 quota 阈值打入,需命中 429 |
| TC-0835 |
同 username 但换远端 IP |
clientIP=1.2.3.4 打满后,换 clientIP=5.6.7.8 继续 |
不同 IP 分别计数,换 IP 仍应触达 bcrypt → 进入下游业务断言(此处为密码错误 401),而非继承上一桶 429 |
安全/限流 |
P0 |
H-1:确认 key 含 IP 维度,远端"任何 IP"都能永久锁死的攻击路径被阻断 |
| TC-0836 |
clientIP 缺失(未挂 RateLimit 中间件) |
clientIP 未注入 ctx |
key 退化为 admin:unknown:<u>,仍能正常限流到共享桶,不得直接 panic 或跳过限流 |
安全/鲁棒 |
P0 |
H-1:fail-closed 兜底;未来删除中间件只会退化 key 不会绕过 |
| TC-0837 |
managementKey 无效路径不得消耗 username 配额 |
任意密码,managementKey 错误 |
401 "managementKey无效";同 username 立刻换 managementKey 正确再来仍有完整 quota |
安全 |
P1 |
H-1:managementKey 校验在限流 Take 之前,防匿名攻击者只靠错 key 就把配额打满 |
H-2 ValidateProductLogin 恒时 bcrypt & 延迟披露状态(internal/logic/pub/loginServiceConstantTime_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0838 |
用户冻结 + 错误密码 |
status=2 的用户,密码错误 |
401 "用户名或密码错误"(不能是 403 "账号已被冻结") |
安全/侧信道 |
P0 |
H-2:账号存在性 + 冻结状态必须在密码正确之前完全不可观察 |
| TC-0839 |
用户冻结 + 正确密码 |
status=2 的用户,密码正确 |
403 "账号已被冻结"(奖励性披露) |
正常 |
P0 |
H-2:只有拿到密码后才披露状态,仍保留业务可见性 |
| TC-0840 |
超管走产品端登录 + 错误密码 |
IsSuperAdmin=1,密码错误 |
401 "用户名或密码错误"(不得提前暴露"超管"身份) |
安全/侧信道 |
P0 |
H-2:超管状态同样延迟披露 |
| TC-0841 |
超管走产品端登录 + 正确密码 |
IsSuperAdmin=1,密码正确 |
403 "超级管理员不允许通过产品端登录,请使用管理后台" |
正常 |
P0 |
H-2:披露顺序反转后的正面路径仍保持原有业务语义 |
| TC-0842 |
用户名不存在 |
不存在用户名 + 任意密码 |
401 "用户名或密码错误",走 dummy bcrypt 恒时对齐 |
安全/枚举 |
P0 |
H-2:沿用 dummy hash 路径,不被 H-2 新顺序破坏 |
H-3 SyncPermsService 事务内锁产品 & 事务内读 perm map(internal/logic/pub/syncPermsTxLock_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0843 |
mock 断言事务内必调用 LockByCodeTx |
1 次正常 sync |
LockByCodeTx 在 tx 内被调用过且先于 FindMapByProductCodeWithTx |
架构 |
P0 |
H-3:锁必须落在 tx 内,顺序固定 |
| TC-0844 |
mock LockByCodeTx 返回 sqlx.ErrNotFound |
在 tx 内返回 NotFound |
SyncPermsError.Code == 404,文案 "产品不存在" |
分支 |
P0 |
H-3:tx 内识别产品被删 → 404 |
| TC-0845 |
mock LockByCodeTx 返回通用 error |
boom |
SyncPermsError/500 包裹;不泄露原始 driver 错误 |
容错 |
P1 |
H-3:非 NotFound 错误必须回滚为 500 |
M-1 UpdateDept 批量 Clean + 错误不再吞掉(internal/logic/dept/updateDeptCleanBatch_audit_test.go + internal/loaders/userDetailsLoaderCleanByUserIds_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0846 |
CleanByUserIds 批量清理多用户缓存 |
预埋 3 用户各 2 产品缓存 |
Redis 中 6 条 ud:userId:productCode + 3 条 ud:idx:u:* 均被删除 |
正确性 |
P0 |
M-1 基础设施:SUNION + 批 DEL 必须覆盖所有索引 |
| TC-0847 |
CleanByUserIds 空 ids 切片 |
[] |
立即返回,不 panic,不调用 Redis |
边界 |
P1 |
M-1:防未来调用方传空列表打空 RTT |
| TC-0848 |
UpdateDept 改 deptType 时调 CleanByUserIds |
mock: FindIdsByDeptId → [100,101],断言 CleanByUserIds 路径(通过 mock 的 FindIdsByDeptId 期望 +真实 loader 执行) |
无错误返回;FindIdsByDeptId 被调用恰好 1 次 |
行为 |
P0 |
M-1:UpdateDept 在变更时才触达用户列表 |
| TC-0849 |
UpdateDept 的 FindIdsByDeptId 失败 |
mock 返回 err |
返回 nil(不是 500);旧权限缓存 TTL 兜底 |
容错 |
P0 |
M-1:修复后的 degraded 成功语义 |
M-2 ProductList / ProductDetail / DeptTree 访问控制(internal/logic/product/productListAccessControl_audit_test.go、internal/logic/product/productDetailAccessControl_audit_test.go、internal/logic/dept/deptTreeAccessControl_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0850 |
MEMBER 调 ProductList |
caller.ProductCode=pA |
仅返回 pA 一条(即使 DB 内有 pB、pC) |
安全/访问控制 |
P0 |
M-2:非超管只见自己产品 |
| TC-0851 |
MEMBER 调 ProductList 且 ProductCode=="" |
游离 MEMBER |
返回空列表 Total=0, List=[] |
边界 |
P0 |
M-2:无 productCode 时降级为 0 条 |
| TC-0852 |
MEMBER 调 ProductDetail 查他产品 |
目标 id 属于 pB |
404 "产品不存在"(不暴露存在性) |
安全/枚举 |
P0 |
M-2:区分开"存在但无权"会被当 oracle |
| TC-0853 |
MEMBER 调 ProductDetail 查自己产品 |
目标 id 属于 pA |
200 OK,AppKey 字段为空(保持原 AppKey-hidden 语义) |
正常路径 |
P0 |
M-2:字段级脱敏不被取消 |
| TC-0854 |
超管调 ProductDetail |
任意 id |
200 OK + AppKey 可见 |
正常路径 |
P1 |
M-2:超管路径不受访问控制影响 |
| TC-0855 |
MEMBER 调 DeptTree |
DeptPath="/1/2/" |
返回树中 Path 前缀匹配的子树;父部门/兄弟部门不可见 |
安全 |
P0 |
M-2:按 DeptPath 剪枝 |
| TC-0856 |
MEMBER 调 DeptTree 且 DeptPath="" |
游离成员 |
返回空切片 [] |
边界 |
P0 |
M-2:无 DeptPath 降级空树 |
| TC-0857 |
ADMIN 调 DeptTree |
产品 ADMIN |
返回完整树(ADMIN fullAccess) |
正常路径 |
P1 |
M-2:ADMIN 保留组织视图 |
M-4 BindRolePerms/UpdateRole 缓存获取失败降级(internal/logic/role/postCommitCacheDegrade_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0858 |
BindRolePerms:事务 OK,FindUserIdsByRoleId 返回 err |
mock |
返回 nil(200)——不再映射 500 |
错误映射 |
P0 |
M-4:degraded 成功;客户端不应重试 |
| TC-0859 |
UpdateRole:事务 OK,FindUserIdsByRoleId 返回 err |
mock |
返回 nil(200) |
错误映射 |
P0 |
M-4:同上 |
M-5 CheckManageAccess Prefetched(internal/logic/auth/checkManageAccessPrefetched_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0860 |
传入 prefetched target,不再查 FindOne |
caller=MEMBER,prefetched.DeptId 合法 |
SysUserModel.FindOne 次数 = 0;业务结果同无 option 版本 |
性能/契约 |
P1 |
M-5:避免重复 FindOne |
| TC-0861 |
prefetched target.Id 与参数 targetUserId 不一致 |
被 defensive 忽略 |
依然触发一次 FindOne 真实查询(option 失效) |
安全 |
P1 |
M-5:prefetched 自洽校验,不让调用方传错 id 绕过访问控制 |
M-6 ExtractClientIP XFF/合法性/behindProxy(internal/middleware/ratelimitMiddlewareXff_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0862 |
behindProxy=true + X-Forwarded-For: 1.1.1.1, 2.2.2.2 |
首段合法 |
返回 1.1.1.1 |
契约 |
P0 |
M-6:XFF 首段优先 |
| TC-0863 |
behindProxy=true + X-Forwarded-For 全非法 + X-Real-IP: 10.0.0.1 |
XFF=garbage, abc,XRI 合法 |
fallthrough 到 10.0.0.1 |
契约 |
P0 |
M-6:非法段跳过后走 XRI |
| TC-0864 |
behindProxy=true + 两头均空 |
无 XFF / 无 XRI |
回落到 RemoteAddr 剥端口后的 host |
容错 |
P1 |
M-6:降级路径 |
| TC-0865 |
behindProxy=true + X-Forwarded-For: " 3.3.3.3 " (空白) |
首段带空白 |
返回 3.3.3.3(trim 后合法) |
边界 |
P1 |
M-6:trim 后再解析 |
| TC-0866 |
behindProxy=false + XFF=1.1.1.1 |
RemoteAddr=5.5.5.5:8080 |
忽略 XFF,返回 5.5.5.5 |
安全/伪造 |
P0 |
M-6:不信任客户端注入的头部 |
M-8 IncrementTokenVersionIfMatch 签名(已落地·契约冻结)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0867 |
透传 username 与 DB 一致 |
CAS 正常 |
成功且 username-key 缓存也失效(沿用 TC-0806 验证) |
契约 |
P0 |
M-8:签名扩展不破坏既有 CAS 语义 |
L-5 CountOtherActiveAdminsTx(internal/model/productmember/countOtherActiveAdminsTx_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0868 |
产品内 3 个 active admin,排除其中 1 |
excludeId=第二个 admin |
返回 2 |
计数 |
P0 |
L-5:排除目标后正确计数 |
| TC-0869 |
唯一 active admin,排除他自己 |
excludeId=唯一 admin |
返回 0 → 上层识别为"最后一个" |
语义 |
P0 |
L-5:removeMember/updateMember 据此防"降级/移除最后一个 admin" |
| TC-0870 |
存在 1 个 active + 1 个 disabled admin |
excludeId=active |
返回 0(disabled 不计入) |
语义 |
P0 |
L-5:仍需状态=enabled 才算 |
4.14 第 6 轮过时用例按新契约重写(2026-04-19 · QA 清理补充)
以下 TC 是在清理阶段按 新契约 重写,用于替代被删除的旧用例(见 §4.12、§4.13 各行"已删除"备注)。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0871 |
超管调 ProductList 走分页路径 |
ctx=SuperAdmin, page/pageSize=1/20 |
调用 FindList(1,20),返回列表且每条 AppKey 原样可见 |
正常路径 |
P0 |
M-2:取代旧 TC-0079/0087,钉死"超管路径 != 非超管路径"分叉 |
| TC-0872 |
ProductDetail FindOne 失败 |
DB 返回 err |
统一返回 CodeError{404,"产品不存在"},文案与"他人产品 → 404"完全一致 |
异常路径 |
P0 |
M-2:取代旧 TC-0085,钉死"无差别 404"避免被用作存在性 oracle |
五、第 7 轮审计驱动测试(2026-04-19 · QA 新增)
本轮围绕 audit-report.md 中 H-1 / H-3 / H-4 / M-1 / M-2 / M-3 / M-4 / L-3 / L-5 / L-6 的修复逐条建立回归锚点。
所有新用例均在"真实 DB + Redis + 原子语义"的一等场景下验证,严禁仅靠 mock 对实现做同义复写。
对既有用例的兼容性调整(两处 bindRoles 测试 + RefreshToken_UserDeleted)见 §5.9。
5.1 M-4 FetchInitialCredentials 一次性凭据取回(internal/logic/product/fetchInitialCredentialsLogic_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0901 |
happy path:超管用有效 ticket 取回初始凭据 |
superAdmin ctx + 合法 ticket |
返回 AppKey/AppSecret/AdminUser/AdminPassword;AppSecret 与 DB 中 bcrypt 匹配 |
正常路径 |
P0 |
M-4:凭据落地 Redis 后端,响应体不再明文外泄 |
| TC-0902 |
相同 ticket 二次消费 |
同上 ticket 第二次取 |
第二次 404 "凭据票据不存在或已被消费" |
一次性 |
P0 |
M-4:Redis.GetDelCtx 原子 GET+DEL |
| TC-0903 |
ticket 为空 |
ticket="" |
400 "ticket 不能为空" |
异常路径 |
P0 |
M-4:入参校验 |
| TC-0904 |
ticket 未知 |
随机 64 字符 ticket |
404 "凭据票据不存在或已被消费" |
异常路径 |
P0 |
M-4:无存在性差异化,避免枚举 oracle |
| TC-0905 |
非超管调用 |
ADMIN ctx |
403 |
权限 |
P0 |
M-4:RequireSuperAdmin 生效 |
| TC-0906 |
未登录调用 |
context.Background() |
401/403 |
权限 |
P0 |
M-4:无 UserDetails 时拒绝 |
| TC-0907 |
Redis 中 payload 被人为破坏 |
手工写入非 JSON 字符串 |
500 "凭据数据异常" 并删除该 key |
健壮性 |
P1 |
M-4:防腐败数据长留 |
| TC-0908 |
Redis key 结构正确 |
观察实际 key |
pm:initcred:{ticket};TTL ≤ 300s |
契约 |
P1 |
M-4:运维可定位 |
| TC-0909 |
TTL 与响应 expiresAt 一致 |
观察返回的 credentialsExpiresAt |
Redis TTL == Unix(expiresAt)-now (±5s) |
契约 |
P1 |
M-4:客户端过期提示与后端一致 |
| TC-0910 |
并发消费同一 ticket |
32 goroutine 同时 Fetch |
仅 1 个成功,其余 31 个返回 404 |
并发 |
P0 |
M-4:GetDelCtx 原子性抗竞态 |
| TC-0911 |
CreateProductResp JSON 不含 appSecret/adminPassword |
marshal resp → json |
字段 appSecret/adminPassword 不出现 |
契约 |
P0 |
M-4:响应体永不明文 |
| TC-0912 |
CreateProductResp 必含 credentialsTicket + credentialsExpiresAt |
marshal resp |
两字段均非空/正数 |
契约 |
P0 |
M-4:新的获取链路必备字段 |
5.2 M-1 / H-1 / L-3 / L-6 Loader 契约(internal/loaders/userDetailsLoader_contract_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0913 |
用户不存在 → (ud, nil) 且 ud.Username == "" |
userId=999999999 |
无 err,Username 空,caller 自行映射 401 |
契约 |
P0 |
M-1:401 vs 503 区分 |
| TC-0914 |
新建用户刚落地 CreateUser 后立刻 Load |
新 username |
读到真实 user,不命中负缓存哨兵 |
契约 |
P0 |
L-6:CreateUser 反向清除负缓存 |
| TC-0915 |
partial load 失败(幽灵 deptId) |
人为把 user.deptId 改成不存在 |
返回 ud(含错误子加载),但不写 5 分钟正缓存 |
契约 |
P0 |
M-1/L-3:局部失败不污染主缓存 |
| TC-0916 |
全绿 MEMBER 正路径 |
正常 user + product + 无角色 |
loadOk=true,命中 5 分钟正缓存 |
契约 |
P0 |
H-1:保证好路径仍能缓存 |
5.3 M-2 UpdatePassword/UpdateStatus RowsAffected(internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0924 |
UpdatePassword:FindOne 填缓存后行被并发删除 |
直接 SQL 删行绕过缓存失效 |
ErrUpdateConflict,不得静默成功 |
并发/TOCTOU |
P0 |
M-2:RowsAffected=0 必须升格 |
| TC-0925 |
UpdatePassword:正常写入 |
存在且未并发 |
持久化 + tokenVersion 递增 |
正常路径 |
P0 |
M-2:hot path 不回归 |
| TC-0926 |
UpdatePassword:user 不存在 |
id=非法 |
ErrNotFound(FindOne 先挂) |
异常路径 |
P0 |
M-2:直接失败 |
| TC-0927 |
UpdateStatus:行被并发删除 |
同 TC-0924 手法 |
ErrUpdateConflict |
并发/TOCTOU |
P0 |
M-2:对称覆盖 |
| TC-0928 |
UpdateStatus:正常禁用 |
status=2 |
持久化 + tokenVersion 递增(用户被踢) |
正常路径 |
P0 |
M-2:禁用副作用 |
| TC-0929 |
UpdateStatus:user 不存在 |
id=非法 |
ErrNotFound |
异常路径 |
P0 |
M-2 |
5.4 M-3 GuardRoleLevelAssignable Fresh Read(internal/logic/auth/guardRoleLevelAssignable_freshRead_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0930 |
caller 的 MinPermsLevel 缓存过期(偏高) |
mock 返回 DB=100,caller 缓存=5,role=50 |
403(以 DB=100 判,50 ≤ 100 越级) |
TOCTOU |
P0 |
M-3:不得信任缓存高权值 |
| TC-0931 |
同级分配 |
DB=50,role=50 |
403 "不能分配权限级别高于自身的角色(含同级)" |
契约 |
P0 |
M-3:含同级 |
| TC-0932 |
严格低一级分配 |
DB=50,role=51 |
放行 |
正常路径 |
P0 |
M-3:正值域 |
| TC-0933 |
SuperAdmin 绕过 DB |
superAdmin caller |
不查 DB,直接放行 |
正常路径 |
P0 |
M-3:短路 |
| TC-0934 |
Product ADMIN 绕过 DB |
memberType=ADMIN |
不查 DB,直接放行 |
正常路径 |
P0 |
M-3:短路 |
| TC-0935 |
DEVELOPER 绕过 DB |
memberType=DEVELOPER |
不查 DB,直接放行 |
正常路径 |
P0 |
M-3:短路 |
| TC-0936 |
caller 在 DB 没有任何角色 |
FindMin... 返回 ErrNotFound |
403 "您没有可分配的角色等级" |
异常路径 |
P0 |
M-3:fail-close |
| TC-0937 |
caller 查询 DB 遇到一般错误 |
非 ErrNotFound |
500(fail-close,不透传原文) |
容错 |
P0 |
M-3:不泄细节 |
| TC-0938 |
caller 为 nil(无 UserDetails) |
ctx 未带 |
401(未授权) |
异常路径 |
P0 |
M-3:nil 保护 |
5.5 H-3 CheckAddMemberAccess 部门链 + 超管防御(internal/logic/auth/checkAddMemberAccess_audit_test.go + internal/logic/member/auditFixes_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0940 |
Product ADMIN 拉跨部门树的人 |
caller path=/100/,target path=/200/201/ |
403,错误含 "其他部门" |
权限 |
P0 |
H-3:ADMIN 不再绕过部门树 |
| TC-0941 |
Product ADMIN 在自己部门树内 |
target 在 /100/101/ |
放行 |
正常路径 |
P0 |
H-3:正值域 |
| TC-0942 |
SuperAdmin 跨一切部门 |
caller superAdmin |
放行,不查 dept |
正常路径 |
P0 |
H-3:短路 |
| TC-0943 |
自己加自己 |
target.Id == caller.UserId |
放行 |
正常路径 |
P0 |
H-3:self-bypass |
| TC-0944 |
caller 没有部门(DeptId=0 或 DeptPath="") |
非超管 |
403 |
异常路径 |
P0 |
H-3:无部门 caller 必须拒绝 |
| TC-0945 |
target 无部门(DeptId=0) |
非自己 |
403 |
异常路径 |
P0 |
H-3:目标无部门视为不可纳管 |
| TC-0946 |
ctx 无 caller |
context.Background() |
401 |
权限 |
P0 |
H-3 |
| TC-0947 |
dept.FindOne 报错 |
mock 返回 err |
403(fail-close,文案不泄细节) |
容错 |
P0 |
H-3 |
| TC-0948 |
target 为 nil |
pass nil |
400 BadRequest |
契约 |
P0 |
H-3 |
| TC-0949 |
AddMember 集成:跨部门被拒 |
真实 DB,Product ADMIN 拉树外 |
403 + 不写 sys_product_member |
集成 |
P0 |
H-3 端到端 |
| TC-0950 |
AddMember 集成:target=SuperAdmin |
superAdmin 被作为 MEMBER 加入 |
403 "超级管理员" + 不写 sys_product_member |
安全 |
P0 |
H-3:超管防混入 |
5.6 H-4 JWT 签名算法断言(internal/logic/auth/parseWithHMAC_audit_test.go)
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0951 |
合法 HS256 + 正确 secret |
正常 token |
解析成功,claims 可读 |
正常路径 |
P0 |
H-4:good path |
| TC-0952 |
alg=none 伪造 |
手工拼无签名段 |
拒绝,错误含 "unexpected signing method" |
安全 |
P0 |
H-4:alg=none 零信任 |
| TC-0953 |
alg=RS256 但用 HS secret 签名 |
RSA→HMAC 混淆攻击 |
拒绝 |
安全 |
P0 |
H-4:算法混淆防御 |
| TC-0954 |
alg=ES256 header |
非 HMAC 族 |
拒绝 |
安全 |
P0 |
H-4:非 HMAC 一律拒 |
| TC-0955 |
HS256 但 secret 错 |
同算法错密钥 |
拒绝(签名校验失败) |
安全 |
P0 |
H-4:基础用例 |
| TC-0956 |
ParseRefreshToken 复用 ParseWithHMAC |
伪造 alg=none |
拒绝 |
安全 |
P0 |
H-4:上层也防 |
| TC-0957 |
乱码 token |
"abc.def" |
拒绝(malformed) |
容错 |
P1 |
H-4 |
| TC-0958 |
ParseRefreshToken 的 tokenType 校验 |
AccessToken 类型 |
拒绝 TokenTypeMismatch |
契约 |
P0 |
H-4:不被 H-4 修复破坏 |
| TC-0959 |
合法 HS256 + 非标 typ header |
typ=JWT-X |
解析成功(只严格校验 alg) |
兼容 |
P1 |
H-4:不过度收紧 |
| TC-0960 |
回放过期 token |
exp 已过 |
拒绝(解析器底层校验) |
安全 |
P0 |
H-4 |
5.7 L-5 清理:删除僵尸方法
| 删除对象 |
受影响测试 |
处理 |
SysPermModel.FindMapByProductCode(非事务) |
sysPermModel_test.go |
改用 FindMapByProductCodeWithTx(在事务上下文中调用);findMapByProductCodeWithTx_audit_test.go 独立验证事务版契约 |
SysProductMemberModel.FindMapByProductCodeUserIds |
sysProductMemberModel_test.go |
删除 3 个关联测试(TC-xxx 已标记为已删除) |
SysProductMemberModel.CountActiveAdmins |
— |
无外部调用者,直接删除 |
5.8 M-1 RefreshToken 用户不存在 vs 账号冻结契约分离
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0029(重写) |
refreshToken 合法但 userId 不存在 |
nonExistentUserId |
401 "用户不存在或已被删除" |
契约 |
P0 |
M-1:取代旧 "403 账号已被冻结",前端可正确走"清本地会话+回登录页" |
5.9 M-4 Handler 薄层 + 路由 Wiring(internal/handler/product/fetchInitialCredentialsHandler_audit_test.go + internal/handler/fetchInitialCredentialsRouteWiring_audit_test.go)
填补 §10.4 中"handler/wiring 未测场景"空白:此前 TC-0901 ~ TC-0912 只在 logic 层覆盖 M-4,但 handler 的解析契约、鉴权中间件挂载、前缀归属都未被钉死。
本组 TC 双通道验证:一端走真实 http.HandlerFunc + Redis 发 HTTP 请求,一端 static-scan routes.go 源码断言 middleware 绑定,互为保险。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0961 |
body 非法 JSON |
{not-valid-json + superAdmin ctx |
400,且文案不含 sql/redis/ticket 关键字 |
契约/健壮性 |
P0 |
M-4:httpx.Parse 错误透传;不泄字段与实现细节 |
| TC-0962 |
无登录上下文 |
不注入 UserDetails |
401 "未登录" |
权限 |
P0 |
M-4:handler 自身也必须 fail-close,不依赖 JwtAuth 中间件 |
| TC-0963 |
非超管 ctx |
MemberType=ADMIN |
403 "仅超级管理员可执行此操作",文案不含 "ticket" |
权限/信息泄漏 |
P0 |
M-4:RequireSuperAdmin 透传;防 ticket 存在性 oracle |
| TC-0964 |
超管 + 空 ticket |
{"ticket":""} |
400,文案含 "ticket" |
契约 |
P0 |
M-4:入参必填校验 |
| TC-0965 |
超管 + 未知 ticket |
随机字符串 |
400 "凭证票据无效或已过期",与"过期"共用文案 |
安全 |
P0 |
M-4:防枚举 oracle,与 logic TC-0904 同契约 |
| TC-0966 |
超管 + 已落地 ticket |
手工 SETEX 合法 JSON payload |
HTTP 200 + 4 字段完整映射;Redis key 被 GetDel 消费 |
正常路径/一致性 |
P0 |
M-4:字段映射正确 + 一次性消费 |
| TC-0967 |
静态 wiring:JwtAuth 绑定 |
读取 routes.go 源码 |
/fetchInitialCredentials 所在 rest.WithMiddlewares 列表含 serverCtx.JwtAuth;prefix=/api/product |
回归/静态 |
P0 |
M-4:防未来 goctl 覆写丢失中间件 |
| TC-0968 |
静态反证:不得挂到限流组 |
读取 routes.go 源码 |
/fetchInitialCredentials 绝不出现在 AdminLoginRateLimit / ProductLoginRateLimit / RefreshTokenRateLimit / SyncRateLimit 的中间件块内 |
回归/静态 |
P0 |
M-4:防被错迁到无鉴权组 |
5.10 既有用例兼容性调整(M-3 fresh DB 读)
| 用例 |
文件 |
调整说明 |
| TC-0813 |
bindRolesEqualLevel_audit_test.go |
新增 seedCallerWithRoleLevel 辅助,在 DB 落地真实 caller user + role + user_role;不再仅依赖 UserDetails.MinPermsLevel 直出 |
| TC-0208 |
bindRolesLogic_test.go |
同上,seed caller permsLevel=50 后触发"越级分配 permsLevel=1"断言 |
上述调整的根因:M-3 修复后 GuardRoleLevelAssignable 强一致从 DB 读取 caller 的 MinPermsLevel,原测试用假 UserId 会命中 ErrNotFound → 403 "您没有可分配的角色等级",与真正要验的"含同级"/"越级"语义错位。
六、第 8 轮审计驱动测试设计(2026-04-19)
本轮对照 audit-report.md 第 8 版逐项建立回归锚点:H-2 + M-1 + M-2 + M-3 + L-1 + L-4 五项已落地修复的对抗性回归,
以及 H-1 + L-3 两项未落地缺陷的"契约先行"骨架测试(默认 t.Skip 并带 AUDIT_PENDING 标签,fix 落地后直接打开 AUDIT_RUN_PENDING=1 即可强制跑红)。
本轮既有测试兼容性调整(Round 7 → Round 8 代码变更触发):4 个已有 test 的 mock 需补 caller 侧 FindMinPermsLevelByUserIdAndProductCode 期望、1 个 productmember 测试移除对已删除 CountActiveAdminsTx 的引用,详见 §6.10。
6.1 H-2 checkPermLevel fresh DB 读(internal/logic/auth/checkPermLevelFreshRead_audit_test.go)
修复目标:checkPermLevel 不再信任 caller.MinPermsLevel 缓存,改走 loadFreshMinPermsLevel 强一致读 DB,
与 GuardRoleLevelAssignable(Round 7 已修)口径对齐,关闭降级 admin 的 5min 缓存 TOCTOU 窗口。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0969 |
caller 缓存级别高(10)但 DB 已降级(20),target 级别 15 |
管理高级目标 |
403 "无权管理权限级别高于或等于您的用户" |
对抗/安全 |
P0 |
H-2:钉死缓存 TOCTOU |
| TC-0970 |
caller 在 DB 中无任何角色(ErrNotFound) |
非超管/非 self 管理路径 |
403(按"无角色 = 最低等级"处理) |
安全 |
P0 |
H-2:ErrNotFound 等同最低 |
| TC-0971 |
caller DB 级别(5)严格高于 target(15) |
正常管理 |
放行 |
正向 |
P0 |
H-2 正向不回归 |
| TC-0972 |
caller 侧 FindMinPermsLevelByUserIdAndProductCode 通用 DB 错 |
— |
500 "校验权限级别失败"(fail-close) |
容错 |
P0 |
H-2:一般错误非 ErrNotFound → 500 |
| TC-0973 |
caller 是 SuperAdmin |
CheckManageAccess 链路 |
短路放行,不发生 caller-side FindMin 查询 |
正向/优化 |
P1 |
H-2:SuperAdmin 必短路 |
| TC-0974 |
caller.UserId == targetUserId(自操作) |
同上 |
短路放行,不发生 caller-side FindMin 查询 |
正向/优化 |
P1 |
H-2:self 必短路 |
| TC-0975 |
共享 helper loadFreshMinPermsLevel 的契约对齐 |
通用 err / ErrNotFound |
分别返回 (0, false, err) 与 (0, true, nil) |
契约 |
P0 |
H-2:helper 契约与 GuardRoleLevelAssignable 同步 |
6.2 M-1 CreateProduct Redis/ticket 失败补偿(internal/logic/product/createProductCompensation_audit_test.go)
修复目标:DB 事务 commit 之后任何一步(RandomHex / Marshal / Redis SetexCtx)失败,必须回滚补偿掉 sys_product + sys_user + sys_product_member 三张新建行,防止"产品 + 管理员孤儿化 → 只能 DBA 手动 SQL 清"。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0976 |
Redis 整个不可达(SetexCtx 永久失败) |
正常 CreateProduct 请求 + broken Redis |
返回 5xx 错误;sys_product/sys_user/sys_product_member 行数各 0 |
对抗/一致性 |
P0 |
M-1:失败链路必须把三张新建行全部补偿掉 |
| TC-0977 |
Redis 失败 + 补偿成功后以同 Code 重建 |
正常 Redis + 相同 productCode |
第二次创建成功,不被 UNIQUE 约束阻塞 |
正向/幂等 |
P0 |
M-1:补偿把位点清空,同 Code 不卡住 |
| TC-0978 |
补偿顺序显式校验(child → parent) |
观察三表最终行数 |
sys_product_member、sys_user、sys_product 均为 0 |
契约 |
P0 |
M-1:删除顺序与外键契约一致 |
6.3 M-2 SyncPerms 404 映射(REST + gRPC)
| 文件 |
覆盖通道 |
TC 编号 |
internal/logic/pub/syncPerms404_audit_test.go |
REST(SyncPermsLogic) |
TC-0979 / TC-0980 |
internal/server/syncPermissions404_audit_test.go |
gRPC(PermServer.SyncPermissions) |
TC-0981 / TC-0982 |
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0979 |
REST:tx 内 LockByCodeTx → ErrNotFound |
AppKey/AppSecret 有效但产品被并发删 |
返回 response.CodeError{Code:404, "产品不存在"} |
契约 |
P0 |
M-2:REST 侧必映射 404 |
| TC-0980 |
REST 反例:未映射 se.Code=500 |
tx 内非业务 err |
继续走 default 分支原样透传 SyncPermsError |
契约/反向 |
P1 |
M-2:防一刀切把 500 误归 404 |
| TC-0981 |
gRPC:同 LockByCodeTx ErrNotFound |
同 TC-0979 输入 |
status.Code() == codes.NotFound,文案 "产品不存在" |
契约 |
P0 |
M-2:gRPC 对外契约 |
| TC-0982 |
gRPC 反例:未映射 code |
同 TC-0980 |
codes.Internal,不得被误分类为 NotFound |
契约/反向 |
P1 |
M-2:防 SDK 误触发重试 |
6.4 M-3 RefreshToken 先签后 CAS(internal/logic/pub/refreshTokenSignBeforeCas_audit_test.go)
修复目标:调换 签名 ↔ CAS ↔ Clean 顺序,任何签名失败不得推进 tokenVersion;配合 M-3 顺序收敛"签 token 副作用"与"DB 状态变更"。
HMAC 本身不会失败,因此本组通过可观测的"预测版本 ↔ DB 新版本 ↔ 新 token.TokenVersion 三者必须严格相等"来把契约钉死。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0983 |
成功路径:DB tokenVersion +1 且新 access/refresh claims.TokenVersion 严格等于 DB 新值 |
合法旧 refresh |
三者相等 |
正向/契约 |
P0 |
M-3:预签版本 ↔ CAS 版本一致 |
| TC-0984 |
CAS 不命中("其他并发赢家已把 DB 推到 version=1"后再来一次 claims=0) |
抢先 IncrementTokenVersionIfMatch |
返回 401 "登录状态已失效",DB tokenVersion 不得再 +1 |
对抗/一致性 |
P0 |
M-3:失败分支绝不推进 DB |
| TC-0985 |
多轮链式刷新 + 旧 token 重放 |
连刷两次后重放第 1 次的新 refreshToken |
第三次 401,DB tokenVersion 不得再 +1 |
对抗 |
P0 |
M-3:新 token 版本号必须匹配 DB,重放必拦 |
6.5 L-1 sysPermModel 占位符契约(internal/model/perm/l1StatusPlaceholder_audit_test.go)
修复目标:FindAllCodesByProductCode / DisableNotInCodesWithTx 的 status = ? 必须走 prepared statement 参数占位符,与 L-4 修复风格对齐;SQL 指纹稳定 + 未来新增枚举值(如 "审核中")不被 Sprintf 版误收。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0986 |
产品含 status ∈ {1, 2, 99} 三类行 |
FindAllCodesByProductCode |
只返回 status=1 的 code |
契约 |
P1 |
L-1:严格 = 不模糊 |
| TC-0987 |
DisableNotInCodesWithTx 只禁用 status=1 且不在白名单的行 |
同上数据 + 白名单含 status=1 的一条 |
仅 1 行被 1→2,99 和既有 2 均不动 |
契约 |
P1 |
L-1:WHERE 严格等值,未来枚举值不被误伤 |
6.6 L-4 SetUserPerms 事务末 COUNT 复核(internal/logic/user/setUserPermsCountRecheck_audit_test.go)
修复目标:关闭 FindByIds 通过 → tx 内 BatchInsertWithTx 落盘之间的 TOCTOU 窗口,防止"脏 user_perm 行与 sys_perm 的 disabled 状态不一致"。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0988 |
FindByIds 前置校验通过(装饰器撒谎 status=1)但 DB 实际 status=2 |
正常请求 |
409 "部分权限在提交时已被禁用",sys_user_perm 必须 0 行脏数据 |
对抗/一致性 |
P0 |
L-4:COUNT 复核失效 → 立即可见 |
| TC-0989 |
全部真实 Enabled 的正向基线 |
两条 perm + ALLOW/DENY 各一 |
2 行落盘 |
正向 |
P0 |
L-4:防止误杀 |
6.7 H-1 PII 暴露 —— 契约先行骨架(internal/logic/user/userDetailPIIMask_audit_test.go)
本组为"对抗性未修复回归":默认 t.Skipf("AUDIT_PENDING H-1 …"),CI 通过 AUDIT_RUN_PENDING=1 可强制跑红。修复方案参考 audit-report.md H-1 条目(CanViewContact + filterPIIForCaller + MaskEmail / MaskPhone)。
| TC编号 |
测试场景 |
输入 |
预期结果(fix 后) |
类型 |
优先级 |
覆盖说明 |
| TC-0990 |
同产品 MEMBER 互看 |
caller 与 target 同产品同级 |
Email 脱敏、Phone 脱敏、Remark 清空 |
安全/PII |
P0 |
H-1:最核心攻击面 |
| TC-0991 |
自看 |
caller.UserId == target.Id |
原样返回 Email/Phone/Remark |
正向 |
P0 |
H-1:fix 后不得误伤 self-view |
| TC-0992 |
SuperAdmin 看任何人 |
caller.IsSuperAdmin |
原样返回 Email/Phone/Remark |
正向 |
P0 |
H-1:防回归(超管被误脱敏) |
6.8 L-3 DeptId=0 legacy 账号契约先行骨架(internal/logic/auth/checkManageAccessDeptZero_audit_test.go)
同样默认 t.Skipf("AUDIT_PENDING L-3 …")。
| TC编号 |
测试场景 |
输入 |
预期结果(fix 后) |
类型 |
优先级 |
覆盖说明 |
| TC-0993 |
legacy DEVELOPER(DeptId=0、DeptPath="")去管理他人 |
合法 target + productCode |
response.CodeError{Code:403},文案含 "未归属"(供运维触发数据迁移工单) |
契约 |
P2 |
L-3:文案与错误结构一致化 |
6.9 第 8 轮新增 TC 汇总
| 审计条目 |
文件 |
TC 编号区间 |
数量 |
状态 |
| H-2 |
checkPermLevelFreshRead_audit_test.go |
TC-0969 ~ TC-0975 |
7 |
✅ |
| M-1 |
createProductCompensation_audit_test.go |
TC-0976 ~ TC-0978 |
3 |
✅ |
| M-2 (REST) |
syncPerms404_audit_test.go |
TC-0979 / TC-0980 |
2 |
✅ |
| M-2 (gRPC) |
syncPermissions404_audit_test.go |
TC-0981 / TC-0982 |
2 |
✅ |
| M-3 |
refreshTokenSignBeforeCas_audit_test.go |
TC-0983 ~ TC-0985 |
3 |
✅ |
| L-1 |
l1StatusPlaceholder_audit_test.go |
TC-0986 / TC-0987 |
2 |
✅ |
| L-4 |
setUserPermsCountRecheck_audit_test.go |
TC-0988 / TC-0989 |
2 |
✅ |
| H-1(未修) |
userDetailPIIMask_audit_test.go |
TC-0990 ~ TC-0992 |
3 |
⏸ Skip |
| L-3(未修) |
checkManageAccessDeptZero_audit_test.go |
TC-0993 |
1 |
⏸ Skip |
| 合计 |
— |
TC-0969 ~ TC-0993 |
25 |
20 ✅ + 5 ⏸ |
6.10 既有测试兼容性调整(Round 8 代码变更触发)
| 用例 |
文件 |
调整说明 |
| TC-0818 / TC-0820 / TC-0821 |
internal/logic/auth/checkManageAccessPrefetch_audit_test.go |
H-2 修复后 checkPermLevel 会对 caller.UserId 发一次 FindMinPermsLevelByUserIdAndProductCode,原 mock 只覆盖 target 侧,补上 caller 侧 EXPECT(返回 50 或 sqlx.ErrNotFound)。 |
TC-0823(TestCheckManageAccess_ErrNotFound_StillTreatedAsNoRole) |
internal/logic/auth/checkPermLevelFailClose_audit_test.go |
同上。 |
TC-0869(CountOtherActiveAdminsTx ↔ CountActiveAdminsTx 一致性) |
internal/model/productmember/countOtherActiveAdmins_audit_test.go |
L-2 修复删除了 CountActiveAdminsTx,原断言改为直接断言 int64(2) 对齐已知种子数据。 |
调整根因:H-2 的 fresh read 把原本只需要 target 侧的 mock 扩展到 target + caller 两路;L-2 的 API 面收敛要求任何依赖 CountActiveAdminsTx 的代码必须切到 CountOtherActiveAdminsTx 或直接种子数据。
7. Round 9 审计驱动测试(M-N1 / M-N2 / M-N3 / M-N4 / L-N1 / L-N2 / L-N3 / L-N4)
本轮 7 项修复(audit-report.md 本轮结论节)共增 TC-0994 ~ TC-1014 计 21 条新用例,并对既有测试按新契约作适配。
7.1 M-N4 CreateUser 部门链防护(internal/logic/user/createUserDeptChain_audit_test.go)
修复目标:CreateUser 必须对非超管 caller 做 caller.DeptPath → newDept.Path 前缀校验,并拒绝非超管 DeptId=0;目标部门 status!=Enabled 一律拒绝(与 L-N2 对齐)。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0994 |
产品 ADMIN 为非自己管辖部门创建用户 |
caller DeptPath=/100/ 目标部门 /999/ |
403 "无权在非自己管辖的部门下创建用户" |
对抗/越权 |
P0 |
M-N4:防产品 ADMIN 预埋 admin*/ops* 等关键用户名合谋 AddMember |
| TC-0995 |
产品 ADMIN 在自己子树下创建用户 |
caller /200/ → 目标 /200/1/ |
正常创建,DB 落盘 |
正向 |
P0 |
M-N4:正向回归,不得误伤合法路径 |
| TC-0996 |
SuperAdmin 在任意部门 / DeptId=0 均可创建 |
超管两条路径 |
两条均成功,支撑跨组织系统账号语义 |
正向 |
P0 |
M-N4:超管豁免分层 |
| TC-0997 |
caller.DeptPath=="" 的 legacy 产品 ADMIN |
DeptId 指向真实部门 |
403 "您未归属任何部门,无权创建用户" |
对抗 |
P1 |
M-N4:legacy 账号 fail-close |
| TC-0998 |
非超管 caller 传 DeptId=0 |
任意合法用户名 |
400 "必须指定部门" |
契约 |
P1 |
M-N4:阻断非超管在部门树外开口 |
| TC-0999 |
目标部门 status=Disabled |
超管 → 已禁用部门 |
400 "目标部门已停用" |
契约 |
P1 |
L-N2:与 UpdateDept 闭环 |
7.2 M-N3 RoleDetail 枚举 Oracle 关闭(internal/logic/role/roleDetailOracle_audit_test.go)
修复目标:非超管跨产品访问 role 与 "id 不存在" 必须返回完全一致的 404 响应,关闭"遍历 id 画出跨产品 roleId 分布图"的侧信道。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1000 |
非超管访问别的产品的 role |
Admin in test_product;目标 role 在 mn3_other_xxx |
404 "角色不存在" |
安全/Oracle |
P0 |
M-N3:跨产品必须 404 而非 403 |
| TC-1001 |
"id 不存在" vs "跨产品" 响应对比 |
两条路径对照 |
code + body 完全一致 |
安全/Oracle |
P0 |
M-N3:彻底消除枚举 oracle |
| TC-1002 |
超管跨产品访问 |
超管 → 跨产品 role + permIds |
正常返回完整 RoleItem |
正向 |
P0 |
M-N3:审计/运维路径不得被误伤 |
7.3 M-N2 UserDetailsLoader.BatchDel Pipelined 索引清理(internal/loaders/userDetailsLoader_batchdel_mn2_audit_test.go)
修复目标:BatchDel 把 N 个用户的 userIndex/productIndex SREM 合进一次 Pipelined RTT,替换历史 2N 次串行调用,关闭"角色绑千人 → UpdateRole/BindRoles 尾延迟秒级"风险。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1013 |
N=2 的真实缓存场景 |
2 用户各 Load 预热后 BatchDel |
主 key DEL、userIndex/productIndex 中 2×3 元素全部 SREM |
契约/缓存 |
P1 |
M-N2:同步清理不能被回退 |
| TC-1014 |
productCode="" 分支 |
无效 uid + 空 productCode |
不 panic / 不报错 |
契约/防御 |
P2 |
M-N2:pipeline 分支 fail-safe |
7.4 M-N1 UserDetailsLoader.Load 返回 ErrLoaderDegraded(internal/loaders/userDetailsLoader_contract_audit_test.go)
修复目标:partial load failure 必须向上冒 ErrLoaderDegraded(而非半成品 (ud, nil)),由调用方映射为 503 / codes.Unavailable;TC-0917 新增 sentinel 稳定性契约。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-0915(重写) |
productCode 不存在触发 loadProduct 失败 |
Load(userId, "unknown_code") |
ud=nil, err=ErrLoaderDegraded;5 分钟正缓存不写入 |
契约/错误 |
P0 |
M-N1:fail-close 不生成半成品 |
| TC-0914(补真实产品) |
负缓存投毒防御 (L-6 主题) 不得被 M-N1 误伤 |
真实 productCode + 新用户首次 Load |
不写 _NOT_FOUND_ 哨兵 |
正向 |
P0 |
M-N1:L-6 路径不得退化 |
| TC-0917(新增) |
ErrLoaderDegraded 作为 sentinel 可 errors.Is 断言 |
直接断言导出符号 |
errors.Is(ErrLoaderDegraded, ErrLoaderDegraded) == true |
契约 |
P2 |
M-N1:调用方可稳定识别 |
7.5 L-N1 ParseWithHMAC 中央化(internal/middleware/parseWithHMAC_centralized_audit_test.go)
修复目标:ParseWithHMAC 上移到 middleware 层作为 HTTP 中间件、gRPC VerifyToken、RefreshToken 等所有 JWT 解析点的唯一入口;logic/auth 的同名函数仅是 alias,避免"算法混淆防御"被任何调用点 drift。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1003 |
合法 HS256 token |
正常 Claims |
解析通过,token.Valid == true |
正向 |
P0 |
L-N1:中央入口正向路径 |
| TC-1004 |
alg=RS256(公钥→HMAC 混淆)伪造 |
攻击者用 secret 当 HS256 密钥签署 |
unexpected signing method 错误 |
安全 |
P0 |
L-N1:CVE-2016-10555 同类防御 |
| TC-1005 |
alg=none 伪造 |
空签名段 |
错误返回 |
安全 |
P0 |
L-N1:深度防御 |
| TC-1006 |
HS256 但错误 secret |
合法结构 + 猜测 secret |
签名校验失败 |
安全 |
P0 |
L-N1:签名错误必拦 |
| TC-1007 |
任意 jwt.Claims 结构体 |
自定义 customClaims |
解析通过,字段可转型回取 |
契约 |
P1 |
L-N1:与具体 claims 类型解耦,所有调用方可复用 |
7.6 L-N3 AdminLogin 三分支时序等齐(internal/logic/pub/adminLoginTiming_audit_test.go)
修复目标:IsSuperAdmin 判断前置到真 bcrypt 之前,"存在但非超管"分支也必须跑一次 dummyBcryptHash,关闭"基于耗时差筛出存在的超管账号"的时序 oracle。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1008 |
非超管+错密码 vs 用户不存在 |
错密码 / 不存在用户名 |
code + body 完全一致 |
安全/Oracle |
P0 |
L-N3:响应不得区分两条分支 |
| TC-1009 |
非超管+正确密码 |
真实普通用户正确密码 |
仍 401 "用户名或密码错误" |
安全 |
P0 |
L-N3:不得以 200 暴露账号存在性 |
| TC-1010 |
两条 dummy bcrypt 分支时序 |
连续 3 次平均耗时 |
非超管+错密 与 不存在 耗时比 <3× |
时序/性能 |
P0 |
L-N3:若比例 >3× 说明 L-N3 被回退(非超管分支跳过了 dummy bcrypt) |
7.7 L-N4 UpdateUserStatus 乐观锁(internal/logic/user/updateUserStatusOptLock_audit_test.go + internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go)
修复目标:SysUserModel.UpdateStatus(ctx, id, status, expectedUpdateTime) 以 updateTime 作为 CAS 锚点;并发冻结/解冻 last-write-wins 被关闭,Logic 把 ErrUpdateConflict 映射为 409。
| TC编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1011 |
他人先冻结后本轮解冻 |
先跑 Update → UpdateTime 推进,本轮仍持旧 updateTime 直冲 model |
model 层 ErrUpdateConflict;Logic happy path 解冻成功且 updateTime 推进 |
并发/CAS |
P0 |
L-N4:CAS 失败路径 + 正向回归 |
| TC-1012 |
Logic 层错误映射 |
model 层强制 ErrUpdateConflict |
映射为 response.ErrConflict(409, "数据已被其他操作修改,请刷新后重试") |
契约 |
P1 |
L-N4:文案与 code 对齐 |
| 既有 TC(适配) |
TestSysUserModel_UpdateStatus_* 三条 |
传入 expectedUpdateTime |
签名变更后仍通过;happy path 新增 time.Sleep 保证秒级 updateTime 推进 |
适配 |
P0 |
L-N4:mock/测试对齐新签名 |
7.8 L-N2 UpdateUserLogic 目标部门状态校验
与 CreateUser 一致(见 TC-0999),UpdateUser 侧已在 updateUserLogic.go 加入 newDept.Status != Enabled 分支;目标覆盖由现有 updateUserDeptScope_audit_test.go 家族承担,本轮无需新增(改动语义与 TC-0999 对称)。
7.9 既有测试兼容性调整(Round 9 代码变更触发)
| 用例 |
文件 |
调整说明 |
TestSysUserModel_UpdateStatus_* 三条 |
internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go |
L-N4:UpdateStatus 新签名 (ctx, id, status, expectedUpdateTime)。冲突用例改为先 FindOne 拿 UpdateTime 再传;happy path 追加 1.1s time.Sleep 确保秒级 updateTime 严格推进;NotFound 用例传 0。 |
MockSysUserModel.UpdateStatus |
internal/testutil/mocks/mock_user_model.go |
对齐 sysUserModel 的新签名,新增 expectedUpdateTime any 参数。 |
| TC-0915 (M-N1 重写) |
internal/loaders/userDetailsLoader_contract_audit_test.go |
改断言 ErrLoaderDegraded + ud==nil;放弃旧"半成品 + 未写正缓存"双重契约。 |
| TC-0914 (L-6 去相干) |
同上 |
补插真实 sys_product 行,避开被 M-N1 升级为 ErrLoaderDegraded 的干扰路径。 |
refreshTokenLogic.go / jwtauthMiddleware.go |
internal/logic/pub/refreshTokenLogic.go 与 internal/middleware/jwtauthMiddleware.go |
Load 返回 err != nil 时一律映射 503 "服务暂时不可用,请稍后重试",不得与"用户不存在"同化为 401。本调整在 Round 9 已有对应回归路径,未改测试用例名。 |
7.10 Round 9 新增 TC 汇总
| 审计条目 |
文件 |
TC 编号区间 |
数量 |
状态 |
| M-N1 |
userDetailsLoader_contract_audit_test.go |
TC-0915(重写)/ TC-0917(新增) |
2 |
✅ |
| M-N2 |
userDetailsLoader_batchdel_mn2_audit_test.go |
TC-1013 / TC-1014 |
2 |
✅ |
| M-N3 |
roleDetailOracle_audit_test.go |
TC-1000 ~ TC-1002 |
3 |
✅ |
| M-N4 |
createUserDeptChain_audit_test.go |
TC-0994 ~ TC-0998 |
5 |
✅ |
| L-N1 |
parseWithHMAC_centralized_audit_test.go |
TC-1003 ~ TC-1007 |
5 |
✅ |
| L-N2 |
createUserDeptChain_audit_test.go |
TC-0999 |
1 |
✅ |
| L-N3 |
adminLoginTiming_audit_test.go |
TC-1008 ~ TC-1010 |
3 |
✅ |
| L-N4 |
updateUserStatusOptLock_audit_test.go |
TC-1011 / TC-1012 |
2 |
✅ |
| 合计 |
— |
TC-0994 ~ TC-1014(含 TC-0915 重写 / TC-0917 新增) |
23 |
23 ✅ |
8. Round 10 审计驱动测试(M-R10-1 / M-R10-2 / M-R10-3 / M-R10-4 / M-R10-5 / L-R10-1 / L-R10-2 / L-R10-10)
本轮对第 10 轮审计报告(audit-report.md,2026-04-19)中 5 条 Medium 与 3 条已落地 Low 做回归闭环;Low 中 L-R10-3 ~ L-R10-9 或留作"机会性优化"或标注"不修",不入测试用例。
8.1 M-R10-1 SyncPerms 事务内 product.Status 复核(internal/logic/pub/syncPermsService.go)
修复目标:LockByCodeTx 返回的 row 必须满足 Status == Enabled,否则立刻 SyncPermsError{Code:403} 出场;防御"产品外层预检查通过 → 事务内被并发 Disable → 仍继续写 sys_perm"的时序漏洞。审计重点是不涉及安全越权(loadPerms 对 Disabled 产品统一返 nil),但关闭"刚被禁用的产品 1 秒内继续生成 perm diff"的观测与审计假象。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1021(适配) |
syncPerms404_audit_test.go 原用例 mock LockByCodeTx 返回 SysProduct{Status: 1} |
原场景保持不变 |
行为不变 |
适配 |
P0 |
M-R10-1:所有既有 audit 路径都必须显式带 Status=1,否则命中 403 分支 |
| TC-1022(适配) |
syncPermsDedup_audit_test.go / syncPermsLogic_mock_test.go / syncPermsTxLock_audit_test.go |
同上 |
行为不变 |
适配 |
P0 |
M-R10-1:对事务内 Status 复核全覆盖 |
| TC-1023(适配) |
syncPermissions404_audit_test.go gRPC 入口 |
LockByCodeTx 必带 Status=1 |
UnmappedCode 仍走 Internal |
适配 |
P0 |
M-R10-1:gRPC 层也落入同一契约 |
8.2 M-R10-2 BindRolePerms / BindRoles 的 RMW 串行化(internal/logic/role/bindRolePermsLogic.go、internal/logic/user/bindRolesLogic.go)
修复目标:"existing 读 → diff → delete+insert" 整段必须收敛进事务;事务首步分别以 LockByIdTx(roleId) 与 FindOneForUpdateTx(memberId) 锁住 RMW 目标行。两个 admin 并发完全覆盖时只允许"A 完整覆盖 → B 基于 A 的最终态覆盖"交错,消除第三态([1,3,4] 这类混合结果)。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1024 |
BindRolePerms 事务首步调用 LockByIdTx 锁 sys_role 行,再走 FindPermIdsByRoleIdTx 读 diff 基准 |
gomock 记录调用顺序 |
TransactCtx → LockByIdTx → FindPermIdsByRoleIdTx → DeleteByRolePermTx → BatchInsertWithTx |
并发/契约 |
P0 |
M-R10-2:bindRolePermsLogic_mock_test.go 已更新 |
| TC-1025 |
BindRolePerms post-commit cache 清理失败仍 Success |
cache Clean 返 error |
响应 Success;事务内 mock 顺序保持 M-R10-2 |
并发/契约 |
P0 |
postCommitCacheDegraded_audit_test.go 已按 M-R10-2 全量重写 mock |
| TC-1026 |
BindRoles 事务首步 FindOneForUpdateTx(memberId) 锁 sys_product_member 行,再走 FindRoleIdsByUserIdForProductTx |
gomock 记录调用顺序 |
TransactCtx → FindOneForUpdateTx → FindRoleIdsByUserIdForProductTx → DeleteByUserIdAndRoleIdsTx → BatchInsertWithTx |
并发/契约 |
P0 |
M-R10-2:bindRolesLogic_mock_test.go 已更新 |
备注:真实 MySQL 下的"两 admin 并发覆盖 → 串行化 last-write-wins"由 InnoDB SELECT ... FOR UPDATE 语义保证,不再重复单测;契约侧由 mock 调用顺序闭环。
8.3 M-R10-3 LoadCallerAssignableLevel 把批量分配 N 次 DB 读压缩为 1 次(internal/logic/auth/access.go、internal/logic/user/bindRolesLogic.go)
修复目标:caller 在一次请求内不变 → loadFreshMinPermsLevel 结果也不变;新增 AssignableLevelSnapshot + LoadCallerAssignableLevel + CheckRoleLevelAgainst API,在 BindRoles 遍历 roles 前打 1 次 DB,循环内做常数时间比较。同时缩小"超管在 loop 中途降级 caller"的 TOCTOU 窗口(原本每次 loop 都重读 → N 个窗口 → 现在 1 个窗口)。
对应测试文件:internal/logic/auth/loadCallerAssignableLevel_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1017 |
SuperAdmin / ADMIN / DEVELOPER 走 HasFullPerms 短路 |
3 条子用例分别构造不同 caller |
HasFullPerms=true;FindMinPermsLevelByUserIdAndProductCode 不得被调用(gomock 无 EXPECT 命中即 fail) |
性能/契约 |
P0 |
M-R10-3:全权调用者零 DB 成本 |
| TC-1018 |
MEMBER caller 仅打 1 次 DB,循环内对 5 个角色走 CheckRoleLevelAgainst 不再打 DB |
mock FindMin Times(1);本地用 5 个 role level 做比较 |
Times(1) 断言命中;同级/更高级角色拒 403;严格低级角色通过 |
性能/安全 |
P0 |
M-R10-3:Times(1) 是核心断言,一旦循环内误打 DB 会命中"unexpected call" |
| TC-1019 |
caller ErrNotFound → NoRole=true,不翻 500 |
mock 返 sqlx.ErrNotFound |
snap.NoRole=true;CheckRoleLevelAgainst(999) 仍 403 "没有可分配的角色等级" |
契约 |
P1 |
M-R10-3:与 loadFreshMinPermsLevel 的口径对称,保留 ErrNotFound → 最低级 的原契约 |
| TC-1020 |
caller 其他 DB 错误 → fail-close 500 |
mock 返通用 error |
CodeError.Code()==500 |
安全 |
P0 |
M-R10-3:DB 抖动不得被同化为"无角色 → 最低级"放行,保持与 L-4 修复一致 |
8.4 M-R10-4 ChangePassword 把 ErrUpdateConflict 显式映射 409(internal/logic/auth/changePasswordLogic.go)
修复目标:与 UpdateUserLogic.UpdateProfile / UpdateUserStatusLogic.UpdateStatus / UpdateRoleLogic.UpdateWithOptLock 口径对齐,底层 userModel.ErrUpdateConflict 显式回 409 + "密码已被其他会话修改,请刷新后重试";修复前 raw error 被 rest 框架兜成 500,前端会把"并发冲突"误判为系统故障,告警看板也会把这类事件归到 5xx 噪声池。
对应测试文件:internal/logic/auth/changePasswordConflict_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1015 |
UpdatePassword 返回 ErrUpdateConflict 时必须回 409 |
mock UpdatePassword → ErrUpdateConflict |
CodeError.Code()==409;文案含 "密码已被其他会话修改" |
契约 |
P0 |
M-R10-4:主路径断言 |
| TC-1016 |
非 ErrUpdateConflict 的 raw error 仍需透传(由 rest 兜 500) |
mock UpdatePassword → errors.New("driver: bad connection") |
errors.Is(err, genericErr)==true;不是 CodeError(不得被误吞为 409) |
反向契约 |
P0 |
M-R10-4:防止修复把所有错误都误包为 409 |
8.5 M-R10-5 登录 / 权限设置对目标 member 的重复查询去重(internal/logic/pub/loginService.go、internal/logic/auth/access.go、internal/logic/user/setUserPermsLogic.go)
修复目标:① loginService 不再额外打 FindOneByProductCodeUserId,改由 UD.MemberType=="" 判定"非成员或已禁用成员"统一走同一分支;② checkPermLevel 新增 WithMemberSink option,把已查到的 targetMember 写回调用方,让 SetUserPerms 避免对同一 (productCode, userId) 的二次 FindOne。同时把"非成员"与"成员已禁用"的对外文案合并为"您不是该产品的有效成员"——关闭一条枚举 oracle。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1027(重写) |
TestLogin_NonMemberWithProductCode |
用户在 productCode 下非成员 |
CodeError.Code()==403;文案 "您不是该产品的有效成员" |
安全/Oracle |
P0 |
M-R10-5:与"禁用成员"同文案 |
| TC-1028(重写) |
TestLogin_DisabledMemberRejected |
用户成员资格 Status=Disabled |
同上 |
安全/Oracle |
P0 |
M-R10-5:两条分支合并成一条路径 |
8.6 L-R10-1 CreateProduct 必填 AdminDeptId(internal/logic/product/createProductLogic.go、internal/types/types.go、perm.api)
修复目标:初始 admin 账号现在必须挂载到一个 Status=Enabled 的真实部门;否则(原 DeptId=0 方案)admin 首次登录后除了改密外什么都做不了(无法 AddMember / CreateUser / 自改部门)。Bundle 契约:CreateProductReq.AdminDeptId 为必填;Logic 入口 FindOne(AdminDeptId) + Status != Enabled → 400。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1029(新增 helper) |
seedAdminDept(t, ctx, svcCtx) 集中化 |
单次调用插一条启用部门 + t.Cleanup |
返回 deptId;测试结束自动清理 |
基础 |
— |
L-R10-1:internal/logic/product/helper_test.go |
| TC-1030(适配) |
所有 createProductLogic_test.go 的正向用例 |
入参带 AdminDeptId=seedAdminDept(...) |
行为不变 |
适配 |
P0 |
L-R10-1:契约变更全量回归 |
| TC-1031(适配) |
fetchInitialCredentialsLogic_audit_test.go |
同上 |
行为不变 |
适配 |
P0 |
L-R10-1:票据消费路径不破坏 |
| TC-1032(适配) |
createProductCompensation_audit_test.go |
同上,补偿路径 |
行为不变 |
适配 |
P0 |
L-R10-1:Redis 降级后的补偿链保留 |
| TC-1033(适配) |
createProductConflict_audit_test.go |
mock SysDeptModel.FindOne 返 Status=1 |
行为不变;AdminDeptId 透传 |
适配 |
P0 |
L-R10-1:mock 侧补齐 |
| TC-1034(适配) |
createProductLogic_mock_test.go |
同上,两处 CreateProductReq |
行为不变 |
适配 |
P0 |
L-R10-1:mock 侧补齐 |
8.7 L-R10-2 CreateProduct 初始 adminPassword 升级为强密码(internal/logic/product/createProductLogic.go)
修复目标:generateRandomHex(12)(24 字节 0-9a-f,仅小写+数字)被替换为 generateStrongInitialPassword(16)(大小写 + 数字 + 符号),长度恒为 16,并在构造时通过 util.ValidatePassword 断言——为未来可能上线的"存量密码强度合规审查"留口径。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1035(重写) |
TestFetchInitialCredentials_HappyPath |
读取一次性票据中的 AdminPassword |
len(cred.AdminPassword)==16(不是旧的 24) |
契约 |
P1 |
L-R10-2:长度断言回归 |
8.8 L-R10-10 gRPC GetUserPerms 合并"userId 不存在"与"非成员"枚举 oracle(internal/server/permserver.go)
修复目标:ud.Username == ""(用户不存在)与 ud.MemberType == ""(非成员/禁用成员)合并为同一 codes.NotFound + "用户不是该产品的有效成员",关闭"持合法 appKey 遍历 userId 区分全局 sys_user 存在性"的枚举旁路。对上游调用方(受信的产品服务端)影响面极小,但修掉之后多租户语义也更干净。
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1036(重写) |
TestGetUserPerms_UserNotFound |
userId 全局不存在 |
status.Code()==NotFound + "用户不是该产品的有效成员" |
安全/Oracle |
P0 |
L-R10-10:与"非成员"同响应 |
| TC-1037(重写) |
TestGetUserPerms_NonMember_PermissionDenied |
userId 存在但非成员 |
同上(status 码由 PermissionDenied 改为 NotFound) |
安全/Oracle |
P0 |
L-R10-10:与"userId 不存在"同响应 |
| TC-1038(重写) |
TestGetUserPerms_DisabledMemberInDevDept_PermissionDenied |
成员 Status=Disabled |
同上 |
安全/Oracle |
P0 |
L-R10-10:禁用成员走 loadMembership 清空 MemberType → 同路径 |
8.9 Round 10 既有测试兼容性调整(本轮代码变更触发)
| 用例 |
文件 |
调整说明 |
syncPerms* 四个 audit 文件 |
internal/logic/pub/syncPerms404_audit_test.go、syncPermsDedup_audit_test.go、syncPermsLogic_mock_test.go、syncPermsTxLock_audit_test.go |
M-R10-1:LockByCodeTx mock 必须返 SysProduct{Status: 1},否则事务内 Status 复核直接 403 让下游 EXPECT 全部不生效 |
syncPermissions404_audit_test.go(gRPC 入口) |
internal/server/syncPermissions404_audit_test.go |
同上;保证"UnmappedCode 仍走 Internal"的原契约依然可验 |
bindRolePermsLogic_mock_test.go |
internal/logic/role/ |
M-R10-2:事务内以 LockByIdTx 锁 sys_role 行;existing 读改为 FindPermIdsByRoleIdTx |
bindRolesLogic_mock_test.go |
internal/logic/user/ |
M-R10-2:事务首步 FindOneForUpdateTx(memberId);existing 读改为 FindRoleIdsByUserIdForProductTx |
postCommitCacheDegraded_audit_test.go |
internal/logic/role/ |
M-R10-2:整段 mock 顺序重写为 TransactCtx → LockByIdTx → FindPermIdsByRoleIdTx → … |
permserver_test.go |
internal/server/ |
L-R10-10:3 条既有 TC 的 status code 与文案统一收敛;M-R10-5:1 条 Login 分支文案统一 |
loginLogic_test.go |
internal/logic/pub/ |
M-R10-5:非成员 / 禁用成员两条分支合并为同一错误消息 |
8.10 Round 10 未进入测试的条目(按审计意见归档)
| 审计条目 |
归档结论 |
L-R10-3 IncrementTokenVersion 非 CAS 路径 (0, nil) 静默返回 |
本轮未改代码;Logout 仅把返回值作为日志副作用,审计意见是"面向未来的返回语义收敛",不入 TC |
L-R10-4 RefreshToken newVersion != predictedVersion 死码 |
审计定档"保留(defence in depth)";不入 TC |
L-R10-5 DeptTree 对普通成员暴露 Status |
审计定档"业务需求决定是否收敛";本仓库未变更;不入 TC |
L-R10-6 CountOtherActiveAdminsTx 用 SELECT id FOR UPDATE + len(ids) |
审计定档"可忽略代码风格优化";本仓库未变更;不入 TC |
| L-R10-7 PermList / RoleList 对同产品成员完整可见 |
审计定档"业务默认契约";不入 TC |
L-R10-8 loadPerms 对全权分支忽略 sys_user_perm DENY |
审计定档"等 DeptType 动态性讨论后统一处理";不入 TC |
L-R10-9 ExtractClientIP XFF 信任边界 |
审计定档"运维反代层硬约束即可",代码侧无变更;不入 TC |
8.11 Round 10 新增 TC 汇总
| 审计条目 |
文件 |
TC 编号区间 |
数量 |
状态 |
| M-R10-1 |
syncPerms*(4 文件) / syncPermissions404_audit_test.go |
TC-1021 ~ TC-1023(适配) |
3 |
✅ |
| M-R10-2 |
bindRolePermsLogic_mock_test.go / bindRolesLogic_mock_test.go / postCommitCacheDegraded_audit_test.go |
TC-1024 ~ TC-1026 |
3 |
✅ |
| M-R10-3 |
loadCallerAssignableLevel_audit_test.go(新增) |
TC-1017 ~ TC-1020 |
4 |
✅ |
| M-R10-4 |
changePasswordConflict_audit_test.go(新增) |
TC-1015 / TC-1016 |
2 |
✅ |
| M-R10-5 |
loginLogic_test.go / permserver_test.go |
TC-1027 / TC-1028 |
2 |
✅ |
| L-R10-1 |
helper_test.go(新增)+ 5 文件适配 |
TC-1029 ~ TC-1034 |
6 |
✅ |
| L-R10-2 |
fetchInitialCredentialsLogic_audit_test.go |
TC-1035 |
1 |
✅ |
| L-R10-10 |
permserver_test.go |
TC-1036 ~ TC-1038 |
3 |
✅ |
| 合计 |
— |
TC-1015 ~ TC-1038(跳号:TC-1015/1016 在 M-R10-4;TC-1017 ~ 1020 在 M-R10-3;TC-1021 起按文件模块编号) |
24 |
24 ✅ |
九、Round 11 审计驱动新增用例 (TC-1039 ~ TC-1071)
本轮审计聚焦 8 个修复点:H-R11-1(UpdatePassword TOCTOU)、M-R11-1(gRPC SyncPermissions / GetUserPerms 入口限流)、M-R11-2(UpdateStatus / IncrementTokenVersion 停用内部 FindOne)、M-R11-3(UpdateUser deptId 切换 vs DeleteDept 的 write skew)、L-R11-1(UpdateMember memberType/status 指针语义)、L-R11-2(Delete 族 SELECT 只取 cache key 列)、L-R11-4(SyncPerms 纯新增不触发 CleanByProduct)、L-R11-5(RotateRefreshToken helper 跨 HTTP/gRPC 共享)。详细修复背景见 audit-report.md。
9.1 H-R11-1 · UpdatePassword 把 expectedUpdateTime 改由调用方显式传入(TC-1039 ~ TC-1043)
修复目标:旧实现 UpdatePassword 在内部自己 FindOne 拿 updateTime 再做 CAS,导致"外层校验旧密码的时间戳" 与"内层 UPDATE 比对的时间戳" 不是同一个快照,并发改密退化为 last-write-wins,一个持有旧密码的会话可以把别人紧急改过的新密码覆盖回去。新签名:UpdatePassword(ctx, id, username, password, mustChangePassword, expectedUpdateTime);调用方 ChangePasswordLogic 显式把"外层 FindOne 拿到的 user.UpdateTime" 透传进来,并把 ErrUpdateConflict 显式映射 409(与 M-R10-4 同口径)。
对应测试文件:
internal/model/user/updatePasswordToctou_audit_test.go(新增)
internal/logic/auth/changePasswordToctou_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1039 |
Model 层正向:expectedUpdateTime 与 DB 一致 → 成功 + tokenVersion+1 + updateTime 前进 |
直接调 UpdatePassword(id, username, newHash, MustChangePasswordNo, existing.UpdateTime) |
err==nil;再 FindOne → password/updateTime/tokenVersion 全部按预期前进 |
契约 |
P0 |
H-R11-1:happy path 钉死新签名语义 |
| TC-1040 |
Model 层 TOCTOU:调用方持有"陈旧的 expectedUpdateTime" |
Session A 持 updateTime=T0;Session B 先成功改密 → DB updateTime=T1;Session A 再用 T0 调 UpdatePassword |
errors.Is(err, ErrUpdateConflict);DB password/updateTime 仍然是 B 的结果,未被回写 |
并发/数据完整性 |
P0 |
H-R11-1:核心反回归——若回退到"内部自 FindOne",这里会误成功 |
| TC-1041 |
Model 层并发:同一 expectedUpdateTime 两 goroutine 并行 CAS |
2 个 goroutine 共享 T0,并发 UpdatePassword |
恰好 1 个成功、1 个 ErrUpdateConflict;DB 最终密码 = 赢者的密码;tokenVersion 只 +1 不是 +2 |
并发/契约 |
P0 |
H-R11-1:并发单胜者;tokenVersion 被累计两次会立即暴露退化 |
| TC-1042 |
Logic 层 E2E:同一 user 连续两次用 "同一旧密码 P0" 发起 ChangePassword,第二次必须 400 "旧密码错误" |
第一次 P0→P1(200),第二次仍送 oldPass=P0 |
第二次 CodeError.Code()==400,msg 含 "旧密码错误";不得成 409(否则 400/409 语义混淆) |
边界 |
P0 |
H-R11-1:400/409 分桶契约 |
| TC-1043 |
Logic 层 mock:ChangePassword 必须把"外层 FindOne 拿到的 user.UpdateTime" 原封不动透传给 Model 层 |
mock UpdatePassword(id, username, _, MustChangePasswordNo, snapshotUpdateTime),断言第 5 个实参 |
mock EXPECT 命中;若回退为"Model 内部再读 updateTime",这里会拿到零值触发失败 |
契约 |
P0 |
H-R11-1:CAS 快照来源契约 |
9.2 M-R11-1 · gRPC SyncPermissions / GetUserPerms 入口限流(TC-1051 ~ TC-1054)
修复目标:PermServer.SyncPermissions / PermServer.GetUserPerms 此前完全无入口限流,bcrypt.Compare(appSecret) 的 CPU 成本 + LockByCodeTx 的事务级 X 锁都在校验路径里,一条合法 appKey 爆破足以把整个进程 CPU 或 MySQL 连接池打穿;新增 GrpcSyncLimiter / GrpcGetUserPermsLimiter(均基于 limit.PeriodLimit),分别以 appKey 桶和 appKey + 源 IP 双桶限速;OverQuota 显式回 codes.ResourceExhausted + "请求过于频繁,请稍后再试"。关键防御:空 AppKey 走 if req.AppKey != "" { Take(...) } 前置分支,不消耗 limiter 配额,避免恶意方用空串把 limiter 的 keyspace 污染成"永不过期的空串桶"。
对应测试文件:internal/server/grpc_rate_limit_mr11_1_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1051 |
SyncPermissions 同 appKey 连打 quota+1 次触发 ResourceExhausted |
GrpcSyncLimiter = NewPeriodLimit(60, 1, rds, uniqPrefix);同一 appKey 连续 2 次调用 |
第 1 次走业务层(Unauthenticated,因 appKey 不真实);第 2 次 codes.ResourceExhausted |
安全/限流 |
P0 |
M-R11-1:appKey 桶命中上限 |
| TC-1052 |
GetUserPerms 同 appKey 连打 quota+1 次触发 ResourceExhausted |
同上 limiter 套给 GrpcGetUserPermsLimiter;同一 appKey 连续 2 次 |
第 2 次 codes.ResourceExhausted |
安全/限流 |
P0 |
M-R11-1:GetUserPerms 双桶中的 appKey 桶 |
| TC-1053 |
空 AppKey 不消耗 limiter 配额 |
AppKey="" 连打 3 次;然后真实 realKey 首次请求 |
3 次空串请求都 codes.Unauthenticated(走 FindOneByAppKey("") → ErrNotFound),realKey 首次仍命中业务层 codes.Unauthenticated(不是 ResourceExhausted) |
安全/防污染 |
P0 |
M-R11-1:空串前置分支缺失会把 limiter 计数器打到上限 |
| TC-1054 |
GetUserPerms 同一 IP、多个不同 appKey → 命中 IP 桶 |
GrpcGetUserPermsLimiter=NewPeriodLimit(60, 2, ...);同 IP 依次用 appKey "a"/"b"/"c" 发请求 |
第 3 次命中 IP 桶上限 codes.ResourceExhausted(尽管每个 appKey 桶都还有额度) |
安全/限流 |
P0 |
M-R11-1:appKey + IP 双桶叠加 |
9.3 M-R11-2 · UpdateStatus / IncrementTokenVersion 停用内部 FindOne 并显式接收 username(TC-1044 ~ TC-1048)
修复目标:UpdateStatus / IncrementTokenVersion 原本在函数体内 FindOne 取 username 做 cache key 失效;修复后改为调用方把 username 透传进来,Model 层不再打第二次 DB 读。由上游 LogoutLogic / UpdateUserStatusLogic 从中间件 UserDetails 里直接取 Username/UpdateTime 后透传。反向契约:哪怕调用方传入"错误的 username",Model 层也只会清理"这个错误 username" 对应的那个 cache 槽,保证了"model 层绝不再自己 FindOne"。
对应测试文件:
internal/model/user/mr11_2_noInternalFindOne_audit_test.go(新增)
internal/logic/auth/logoutUsernameForward_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1044 |
UpdateStatus 用"错误 username" 调用 → Model 层仍按错误 key 清理 |
真实 username="u1";调 UpdateStatus(id, "WRONG", ...);预置 u1 和 WRONG 两个 cache 槽 |
只有 WRONG 对应的 cache 槽被删;u1 的 cache 槽仍在(证明 Model 层没有自己 FindOne 纠正 username) |
契约/反向 |
P0 |
M-R11-2:内部二次 FindOne 回退一跑即红 |
| TC-1045 |
IncrementTokenVersion 用"错误 username" 调用 → 同上 |
同上 |
同上;DB tokenVersion 正常前进 |
契约/反向 |
P0 |
M-R11-2:IncrementTokenVersion 分支 |
| TC-1046 |
IncrementTokenVersion 对"已被删除的行" 仍正常走 RowsAffected=0 → ErrUpdateConflict 分支 |
先 Insert 再 Delete,随后调 IncrementTokenVersion(deletedId, "anyName", deletedUpdateTime) |
errors.Is(err, ErrUpdateConflict);不触发 panic / nil user 崩溃(证明没有 FindOne 前置) |
契约/边界 |
P0 |
M-R11-2:删后 CAS 的边界 |
| TC-1047 |
Logic 层 Logout 必须把 ud.Username 透传到 Model |
mock IncrementTokenVersion(id, "u1", _),ud.Username="u1" |
mock EXPECT 命中,不得出现任何 FindOne mock 调用 |
契约 |
P0 |
M-R11-2:Logout 口径 |
| TC-1048 |
Logic 层 Logout 即使 ud.Username=="" 也必须透传(空串) |
mock IncrementTokenVersion(id, "", _) |
mock EXPECT 命中,空串仍被透传(Model 层自负其责) |
契约/边界 |
P1 |
M-R11-2:不在 Logic 层搬运内部补全 |
9.4 M-R11-3 · UpdateUser deptId 切换 vs DeleteDept 的 write skew 闭合(TC-1049 ~ TC-1050)
修复目标:之前 UpdateUser 在"切换 deptId 到目标 dept" 前只做外层 FindOne,没拿住"目标 dept 在事务提交瞬间仍然启用"的 S 锁;并发的 DeleteDept 完全可以"你校验时我还在 → 你进事务我已删" → 提交出一个"dept 已删,但 user 的 deptId 还指向它" 的 write skew 残片。修复引入 SysDeptModel.FindOneForShareTx(SELECT ... LOCK IN SHARE MODE),新签名 SysUserModel.UpdateProfileWithTx,业务把"读目标 dept → 复核 status=Enabled → UPDATE user" 整段收敛进一个事务。
对应测试文件:internal/logic/user/updateUserWriteSkew_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1049 |
deptId 切换场景下并发 DeleteDept 被"S 锁 / X 锁"串行化 |
起始 userDeptId=dA;并发 goroutine:A 做 UpdateUser{DeptId:dB},B 做 DeleteDept(dB);多轮 |
总是 2 个分支之一:① A 先进 → B 看到 dB 有成员 → ErrHasUsers;② B 先进 → A 看到 dB.status=Disabled/已删 → ErrBadRequest 或 404。绝不出现 "A 成功 + B 成功 + user.deptId=dB(已删)" 的 skew 残片(直接查 DB 做断言,绕过 cache) |
并发/数据完整性 |
P0 |
M-R11-3:核心反回归 |
| TC-1050 |
非事务路径:deptId 未变的 UpdateUser 不触发 FindOneForShareTx 的 S 锁路径 |
构造"只改 nickname、deptId 不变" 的更新 |
事务只走 UpdateProfileWithTx;SysDeptModel.FindOneForShareTx 未被打到(观察事务 SQL / mock 无 expect) |
契约/性能 |
P1 |
M-R11-3:避免"无切换时也打 S 锁" 导致退化 |
9.5 L-R11-1 · UpdateMember memberType/status 指针语义(TC-1055 ~ TC-1060)
修复目标:UpdateMemberReq.MemberType / UpdateMemberReq.Status 由"空字符串 / 零值 = 不修改" 的隐式约定升级为 *string / *int64,避免"用户把字段显式设成 "" / 0" 被误当成"不更新"。两个字段同时 nil 时 UpdateMemberLogic 直接 ErrBadRequest(400)。
对应测试文件:internal/logic/member/updateMemberPartialPointer_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1055 |
MemberType==nil && Status==nil → 400 "请至少提供一个要更新的字段" |
UpdateMemberReq{productCode, userId} 两字段都不传 |
CodeError.Code()==400,msg 含 "至少提供一个要更新的字段" |
契约 |
P0 |
L-R11-1:nil 判定入口 |
| TC-1056 |
只传 Status,MemberType 保持不变 |
{Status: Int64Ptr(2)},原 member.MemberType="ADMIN" |
DB:memberType 仍是 "ADMIN",status=2 |
契约 |
P0 |
L-R11-1:部分更新语义 |
| TC-1057 |
只传 MemberType,Status 保持不变 |
{MemberType: StrPtr("DEVELOPER")},原 member.Status=1 |
DB:memberType 变为 "DEVELOPER",status 仍是 1 |
契约 |
P0 |
L-R11-1:镜像对称 |
| TC-1058 |
DEVELOPER → 只改 Status 时跳过"分配校验" |
只传 Status=1,member.MemberType="DEVELOPER" |
不走分配校验分支;memberType 保持 DEVELOPER;状态落盘为 1 |
契约/性能 |
P1 |
L-R11-1:DEVELOPER 分支被误挂会立即红 |
| TC-1059 |
非法 Status 值(例如 7)→ 400 |
{Status: Int64Ptr(7)} |
CodeError.Code()==400 |
边界 |
P0 |
L-R11-1:Status 枚举防御 |
| TC-1060 |
完全 no-op(传进来的值与 DB 现值相同)→ 返 nil 且 updateTime 不前进 |
传 {Status: Int64Ptr(member.Status)} |
err==nil;DB updateTime 保持原值 |
契约/幂等 |
P1 |
L-R11-1:MySQL 行为——值未变 RowsAffected=0,不被误升格为冲突 |
9.6 L-R11-2 · Delete 族 SELECT 只取 cache key 列(TC-1061 ~ TC-1062)
修复目标:DeleteByRoleIdTx / DeleteByUserIdAndRoleIdsTx / DeleteByUserIdForProductTx / DisableNotInCodesWithTx 在"先读后删" 模式里以前用 SELECT * 吸满全行,但只用到 id + cache key 所需两三列;改为只 SELECT id, userId, roleId 等(按表而定),节省 MySQL → Go 内存拷贝、降低 net bytes,同时不改变缓存语义。要求所有"id 级 + 组合 key"cache 槽都被删除。
对应测试文件:internal/model/userrole/deleteCacheKey_r11_2_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1061 |
DeleteByRoleIdTx 删除多行后,id 级和组合 key 缓存全部失效 |
预置 3 条 sys_user_role(roleId=R),先 FindOne / 组合 key FindOne 把所有 cache 槽填热 |
删除后 FindOne(id) 均返 ErrNotFound;FindOneByUserIdRoleId(userId, R) 也均返 ErrNotFound;无脏读 |
契约/缓存 |
P0 |
L-R11-2:两套 key 都得失效 |
| TC-1062 |
DeleteByUserIdAndRoleIdsTx 只删指定 (userId, roleIds) 集合后同上 |
预置多条;删除部分;剩余部分保留 |
被删的全返 ErrNotFound;未被删的 FindOne 正常命中;两层 cache key 对齐 |
契约/缓存 |
P0 |
L-R11-2:组合 key 的定点失效不误伤 |
9.7 L-R11-4 · SyncPerms 纯新增时不触发 CleanByProduct(TC-1063 ~ TC-1065)
修复目标:ExecuteSyncPerms 以前只要事务提交就无条件调 UserDetailsLoader.CleanByProduct(productCode),哪怕这次同步只新增权限声明——新增对"既有 user 的可用权限集合"没有任何副作用,却会触发全产品用户的 perms cache 级联失效,对大产品是一次可观测抖动。修复为:仅当 updated > 0 || disabled > 0 才调 CleanByProduct。
对应测试文件:internal/logic/pub/syncPermsCleanByProduct_r11_4_audit_test.go(新增)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1063 |
纯新增(updated=0, disabled=0)→ 不触发 CleanByProduct |
预先在 Redis 设置 productIndexKey canary;执行 ExecuteSyncPerms(perms=全新 codes) |
canary 仍在 Redis(未被 CleanByProduct 删除);added>0 |
契约/性能 |
P0 |
L-R11-4:主反回归 |
| TC-1064 |
至少一条 update(code 存在但 name/Status/Sort 变更)→ 必须触发 CleanByProduct |
预置 canary + 一条已有 perm;然后 sync 带同 code 但改名 |
canary 被删除(CleanByProduct 触达);updated>0 |
契约 |
P0 |
L-R11-4:update 路径 |
| TC-1065 |
至少一条 disable(列表里不含的 perm 被置 Disabled)→ 必须触发 CleanByProduct |
同上但 sync 不传原 code;旧 perm 被禁用 |
canary 被删除;disabled>0 |
契约 |
P0 |
L-R11-4:disable 路径 |
9.8 L-R11-5 · RotateRefreshToken helper 在 HTTP/gRPC 共享(TC-1066 ~ TC-1071)
修复目标:internal/logic/pub/refreshTokenLogic.go(HTTP)与 internal/server/permserver.go 的 gRPC RefreshToken 之前各自实现 "try sign → CAS → Clean → forensic compare",一旦两端漂移就会出现"HTTP 签出的 refreshToken 在 gRPC 校验被拒" 这种隐蔽的跨协议断层。修复抽出 authHelper.RotateRefreshToken(ctx, svcCtx, claims, ud) 作为唯一事实源;HTTP/gRPC 都调同一个 helper,错误分型由 helper 负责区分 ErrTokenVersionMismatch 与其他内部错误,由各自入口映射 401 / codes.Unauthenticated。
对应测试文件:
internal/logic/auth/rotateRefreshToken_r11_5_audit_test.go(新增,helper 直测)
internal/server/grpcHttpRotateInterop_r11_5_audit_test.go(新增,跨协议互认)
| TC 编号 |
测试场景 |
输入 |
预期结果 |
类型 |
优先级 |
覆盖说明 |
| TC-1066 |
helper happy path:返回新 access+refresh、DB tokenVersion +1、user cache Clean |
合法 claims + ud + 真实 DB |
tokens.AccessToken != "";DB tokenVersion 前进 1;UserDetailsLoader.Load(id) 必须重新打 DB(cache 已 Clean) |
契约 |
P0 |
L-R11-5:核心正向 |
| TC-1067 |
helper:claims.TokenVersion 与 DB 不一致 → ErrTokenVersionMismatch |
claims.TokenVersion=0,DB=5 |
errors.Is(err, userModel.ErrTokenVersionMismatch);DB tokenVersion 不变;tokens 零值 |
安全 |
P0 |
L-R11-5:CAS mismatch 不升版 |
| TC-1068 |
helper:用户已删除 → ErrTokenVersionMismatch |
先 Insert 再 Delete;claims 携带该 id |
同上 error;不 panic |
边界 |
P0 |
L-R11-5:删后 CAS |
| TC-1069 |
跨协议互认:HTTP 签出的 refreshToken 能被 gRPC RefreshToken 无缝续签 |
HTTP 首刷成功(v0→v1,拿到 newRt1);把 newRt1 直接灌到 gRPC |
gRPC 返 NoError;新 tokens 非空;DB tokenVersion 再 +1(v1→v2) |
契约/集成 |
P0 |
L-R11-5:核心反漂移 |
| TC-1070 |
跨协议互认:gRPC 签出的 refreshToken 能被 HTTP RefreshToken 无缝续签 |
gRPC 先刷(v0→v1);把新 rt 灌到 HTTP |
HTTP err==nil,DB 前进(v1→v2) |
契约/集成 |
P0 |
L-R11-5:对称镜像 |
| TC-1071 |
gRPC 重放:旧 rtV0 已被用过一次,再发给 gRPC 必须 Unauthenticated(而非 Internal) |
gRPC 首刷成功;再用同一个 rtV0 调 gRPC |
status.Code()==codes.Unauthenticated;Msg 含 "登录状态已失效" |
安全/错误映射 |
P0 |
L-R11-5:ErrTokenVersionMismatch 的协议映射 |
9.9 Round 11 新增 TC 汇总
| 审计条目 |
文件 |
TC 编号区间 |
数量 |
状态 |
| H-R11-1 |
updatePasswordToctou_audit_test.go(新增) / changePasswordToctou_audit_test.go(新增) |
TC-1039 ~ TC-1043 |
5 |
✅ |
| M-R11-2 |
mr11_2_noInternalFindOne_audit_test.go(新增) / logoutUsernameForward_audit_test.go(新增) |
TC-1044 ~ TC-1048 |
5 |
✅ |
| M-R11-3 |
updateUserWriteSkew_audit_test.go(新增) |
TC-1049 ~ TC-1050 |
2 |
✅ |
| M-R11-1 |
grpc_rate_limit_mr11_1_audit_test.go(新增) |
TC-1051 ~ TC-1054 |
4 |
✅ |
| L-R11-1 |
updateMemberPartialPointer_audit_test.go(新增) |
TC-1055 ~ TC-1060 |
6 |
✅ |
| L-R11-2 |
deleteCacheKey_r11_2_audit_test.go(新增) |
TC-1061 ~ TC-1062 |
2 |
✅ |
| L-R11-4 |
syncPermsCleanByProduct_r11_4_audit_test.go(新增) |
TC-1063 ~ TC-1065 |
3 |
✅ |
| L-R11-5 |
rotateRefreshToken_r11_5_audit_test.go(新增) / grpcHttpRotateInterop_r11_5_audit_test.go(新增) |
TC-1066 ~ TC-1071 |
6 |
✅ |
| 合计 |
— |
TC-1039 ~ TC-1071 |
33 |
33 ✅ |
9.10 Round 11 既有测试兼容性适配(本轮签名变更触发)
| 适配项 |
文件 |
调整说明 |
SysUserModel.UpdatePassword / UpdateStatus / IncrementTokenVersion 签名新增 username、部分路径新增 expectedUpdateTime |
internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go、internal/model/user/incrementTokenVersion_audit_test.go、internal/logic/user/updateUserStatusOptLock_audit_test.go、internal/logic/auth/changePasswordConflict_audit_test.go |
入参全部补齐 user.Username / user.UpdateTime;原"用户不存在" 的 ErrNotFound 期望改为 ErrUpdateConflict(现行实现用 RowsAffected=0 统一升格) |
mocks.MockSysUserModel |
internal/testutil/mocks/mock_user_model.go |
mockgen 重新生成,方法签名与新接口对齐(多出 username / expectedUpdateTime) |
mocks.MockSysDeptModel 新增 FindOneForShareTx |
internal/testutil/mocks/mock_dept_model.go |
mockgen 重新生成;M-R11-3 需要的 S 锁 API mock 齐备 |
types.UpdateMemberReq.MemberType / Status 改 *string / *int64 |
internal/logic/member/auditFixes_test.go、internal/logic/member/updateMemberLogic_test.go |
引入 strPtr / int64Ptr 本地 helper,3 处调用全部改传指针 |