测试范围: API (go-zero REST, 全 POST) + gRPC (status codes) + Model 层 (_gen.go 模板生成 + 自定义方法) + Logic 单元测试 + util 层 + 访问控制 + UserDetailsLoader 测试报告与代码审计详见 test-report.md
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
共 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 | 手写 |
输入: 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
注意: 所有路由统一为 POST 方法,请求参数均通过 JSON Body 传递。
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 | loginService |
| TC-0014 | POST /api/auth/login | 产品被禁用时拒绝登录 | product.status=Disabled | 403 "该产品已被禁用" | 安全 | P0 | loginService |
| TC-0751 | POST /api/auth/login | 用户名不存在 + 任意密码 | username="__no_such_user__" | 返回与"存在用户但密码错"完全一致的错误文案("用户名或密码错误"),仍会走 dummy bcrypt 耗时 | 安全/枚举 | P0 | 用 dummy hash 比对,防止通过响应差异枚举用户名 |
| TC-0752 | POST /api/auth/login | 用户名存在但密码错 | 存在用户 + 错误密码 | 同 TC-0751 相同 code、相同文案 | 安全/对照 | P0 | 与 TC-0751 联动,断言两条分支对外表现一致 |
| TC-0753 | POST /api/auth/login | 登录限流 key 必须基于 ip:username |
同 IP 攻击 userA 耗尽配额 | userA 被限流 429,但同 IP 登录 userB 仍放行 | 安全/限流 | P0 | UsernameLoginLimit key = ip:username,避免单 IP 单用户暴力破解拖垮同一 IP 其他用户登录 |
| TC-0838 | POST /api/auth/login | 用户冻结 + 错误密码 | status=2 的用户,密码错误 | 401 "用户名或密码错误"(不能是 403 "账号已被冻结") | 安全/侧信道 | P0 | 账号存在性 + 冻结状态必须在密码正确之前完全不可观察 |
| TC-0839 | POST /api/auth/login | 用户冻结 + 正确密码 | status=2 的用户,密码正确 | 403 "账号已被冻结"(奖励性披露) | 正常 | P0 | 只有拿到密码后才披露状态,仍保留业务可见性 |
| TC-0840 | POST /api/auth/login | 超管走产品端登录 + 错误密码 | IsSuperAdmin=1,密码错误 | 401 "用户名或密码错误"(不得提前暴露"超管"身份) | 安全/侧信道 | P0 | 超管状态同样延迟披露 |
| TC-0841 | POST /api/auth/login | 超管走产品端登录 + 正确密码 | IsSuperAdmin=1,密码正确 | 403 "超级管理员不允许通过产品端登录,请使用管理后台" | 正常 | P0 | 披露顺序反转后的正面路径仍保持原有业务语义 |
| TC-0842 | POST /api/auth/login | 用户名不存在 | 不存在用户名 + 任意密码 | 401 "用户名或密码错误",走 dummy bcrypt 恒时对齐 | 安全/枚举 | P0 | 沿用 dummy hash 路径,不被 H-2 新顺序破坏 |
| TC-1213 | POST /api/auth/login | cap.js 未启用 + 验证码为空 | {"username":"u","password":"p","productCode":"pc","captchaId":"","captchaCode":""} |
400 "验证码不能为空" | 输入校验 | P0 | Capjs.Enable=0 时强制验证码校验 |
| TC-1214 | POST /api/auth/login | cap.js 未启用 + 验证码错误/过期 | {"captchaId":"non_existent","captchaCode":"0000"} |
400 "验证码错误或已过期" | 异常路径 | P0 | VerifyCaptcha 失败分支 |
| TC-1215 | POST /api/auth/login | cap.js 未启用 + 验证码正确 → 正常登录 | 预设验证码 + 有效凭证 | 200 + accessToken/refreshToken | 正常路径 | P0 | 验证码通过后走完整登录流程 |
| TC-1250 | POST /api/auth/login | cap.js 已启用时传统接口被拒绝 | Capjs.Enable=1,任意凭证 | 400 "当前已启用人机验证,请使用人机验证登录" | 互斥门控 | P0 | Enable=1 时传统登录接口必须拒绝,强制走 /auth/login/cap |
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 | 统一错误消息防用户枚举 |
| 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 | 冻结/非超管统一返回同一错误防枚举 |
| 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 | 防用户名枚举爆破 |
| TC-0710 | POST /api/auth/adminLogin | 同 IP,产品登录用尽配额后管后登录仍放行 | productLoginRL 配额=1 打满,再打 adminLoginRL | adminLoginRL 正常放行 1 次 | 安全 | P0 | keyPrefix 区分 product/admin |
| TC-0781 | POST /api/auth/adminLogin | 不存在用户登录管理后台响应时间恒定 | 不存在的用户名 | code=401, "用户名或密码错误",执行 dummy bcrypt | 安全 | P0 | dummy bcrypt 恒时对齐 |
| TC-0834 | POST /api/auth/adminLogin | 相同 IP + 相同 username 连续打满 quota | clientIP=1.2.3.4, username=superA,连打 >quota 次错误密码 |
超限后返回 429 "登录尝试过于频繁,请5分钟后再试",同 IP 下同 username 不再放行 | 安全/限流 | P0 | 修复后 key=admin:<ip>:<u>;按 quota 阈值打入,需命中 429 |
| TC-0835 | POST /api/auth/adminLogin | 同 username 但换远端 IP | clientIP=1.2.3.4 打满后,换 clientIP=5.6.7.8 继续 |
不同 IP 分别计数,换 IP 仍应触达 bcrypt → 进入下游业务断言(此处为密码错误 401),而非继承上一桶 429 | 安全/限流 | P0 | 确认 key 含 IP 维度,远端"任何 IP"都能永久锁死的攻击路径被阻断 |
| TC-0836 | POST /api/auth/adminLogin | clientIP 缺失(未挂 RateLimit 中间件) | clientIP 未注入 ctx | key 退化为 admin:unknown:<u>,仍能正常限流到共享桶,不得直接 panic 或跳过限流 |
安全/鲁棒 | P0 | fail-closed 兜底;未来删除中间件只会退化 key 不会绕过 |
| TC-0837 | POST /api/auth/adminLogin | managementKey 无效路径不得消耗 username 配额 | 任意密码,managementKey 错误 | 401 "managementKey无效";同 username 立刻换 managementKey 正确再来仍有完整 quota | 安全 | P1 | managementKey 校验在限流 Take 之前,防匿名攻击者只靠错 key 就把配额打满 |
| TC-1008 | POST /api/auth/adminLogin | 非超管+错密码 vs 用户不存在 | 错密码 / 不存在用户名 | code + body 完全一致 | 安全/Oracle | P0 | 响应不得区分两条分支 |
| TC-1009 | POST /api/auth/adminLogin | 非超管+正确密码 | 真实普通用户正确密码 | 仍 401 "用户名或密码错误" | 安全 | P0 | 不得以 200 暴露账号存在性 |
| TC-1010 | POST /api/auth/adminLogin | 两条 dummy bcrypt 分支时序 | 连续 3 次平均耗时 | 非超管+错密 与 不存在 耗时比 <3× |
时序/性能 | P0 | 若比例 >3× 说明 L-N3 被回退(非超管分支跳过了 dummy bcrypt) |
| TC-1216 | POST /api/auth/adminLogin | cap.js 未启用 + 验证码为空 | {"username":"u","password":"p","managementKey":"valid","captchaId":"","captchaCode":""} |
400 "验证码不能为空" | 输入校验 | P0 | Capjs.Enable=0 时强制验证码校验 |
| TC-1217 | POST /api/auth/adminLogin | cap.js 未启用 + 验证码错误/过期 | {"captchaId":"non_existent","captchaCode":"0000","managementKey":"valid"} |
400 "验证码错误或已过期" | 异常路径 | P0 | VerifyCaptcha 失败分支 |
| TC-1218 | POST /api/auth/adminLogin | cap.js 未启用 + 验证码正确 → 超管正常登录 | 预设验证码 + 超管凭证 + managementKey | 200 + accessToken/refreshToken | 正常路径 | P0 | 验证码通过后走完整管理端登录流程 |
| TC-1251 | POST /api/auth/adminLogin | cap.js 已启用时传统接口被拒绝 | Capjs.Enable=1,任意凭证 + managementKey | 400 "当前已启用人机验证,请使用人机验证登录" | 互斥门控 | P0 | Enable=1 时传统管理端登录接口必须拒绝,强制走 /auth/adminLogin/cap |
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 | 禁止跨产品切换 |
| 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 |
| TC-0720 | POST /api/auth/refreshToken 或 logout | 正常登出 | 已登录用户 ctx + WithUserDetails | 用户 tokenVersion=0 → 1;重新 Load 时 TokenVersion 已递增 | 正常路径 | P0 | IncrementTokenVersion + UserDetailsLoader.Del |
| TC-0721 | POST /api/auth/refreshToken 或 logout | 未登录调用 /auth/logout | ctx 中无 userDetails | 返回 401 "未登录" | 错误路径 | P0 | 未登录兜底 |
| TC-0722 | POST /api/auth/refreshToken 或 logout | 同一用户连续两次 logout | 登出两次 | tokenVersion 累加至 2 | 幂等/累加 | P1 | 每次自增,不被覆盖 |
| TC-0832 | POST /api/auth/refreshToken 或 logout | 静态 wiring 检查:routes.go 中 /auth/refreshToken 所在 rest.WithMiddlewares(...) 块必须包含 serverCtx.RefreshTokenRateLimit |
正则匹配 | 命中 | 架构/wiring | P0 | 防有人把中间件从路由剥离而忘了通知 QA |
| TC-0833 | POST /api/auth/refreshToken 或 logout | 行为验证:构造等价中间件链(quota=1),同 IP 连打 2 次;再换 IP 打 1 次 | RemoteAddr 三个样本 | 首次放行(业务层 401);同 IP 第 2 次 Code=429 "过于频繁";不同 IP 不受影响 |
安全/限流 | P0 | 与 wiring 正交交叉验证,限流真实生效且按 IP 隔离 |
| TC-0983 | POST /api/auth/refreshToken 或 logout | 成功路径:DB tokenVersion +1 且新 access/refresh claims.TokenVersion 严格等于 DB 新值 | 合法旧 refresh | 三者相等 | 正向/契约 | P0 | 预签版本 ↔ CAS 版本一致 |
| TC-0984 | POST /api/auth/refreshToken 或 logout | CAS 不命中("其他并发赢家已把 DB 推到 version=1"后再来一次 claims=0) | 抢先 IncrementTokenVersionIfMatch | 返回 401 "登录状态已失效",DB tokenVersion 不得再 +1 | 对抗/一致性 | P0 | 失败分支绝不推进 DB |
| TC-0985 | POST /api/auth/refreshToken 或 logout | 多轮链式刷新 + 旧 token 重放 | 连刷两次后重放第 1 次的新 refreshToken | 第三次 401,DB tokenVersion 不得再 +1 | 对抗 | P0 | 新 token 版本号必须匹配 DB,重放必拦 |
| TC-1066 | POST /api/auth/refreshToken 或 logout | 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 | 核心正向 |
| TC-1067 | POST /api/auth/refreshToken 或 logout | helper:claims.TokenVersion 与 DB 不一致 → ErrTokenVersionMismatch |
claims.TokenVersion=0,DB=5 | errors.Is(err, userModel.ErrTokenVersionMismatch);DB tokenVersion 不变;tokens 零值 |
安全 | P0 | CAS mismatch 不升版 |
| TC-1068 | POST /api/auth/refreshToken 或 logout | helper:用户已删除 → ErrTokenVersionMismatch |
先 Insert 再 Delete;claims 携带该 id | 同上 error;不 panic | 边界 | P0 | 删后 CAS |
| TC-1069 | POST /api/auth/refreshToken 或 logout | 跨协议互认:HTTP 签出的 refreshToken 能被 gRPC RefreshToken 无缝续签 | HTTP 首刷成功(v0→v1,拿到 newRt1);把 newRt1 直接灌到 gRPC | gRPC 返 NoError;新 tokens 非空;DB tokenVersion 再 +1(v1→v2) |
契约/集成 | P0 | 核心反漂移 |
| TC-1070 | POST /api/auth/refreshToken 或 logout | 跨协议互认:gRPC 签出的 refreshToken 能被 HTTP RefreshToken 无缝续签 | gRPC 先刷(v0→v1);把新 rt 灌到 HTTP | HTTP err==nil,DB 前进(v1→v2) |
契约/集成 | P0 | 对称镜像 |
| TC-1071 | POST /api/auth/refreshToken 或 logout | gRPC 重放:旧 rtV0 已被用过一次,再发给 gRPC 必须 Unauthenticated(而非 Internal) | gRPC 首刷成功;再用同一个 rtV0 调 gRPC | status.Code()==codes.Unauthenticated;Msg 含 "登录状态已失效" |
安全/错误映射 | P0 | ErrTokenVersionMismatch 的协议映射 |
| TC-1117 | POST /api/auth/refreshToken | RotateRefreshToken post-commit UD 缓存清理与请求 ctx 解耦 | 构造 parent = WithCancel(bg);插入 user + 预热 UD cache;RotateRefreshToken(parent, ...) 成功返回后 cancel(parent) |
RotateRefreshToken err==nil;DB tokenVersion 前进 1;再次 UserDetailsLoader.Load(userId, "") 读到的 ud.TokenVersion 必须是 DB 新值(说明 Clean 已在 detached ctx 上触发),Redis 里原 cacheKey 不再命中 |
安全/生命周期 | P0 | M-R14-1:UserDetailsLoader.Clean 必须跑在 DetachCacheCleanCtx 返回的新 ctx 上,否则请求 ctx 在 HTTP deadline / client 断连场景里会把 Clean 连带 cancel,留 5 分钟 TTL 的"旧 tokenVersion UD 缓存" 让旧 access token 复活 |
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 | seen去重 |
| TC-0048 | POST /api/perm/sync | 事务保护-中途失败回滚 | 模拟BatchUpdate失败 | 全部操作回滚, 返回SyncPermsError(500,"同步权限事务失败"), 不透传DB错误 | 事务验证 | P0 | LockByCodeTx→FindMapByProductCodeWithTx→BatchInsert→BatchUpdate 任一失败统一 500 |
| TC-0807 | POST /api/perm/sync | FindMapByProductCodeWithTx 与非事务版返回等价 |
Tx + 产品有启/禁用权限 | 两次调用数据一致;仅 status=1 被返回 |
一致性 | P0 | 基础设施:事务内读 perm map |
| TC-0808 | POST /api/perm/sync | 产品无权限时返回 | 空集 | 非 nil 空 map(防 upstream NPE) | 健壮性 | P1 | 空产品语义固定 |
| TC-0809 | POST /api/perm/sync | LockByCodeTx 对存在 code 返回完整行 |
Tx 内 SELECT ... FOR UPDATE |
行数据完整 | 正常路径 | P0 | 基础设施:事务内锁产品行 |
| TC-0810 | POST /api/perm/sync | 对不存在 code | Tx | sqlx.ErrNotFound |
分支 | P0 | 让 logic 层分辨"产品不存在" vs "DB 错误" |
| TC-0811 | POST /api/perm/sync | 两个事务同时锁同一行 | 并发 LockByCodeTx |
后者被阻塞,前者 commit 后才继续 | 并发/锁 | P0 | 实证 FOR UPDATE 的行级锁语义 |
| TC-0826 | POST /api/perm/sync | 同一 perm code 在 req 中重复 | perms = [A, A] |
落盘仅 1 条(入参内部去重) | 防自伤 | P0 | 入参级去重,避免 tx 内自撞 UNIQUE |
| TC-0843 | POST /api/perm/sync | mock 断言事务内必调用 LockByCodeTx |
1 次正常 sync | LockByCodeTx 在 tx 内被调用过且先于 FindMapByProductCodeWithTx |
架构 | P0 | 锁必须落在 tx 内,顺序固定 |
| TC-0844 | POST /api/perm/sync | mock LockByCodeTx 返回 sqlx.ErrNotFound |
在 tx 内返回 NotFound | SyncPermsError.Code == 404,文案 "产品不存在" |
分支 | P0 | tx 内识别产品被删 → 404 |
| TC-0845 | POST /api/perm/sync | mock LockByCodeTx 返回通用 error |
boom | SyncPermsError/500 包裹;不泄露原始 driver 错误 | 容错 | P1 | 非 NotFound 错误必须回滚为 500 |
| TC-0979 | POST /api/perm/sync | REST:tx 内 LockByCodeTx → ErrNotFound |
AppKey/AppSecret 有效但产品被并发删 | 返回 response.CodeError{Code:404, "产品不存在"} |
契约 | P0 | REST 侧必映射 404 |
| TC-0980 | POST /api/perm/sync | REST 反例:未映射 se.Code=500 |
tx 内非业务 err | 继续走 default 分支原样透传 SyncPermsError |
契约/反向 | P1 | 防一刀切把 500 误归 404 |
| TC-0981 | POST /api/perm/sync | gRPC:同 LockByCodeTx ErrNotFound | 同 TC-0979 输入 | status.Code() == codes.NotFound,文案 "产品不存在" |
契约 | P0 | gRPC 对外契约 |
| TC-0982 | POST /api/perm/sync | gRPC 反例:未映射 code | 同 TC-0980 | codes.Internal,不得被误分类为 NotFound |
契约/反向 | P1 | 防 SDK 误触发重试 |
| TC-1021 | POST /api/perm/sync | 既有 404 路径用例的 LockByCodeTx mock 显式携带 Status=1 |
原场景保持不变 | 行为不变 | 适配 | P0 | 所有既有 audit 路径都必须显式带 Status=1,否则命中 403 分支 |
| TC-1022 | POST /api/perm/sync | Dedup / mock / TxLock 三路径下事务内 Status 复核全覆盖 | 同上 | 行为不变 | 适配 | P0 | 对事务内 Status 复核全覆盖 |
| TC-1023 | POST /api/perm/sync | gRPC SyncPermissions 入口同样落入 Status=1 契约 |
LockByCodeTx 必带 Status=1 |
UnmappedCode 仍走 Internal | 适配 | P0 | gRPC 层也落入同一契约 |
| TC-1063 | POST /api/perm/sync | 纯新增(added>0 && updated==0 && disabled==0)→ 必须触发 CleanByProduct | 执行首次 ExecuteSyncPerms 打底,之后用 primeProductIndex 置 canary;再次触发 ExecuteSyncPerms(perms=全新 codes) |
canary 被删除;added>0;全权用户(SuperAdmin/本产品 ADMIN/DEVELOPER/DEV 启用成员)的 UD 聚合缓存必须立刻刷新,不得在 5min TTL 窗口内继续返回旧 perm 集合 |
契约/安全 | P0 | 全权用户 loadPerms 走 FindAllCodesByProductCode(productCode),新增 perm 若不清 UD 会造成 5min 视图偏差,所有触达 perm 的分支都要失效 |
| TC-1064 | POST /api/perm/sync | 至少一条 update(code 存在但 name/Status/Sort 变更)→ 必须触发 CleanByProduct | 预置 canary + 一条已有 perm;然后 sync 带同 code 但改名 | canary 被删除(CleanByProduct 触达);updated>0 |
契约 | P0 | update 路径 |
| TC-1065 | POST /api/perm/sync | 至少一条 disable(列表里不含的 perm 被置 Disabled)→ 必须触发 CleanByProduct | 同上但 sync 不传原 code;旧 perm 被禁用 | canary 被删除;disabled>0 |
契约 | P0 | disable 路径 |
| TC-1118 | POST /api/perm/sync | ExecuteSyncPerms post-commit CleanByProduct 与请求 ctx 解耦 | parent = WithCancel(bg);第一次 sync 注入 perm 打底;第二次 sync 改 Name 前用 primeProductIndex 预置 canary;传 parent 执行第二次 sync 后 立即 cancel(parent),再观察 Redis |
第二次 sync 返回 err==nil、updated==1;productIndexKey canary 已被删除 —— 说明 CleanByProduct 跑在 DetachCacheCleanCtx 上、与 parent cancel 解耦;若回退为"直接用 parent ctx",Redis DEL 会被 cancel 打断 → canary 残留 |
安全/生命周期 | P0 | M-R14-1:与 TC-1117 对偶,堵住"事务已提交但 UD 仍在 5 分钟 TTL 内挂着被禁用 perm" 的窗口 |
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 |
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失败 |
| TC-0769 | POST /api/auth/changePassword | ChangePassword 超过 TokenOpLimiter 配额 | 同一 userId 连续调用超限 | 429 "操作过于频繁,请稍后再试" | 安全/限流 | P0 | TokenOpLimiter.Take("chpwd:%d") |
| TC-0770 | POST /api/auth/changePassword | 冻结用户调用 ChangePassword | user.Status=Disabled | 403 "账号已被冻结" | 安全 | P0 | bcrypt 前检查 user.Status |
| TC-0771 | POST /api/auth/changePassword | 原密码错误时记录日志 | 错误密码 | 400 "原密码错误" + 日志含 change-password old-password mismatch |
可观测 | P1 | 失败日志可审计 |
| TC-1015 | POST /api/auth/changePassword | UpdatePassword 返回 ErrUpdateConflict 时必须回 409 |
mock UpdatePassword → ErrUpdateConflict |
CodeError.Code()==409;文案含 "密码已被其他会话修改" |
契约 | P0 | 主路径断言 |
| TC-1016 | POST /api/auth/changePassword | 非 ErrUpdateConflict 的 raw error 仍需透传(由 rest 兜 500) |
mock UpdatePassword → errors.New("driver: bad connection") |
errors.Is(err, genericErr)==true;不是 CodeError(不得被误吞为 409) |
反向契约 | P0 | 防止修复把所有错误都误包为 409 |
| TC-1039 | POST /api/auth/changePassword | Model 层正向:expectedUpdateTime 与 DB 一致 → 成功 + tokenVersion+1 + updateTime 前进 |
直接调 UpdatePassword(id, username, newHash, MustChangePasswordNo, existing.UpdateTime) |
err==nil;再 FindOne → password/updateTime/tokenVersion 全部按预期前进 |
契约 | P0 | happy path 钉死新签名语义 |
| TC-1040 | POST /api/auth/changePassword | 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 | 核心反回归——若回退到"内部自 FindOne",这里会误成功 |
| TC-1041 | POST /api/auth/changePassword | Model 层并发:同一 expectedUpdateTime 两 goroutine 并行 CAS | 2 个 goroutine 共享 T0,并发 UpdatePassword |
恰好 1 个成功、1 个 ErrUpdateConflict;DB 最终密码 = 赢者的密码;tokenVersion 只 +1 不是 +2 |
并发/契约 | P0 | 并发单胜者;tokenVersion 被累计两次会立即暴露退化 |
| TC-1042 | POST /api/auth/changePassword | Logic 层 E2E:同一 user 连续两次用 "同一旧密码 P0" 发起 ChangePassword,第二次必须 400 "旧密码错误" | 第一次 P0→P1(200),第二次仍送 oldPass=P0 | 第二次 CodeError.Code()==400,msg 含 "旧密码错误";不得成 409(否则 400/409 语义混淆) |
边界 | P0 | 400/409 分桶契约 |
| TC-1043 | POST /api/auth/changePassword | Logic 层 mock:ChangePassword 必须把"外层 FindOne 拿到的 user.UpdateTime" 原封不动透传给 Model 层 | mock UpdatePassword(id, username, _, MustChangePasswordNo, snapshotUpdateTime),断言第 5 个实参 |
mock EXPECT 命中;若回退为"Model 内部再读 updateTime",这里会拿到零值触发失败 | 契约 | P0 | CAS 快照来源契约 |
| TC-1179 | POST /api/auth/changePassword | "新旧密码相同"校验必须排在 bcrypt.CompareHashAndPassword 之前 |
用假冒 OldPassword(bcrypt 比对会失败),但 NewPassword 与 OldPassword 字面相等;请求落 ChangePassword |
CodeError.Code()==400;文案精确等于 "新密码不能与原密码相同"(若文案是"原密码错误"说明顺序被误回滚);UpdatePassword 绝不被调用 |
性能/契约 | P1 | 廉价字符串判等要跑在昂贵 bcrypt 之前,防止攻击者借 OldPassword 长度/并发把 bcrypt 放大成 CPU DoS;也把"用户输入了同一个新密码"早期吐回,避免无谓的 hash |
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 正向匹配 |
| TC-0774 | POST /api/product/create | appKey 长度=32 hex字符 (16字节) | CreateProduct | appKey 长度 = 32 | 功能 | P0 | generateRandomHex(16) → 32 hex chars |
| TC-0775 | POST /api/product/create | appSecret 长度=64 hex字符 (32字节) | CreateProduct | appSecret 长度 = 64 | 功能 | P0 | generateRandomHex(32) → 64 hex chars |
| TC-0776 | POST /api/product/create | 初始管理员密码长度=24 hex字符 (12字节) | CreateProduct | adminPassword 长度 = 24 | 功能 | P0 | generateRandomHex(12) → 24 hex chars,96 bit 熵 |
| TC-0827 | POST /api/product/create | mock InsertWithTx 返回 mysql error 1062,message 不含 "uk_code" |
&mysql.MySQLError{Number:1062,Message:"generic"} |
返回 response.ErrConflict(409) |
错误映射 | P0 | 去掉脆弱 strings.Contains 依赖,靠 mysql.MySQLError.Number 判定 |
| TC-0901 | POST /api/product/create | happy path:超管用有效 ticket 取回初始凭据 | superAdmin ctx + 合法 ticket | 返回 AppKey/AppSecret/AdminUser/AdminPassword;AppSecret 与 DB 中 bcrypt 匹配 | 正常路径 | P0 | 凭据落地 Redis 后端,响应体不再明文外泄 |
| TC-0902 | POST /api/product/create | 相同 ticket 二次消费 | 同上 ticket 第二次取 | 第二次 404 "凭据票据不存在或已被消费" | 一次性 | P0 | Redis.GetDelCtx 原子 GET+DEL |
| TC-0903 | POST /api/product/create | ticket 为空 | ticket="" | 400 "ticket 不能为空" | 异常路径 | P0 | 入参校验 |
| TC-0904 | POST /api/product/create | ticket 未知 | 随机 64 字符 ticket | 404 "凭据票据不存在或已被消费" | 异常路径 | P0 | 无存在性差异化,避免枚举 oracle |
| TC-0905 | POST /api/product/create | 非超管调用 | ADMIN ctx | 403 | 权限 | P0 | RequireSuperAdmin 生效 |
| TC-0906 | POST /api/product/create | 未登录调用 | context.Background() | 401/403 | 权限 | P0 | 无 UserDetails 时拒绝 |
| TC-0907 | POST /api/product/create | Redis 中 payload 被人为破坏 | 手工写入非 JSON 字符串 | 500 "凭据数据异常" 并删除该 key | 健壮性 | P1 | 防腐败数据长留 |
| TC-0908 | POST /api/product/create | Redis key 结构正确 | 观察实际 key | pm:initcred:{ticket};TTL ≤ 300s |
契约 | P1 | 运维可定位 |
| TC-0909 | POST /api/product/create | TTL 与响应 expiresAt 一致 | 观察返回的 credentialsExpiresAt | Redis TTL == Unix(expiresAt)-now (±5s) | 契约 | P1 | 客户端过期提示与后端一致 |
| TC-0910 | POST /api/product/create | 并发消费同一 ticket | 32 goroutine 同时 Fetch | 仅 1 个成功,其余 31 个返回 404 | 并发 | P0 | GetDelCtx 原子性抗竞态 |
| TC-0911 | POST /api/product/create | CreateProductResp JSON 不含 appSecret/adminPassword | marshal resp → json | 字段 appSecret/adminPassword 不出现 |
契约 | P0 | 响应体永不明文 |
| TC-0912 | POST /api/product/create | CreateProductResp 必含 credentialsTicket + credentialsExpiresAt | marshal resp | 两字段均非空/正数 | 契约 | P0 | 新的获取链路必备字段 |
| TC-0961 | POST /api/product/create | body 非法 JSON | {not-valid-json + superAdmin ctx |
400,且文案不含 sql/redis/ticket 关键字 | 契约/健壮性 | P0 | httpx.Parse 错误透传;不泄字段与实现细节 |
| TC-0962 | POST /api/product/create | 无登录上下文 | 不注入 UserDetails | 401 "未登录" | 权限 | P0 | handler 自身也必须 fail-close,不依赖 JwtAuth 中间件 |
| TC-0963 | POST /api/product/create | 非超管 ctx | MemberType=ADMIN | 403 "仅超级管理员可执行此操作",文案不含 "ticket" | 权限/信息泄漏 | P0 | RequireSuperAdmin 透传;防 ticket 存在性 oracle |
| TC-0964 | POST /api/product/create | 超管 + 空 ticket | {"ticket":""} |
400,文案含 "ticket" | 契约 | P0 | 入参必填校验 |
| TC-0965 | POST /api/product/create | 超管 + 未知 ticket | 随机字符串 | 400 "凭证票据无效或已过期",与"过期"共用文案 | 安全 | P0 | 防枚举 oracle,与 logic TC-0904 同契约 |
| TC-0966 | POST /api/product/create | 超管 + 已落地 ticket | 手工 SETEX 合法 JSON payload | HTTP 200 + 4 字段完整映射;Redis key 被 GetDel 消费 |
正常路径/一致性 | P0 | 字段映射正确 + 一次性消费 |
| TC-0967 | POST /api/product/create | 静态 wiring:JwtAuth 绑定 | 读取 routes.go 源码 |
/fetchInitialCredentials 所在 rest.WithMiddlewares 列表含 serverCtx.JwtAuth;prefix=/api/product |
回归/静态 | P0 | 防未来 goctl 覆写丢失中间件 |
| TC-0968 | POST /api/product/create | 静态反证:不得挂到限流组 | 读取 routes.go 源码 |
/fetchInitialCredentials 绝不出现在 AdminLoginRateLimit / ProductLoginRateLimit / RefreshTokenRateLimit / SyncRateLimit 的中间件块内 |
回归/静态 | P0 | 防被错迁到无鉴权组 |
| TC-0976 | POST /api/product/create | Redis 整个不可达(SetexCtx 永久失败) |
正常 CreateProduct 请求 + broken Redis | 返回 5xx 错误;sys_product/sys_user/sys_product_member 行数各 0 |
对抗/一致性 | P0 | 失败链路必须把三张新建行全部补偿掉 |
| TC-0977 | POST /api/product/create | Redis 失败 + 补偿成功后以同 Code 重建 | 正常 Redis + 相同 productCode | 第二次创建成功,不被 UNIQUE 约束阻塞 | 正向/幂等 | P0 | 补偿把位点清空,同 Code 不卡住 |
| TC-0978 | POST /api/product/create | 补偿顺序显式校验(child → parent) | 观察三表最终行数 | sys_product_member、sys_user、sys_product 均为 0 |
契约 | P0 | 删除顺序与外键契约一致 |
| TC-1029 | POST /api/product/create | seedAdminDept(t, ctx, svcCtx) 集中化 |
单次调用插一条启用部门 + t.Cleanup |
返回 deptId;测试结束自动清理 | 基础 | — | internal/logic/product/helper_test.go |
| TC-1030 | POST /api/product/create | CreateProduct 所有正向用例携带 AdminDeptId 后行为不变 |
入参带 AdminDeptId=seedAdminDept(...) |
行为不变 | 适配 | P0 | 契约变更全量回归 |
| TC-1031 | POST /api/product/create | FetchInitialCredentials 票据消费路径携带 AdminDeptId 后行为不变 |
同上 | 行为不变 | 适配 | P0 | 票据消费路径不破坏 |
| TC-1032 | POST /api/product/create | Redis 降级补偿链路径携带 AdminDeptId 后行为不变 |
同上,补偿路径 | 行为不变 | 适配 | P0 | Redis 降级后的补偿链保留 |
| TC-1033 | POST /api/product/create | 冲突路径下 SysDeptModel.FindOne 返 Status=1 且 AdminDeptId 透传 |
mock SysDeptModel.FindOne 返 Status=1 |
行为不变;AdminDeptId 透传 |
适配 | P0 | mock 侧补齐 |
| TC-1034 | POST /api/product/create | mock 侧两处 CreateProductReq 补齐 AdminDeptId 字段 |
同上,两处 CreateProductReq |
行为不变 | 适配 | P0 | mock 侧补齐 |
| TC-1035 | POST /api/product/create | 一次性票据 AdminPassword 长度=16(不再是旧的 24) |
读取一次性票据中的 AdminPassword |
len(cred.AdminPassword)==16(不是旧的 24) |
契约 | P1 | 长度断言回归 |
| TC-1202 | POST /api/product/create | 落盘的 admin_<code> 账号 sys_user.Avatar 必须是 SQL NULL |
真实 CreateProduct 完整落盘后查 CASE WHEN avatar IS NULL |
字段值 1(NULL 而非空串) | 契约/数据映射 | P1 | 与 CreateUser 同步契约;ORM 层 sql.NullString{Valid:false} 必须序列化成 SQL NULL 而非 "",否则未来 WHERE avatar IS NULL 判空分支会失准 |
| TC-1203 | POST /api/product/create | tx 内必须在 InsertWithTx 之前对 adminDeptId 取 S 锁;并发 DeleteDept 已提交 → 立即 400 |
mock:FindOne 预检通过(status=1);TransactCtx 执行闭包;闭包内 FindOneForShareTx 返 sqlx.ErrNotFound |
返 CodeError.Code()==400,文案含 "管理员部门不存在或已删除";SysProductModel.InsertWithTx / SysUserModel.InsertWithTx / SysProductMemberModel.InsertWithTx 一次都未被调用(若被调,gomock 在未声明 EXPECT 上立刻失败) |
并发/事务闭包 | P0 | 锁序:先 pre-check 再 tx 内 FOR SHARE,闭合"pre-check 通过→并发 DeleteDept 提交→Insert 写入幽灵 deptId"的窗口;orphan admin 一旦落盘会挂在已删除部门下,后续 DeptPath 校验全部失效 |
| 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 |
| — | — | — | — | 已删除:分页边界由 util.TestNormalizePage 单元测试覆盖;列表语义被 M-2 拆分为"超管走 FindList / 非超管只看自己"两条独立契约(TC-0850、TC-0871) |
|||
| — | — | — | — | — | 已删除:被 M-2 契约合并改写为 TC-0852(他人产品 → 404)、TC-0853(自己产品 AppKey 脱敏)、TC-0872(FindOne 错误 → 404 无差别响应) | ||
| — | — | — | — | — | 已删除:由 TC-0850(list)+ TC-0853(detail)覆盖 | ||
| — | — | — | — | — | 已删除:由 TC-0854(detail)+ TC-0871(list)覆盖 | ||
| TC-0090 | POST /api/product/update | updateProduct 非法状态值被拒绝 | status=99 | 400 "产品状态值无效" | 输入校验 | P0 | 仅允许 1/2 |
| TC-1138 | POST /api/product/update | Enabled→Disabled:产品下所有启用成员 sys_user.tokenVersion 批量 +1 | 产品含 3 个 Status=1 成员 + 1 个 Status=2 成员,req.Status=2 |
3 个启用成员的 tokenVersion 严格 +1;禁用成员 tokenVersion 不变;产品 status=2 同事务落盘 |
安全/会话吊销 | P0 | BatchIncrementTokenVersionWithTx + FindActiveMemberUserIdsByProductCodeTx 原子语义 |
| TC-1139 | POST /api/product/update | Disabled→Enabled:tokenVersion 全部不变 | 产品原 status=2 含成员,req.Status=1 |
所有成员 tokenVersion 不变;产品 status=1 |
正向回归 | P0 | 重启用不让任何用户获得未曾持有的权限,无需吊销 |
| TC-1140 | POST /api/product/update | 禁用产品无 active 成员:事务不崩,批量 UPDATE 跳过 | 产品下 0 启用成员 | err==nil;产品 status=2;BatchIncrementTokenVersionWithTx 的空分支被走过 |
边界 | P0 | len(ids)==0 早退;不得在空集合上构造非法 SQL |
| TC-1141 | POST /api/product/update | 跨产品隔离:禁用产品 A 不递增产品 B 成员的 tokenVersion | 用户 U 同时是产品 A/B 的启用成员;禁用 A | U 的 tokenVersion +1(因 A 的成员身份);产品 B 成员仅 U 的 tokenVersion 被打高一次,其他 B 独占成员的 tokenVersion 严格不变 | 安全/跨产品最小化 | P0 | FindActiveMemberUserIdsByProductCodeTx 必须按 productCode 过滤;不得误伤其他产品专属成员 |
| TC-1142 | POST /api/product/update | post-commit 失效 sysProduct id/appKey/code 三把 key | 预热三把缓存后正常更新 | 三把 key 均被 DEL | 缓存一致性 | P0 | InvalidateProductCache 必须被 Logic 在事务 commit 后显式调用 |
| TC-0850 | POST /api/product/update 或 list/detail | MEMBER 调 ProductList | caller.ProductCode=pA |
仅返回 pA 一条(即使 DB 内有 pB、pC) | 安全/访问控制 | P0 | 非超管只见自己产品 |
| TC-0851 | POST /api/product/update 或 list/detail | MEMBER 调 ProductList 且 ProductCode=="" |
游离 MEMBER | 返回空列表 Total=0, List=[] |
边界 | P0 | 无 productCode 时降级为 0 条 |
| TC-0852 | POST /api/product/update 或 list/detail | MEMBER 调 ProductDetail 查他产品 | 目标 id 属于 pB | 404 "产品不存在"(不暴露存在性) | 安全/枚举 | P0 | 区分开"存在但无权"会被当 oracle |
| TC-0853 | POST /api/product/update 或 list/detail | MEMBER 调 ProductDetail 查自己产品 | 目标 id 属于 pA | 200 OK,AppKey 字段为空(保持原 AppKey-hidden 语义) |
正常路径 | P0 | 字段级脱敏不被取消 |
| TC-0854 | POST /api/product/update 或 list/detail | 超管调 ProductDetail | 任意 id | 200 OK + AppKey 可见 | 正常路径 | P1 | 超管路径不受访问控制影响 |
| TC-0855 | POST /api/product/update 或 list/detail | MEMBER 调 DeptTree | DeptPath="/1/2/" |
返回树中 Path 前缀匹配的子树;父部门/兄弟部门不可见 | 安全 | P0 | 按 DeptPath 剪枝 |
| TC-0856 | POST /api/product/update 或 list/detail | MEMBER 调 DeptTree 且 DeptPath="" | 游离成员 | 返回空切片 [] |
边界 | P0 | 无 DeptPath 降级空树 |
| TC-0857 | POST /api/product/update 或 list/detail | 产品 ADMIN 调 DeptTree | AdminCtx + DeptPath="/100/1/" |
仅返回 /100/1/ 子树(与 MEMBER 同路径);父部门 /100/ / 平行分支 /200/ 不可见 |
安全/跨产品信息最小化 | P0 | fullAccess = caller.IsSuperAdmin;原 "ADMIN 保留组织视图" 契约已被收回,避免小产品 ADMIN 侦察大产品的 DEV/HR 部门命名 |
| TC-1128 | POST /api/product/update 或 list/detail | SuperAdmin 调 DeptTree | SuperAdminCtx |
返回完整树(/100/ + /200/ 两个根) |
正常路径 | P0 | fullAccess 仅对 SuperAdmin;正向回归 |
| TC-1129 | POST /api/product/update 或 list/detail | 产品 DEVELOPER 调 DeptTree | DeveloperCtx + DeptPath="/100/1/" |
仅返回 /100/1/ 子树(父部门 / 平行分支不可见) |
安全 | P0 | 与 MEMBER 同剪枝路径;避免 DEVELOPER 枚举全组织结构 |
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修复后端到端 |
| TC-1084 | POST /api/dept/create | 父部门已 Disabled → 事务内 FindOneForShareTx 复核拒绝 | - | ParentStatus=2 | 400 "父部门已被禁用,无法创建子部门";DB 无子行 | P0 | 修复前只 SELECT id 会放行 |
| TC-1085 | POST /api/dept/create | 父部门 Enabled 正向路径 + parentPath 来自事务内 snapshot | - | ParentStatus=1 | 子部门创建成功;child.Path = parent.Path + childId + "/" |
P0 | 保证 parentPath 源自锁视图 |
| TC-1086 | POST /api/dept/create | CreateDept × UpdateDept(禁用父) 并发 6 轮 | - | 每轮起 CreateDept + 裸 UPDATE status=2 |
合法终态二选一:(a) CreateDept 先成功(父当时仍 Enabled),(b) UpdateDept 先成功 → CreateDept 收 400 "父部门已被禁用";禁止出现 DB 残留子行 + 父 Disabled 但子 Enabled 的 write skew | P0 | 事务内 S 锁与裸 UPDATE 的锁链闭合 |
| TC-1202 | POST /api/dept/create | Sort 超出范围 [-100000, 100000] 被拒绝 | Sort = -100001 / Sort = 100001 / Sort = math.MaxInt64 |
400 "排序值必须在 -100000 到 100000 之间";DB 无新行 | 边界/输入校验 | P0 | 防极端 Sort 值透传 DB 破坏同级排序稳定性;与 UpdateDept 同口径校验 |
| 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 | 级联缓存失效 |
| 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 | 检查关联用户 |
| — | — | — | — | — | 已删除:M-2 后 DeptTree 按 caller 身份剪枝,旧测试假定任何身份可拿全树已不成立;新契约由 TC-0855/0856/0857(deptTreeAccessControl_audit_test.go)覆盖,"孤儿→根"行为隐含在 fullAccess 路径中 |
||
| TC-0714 | POST /api/dept/update 或 delete/tree | DeptType/Status 未变更时不清缓存 | 只改 name | 无 Clean 调用 | 分支覆盖 | P1 | unchanged 分支 |
| TC-0715 | POST /api/dept/update 或 delete/tree | 乐观锁冲突返回 ErrConflict | UpdateWithOptLock 返回 0 行 | 返回 409/Conflict | 并发 | P0 | 版本号冲突 |
| TC-0759 | POST /api/dept/update 或 delete/tree | 10 goroutine 同时 UpdateWithOptLock 同一行 | 共享 expectedUpdateTime=t0 | 恰好 1 成功 + 9 ErrUpdateConflict; DB 里 UpdateTime 被推进, Remark 非初值 |
并发/竞态 | P0 | WHERE updateTime=? + RowsAffected 判定, 挡"无声覆盖"退化 |
| TC-0766 | POST /api/dept/update 或 delete/tree | 删除有子部门的部门(FOR UPDATE 锁定读) | parentId 指向目标部门的子部门存在 | 400 "该部门下存在子部门,无法删除" | 业务约束 | P0 | SELECT id ... FOR UPDATE 子部门存在性锁定读 |
| TC-0767 | POST /api/dept/update 或 delete/tree | 删除有关联用户的部门(FOR UPDATE 锁定读) | deptId 指向目标部门的用户存在 | 400 "该部门下仍有关联用户,无法删除" | 业务约束 | P0 | SELECT id ... FOR UPDATE 用户存在性锁定读 |
| TC-0768 | POST /api/dept/update 或 delete/tree | CreateDept 父部门 FOR SHARE 锁生效 | parentId > 0 | 事务内对父部门 SELECT FOR SHARE;父不存在则 404 |
安全 | P0 | CreateDept 防并发删除父部门 |
| TC-0846 | POST /api/dept/update 或 delete/tree | CleanByUserIds 批量清理多用户缓存 |
预埋 3 用户各 2 产品缓存 | Redis 中 6 条 ud:userId:productCode + 3 条 ud:idx:u:* 均被删除 |
正确性 | P0 | M-1 基础设施:SUNION + 批 DEL 必须覆盖所有索引 |
| TC-0847 | POST /api/dept/update 或 delete/tree | CleanByUserIds 空 ids 切片 |
[] |
立即返回,不 panic,不调用 Redis | 边界 | P1 | 防未来调用方传空列表打空 RTT |
| TC-0848 | POST /api/dept/update 或 delete/tree | UpdateDept 改 deptType 时调 CleanByUserIds |
mock: FindIdsByDeptId → [100,101],断言 CleanByUserIds 路径(通过 mock 的 FindIdsByDeptId 期望 +真实 loader 执行) | 无错误返回;FindIdsByDeptId 被调用恰好 1 次 | 行为 | P0 | UpdateDept 在变更时才触达用户列表 |
| TC-0849 | POST /api/dept/update 或 delete/tree | UpdateDept 的 FindIdsByDeptId 失败 |
mock 返回 err | 返回 nil(不是 500);旧权限缓存 TTL 兜底 |
容错 | P0 | 修复后的 degraded 成功语义 |
| TC-1174 | POST /api/dept/update | DeptType DEV→NORMAL 必须批量递增部门下所有成员 tokenVersion | seed Dept(DeptType=DEV, Status=1) + 3 个成员;{DeptType:"NORMAL"} |
3 个成员 sys_user.tokenVersion 严格 +1;部门 deptType="NORMAL" 同事务落盘 |
安全/会话吊销 | P0 | DEV→NORMAL = 全权分支失效,必须把还挂在 DEV 身份上的 access token 打掉;BatchIncrementTokenVersionWithTx + FindIdsByDeptIdForShareTx 原子语义 |
| TC-1175 | POST /api/dept/update | DEV 部门 Status Enabled→Disabled 批量递增 tokenVersion | seed Dept(DeptType=DEV, Status=1) + 2 个成员;{Status:2} |
2 个成员 tokenVersion +1;部门 status=2 |
安全/会话吊销 | P0 | 禁用 DEV 部门 = 全权分支熄灯,对偶 TC-1174,同样走收窄分支 |
| TC-1176 | POST /api/dept/update | NORMAL 部门 Status Enabled→Disabled 批量递增 tokenVersion | seed Dept(DeptType=NORMAL, Status=1) + 2 个成员;{Status:2} |
2 个成员 tokenVersion +1;部门 status=2 |
安全/会话吊销 | P0 | 冻结部门必须冻结其成员会话,避免 DB 里 Disabled 但旧 JWT 仍通 |
| TC-1177 | POST /api/dept/update | NORMAL→DEV(升权方向)tokenVersion 保持不变 | seed Dept(DeptType=NORMAL, Status=1) + 1 个成员;{DeptType:"DEV"} |
成员 tokenVersion 与初值严格相等;部门 deptType="DEV" 落盘 |
正向回归 | P0 | 升权不构成收窄,不得把合法用户无故踢下线 |
| TC-1178 | POST /api/dept/update | Status Disabled→Enabled(恢复启用)tokenVersion 保持不变 | seed Dept(DeptType=NORMAL, Status=2) + 1 个成员;{Status:1} |
成员 tokenVersion 与初值严格相等;部门 status=1 |
正向回归 | P0 | 解冻方向镜像 TC-1177;仅"收窄"分支递增 tokenVersion |
| TC-1200 | POST /api/dept/delete | 成功删除后必须在 post-commit 上显式失效 sysDept:id 缓存 |
插部门 → 手工 SET 一份 ghost 快照到 <prefix>:cache:sysDept:id:<id>(模拟 commit 前的并发回填)→ DeleteDept |
Redis key 被 DEL(Exists==false);若残留则说明 post-commit 失效兜底被误撤,旧 dept 快照最长 5min TTL 内仍可被 FindOne 读到,叠加 orphan user 会放大为跨部门授权泄漏 |
缓存一致性/安全 | P0 | sqlc.CachedConn.ExecCtx 在 tx commit 之前已 DelCache,commit 前任何并发读都可能把旧行回填;必须用 detached ctx 在 commit 之后再显式 InvalidateDeptCache |
| TC-1203 | POST /api/dept/update | Sort 超出范围 [-100000, 100000] 被拒绝 | Sort = 100001 / Sort = -100001 |
400 "排序值必须在 -100000 到 100000 之间";DB 记录不变 | 边界/输入校验 | P0 | 与 CreateDept 同口径校验,防极端 Sort 值破坏部门树排序稳定性 |
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 | 空结果 |
| 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失败 |
| TC-0730 | POST /api/role/* | 非超管 admin 把 roleA.PermsLevel 从 100 调到 10(数字变小 = 提升权级) | AdminCtx,PermsLevel 100→10 | 403 "非超管不能提升角色的权限级别",DB 保持 100 | 安全 | P0 | caller.IsSuperAdmin=false && newLevel<oldLevel(数字越小 = 权限越高);R12 后错误消息从"降低"修正为"提升" |
| TC-0731 | POST /api/role/* | 产品 admin 保持或提升 PermsLevel | 100→100、100→500 | 均允许;DB 最终 PermsLevel=500 | 正常路径 | P0 | new>=old 放行 |
| TC-0732 | POST /api/role/* | 超管降低 PermsLevel | SuperAdminCtx,500→10 | 成功 | 正常路径 | P0 | IsSuperAdmin 绕开 |
| TC-0733 | POST /api/role/* | PermsLevel 越界(0/-1/1000/10000) | 任意非法 PermsLevel | 400 "权限级别必须在 1-999 之间" | 边界 | P0 | L-3 前置校验 |
| TC-0777 | POST /api/role/* | UpdateRole post-commit 缓存清理失败时仍返回成功 | FindUserIdsByRoleId 返回 err |
handler 返回 nil(200)——缓存失效尽力而为,不得因缓存失败把已提交事务映射为 500 让客户端误重试 | 容错 | P0 | 对齐 UpdateRole 注释语义;与 TC-0859 联合覆盖 |
| TC-0778 | POST /api/role/* | UpdateRole 乐观锁冲突 | 并发修改同一角色 | 409 "数据已被其他操作修改,请刷新后重试" | 并发安全 | P0 | UpdateWithOptLock WHERE updateTime=? |
| TC-0779 | POST /api/role/* | UpdateProduct 乐观锁冲突 | 并发修改同一产品 | 409 同上 | 并发安全 | P0 | UpdateWithOptLock WHERE updateTime=? |
| TC-0780 | POST /api/role/* | UpdateMember 基于事务内 locked 数据更新 | 正常更新 | 成功,使用 locked 行数据组装 UPDATE |
数据一致 | P0 | 事务内 FindOneForUpdateTx 结果作为更新基础 |
| TC-0858 | POST /api/role/* | BindRolePerms:事务 OK,FindUserIdsByRoleId 返回 err |
mock | 返回 nil(200)——不再映射 500 | 错误映射 | P0 | degraded 成功;客户端不应重试 |
| TC-0859 | POST /api/role/* | UpdateRole:事务 OK,FindUserIdsByRoleId 返回 err |
mock | 返回 nil(200) | 错误映射 | P0 | 同上 |
| TC-1000 | POST /api/role/* | 非超管访问别的产品的 role | Admin in test_product;目标 role 在 mn3_other_xxx |
404 "角色不存在" | 安全/Oracle | P0 | 跨产品必须 404 而非 403 |
| TC-1001 | POST /api/role/* | "id 不存在" vs "跨产品" 响应对比 | 两条路径对照 | code + body 完全一致 | 安全/Oracle | P0 | 彻底消除枚举 oracle |
| TC-1002 | POST /api/role/* | 超管跨产品访问 | 超管 → 跨产品 role + permIds | 正常返回完整 RoleItem | 正向 | P0 | 审计/运维路径不得被误伤 |
| TC-1119 | POST /api/role/update | L-R14-1 非超管访问别产品 roleId 必须 404(不是 403) | AdminCtx("test_product") 调 UpdateRole(id),该 roleId 实际归属 other_* |
CodeError.Code()==404,文案 "角色不存在";DB 未写入 |
安全/枚举 | P0 | authHelper.ResolveOwnRoleOr404 收敛 404 vs 403 枚举 oracle,与 RoleDetail 的 M-N3 口径一致 |
| TC-1197 | POST /api/role/create | 非超管 product ADMIN 禁止创建 permsLevel=1 顶格角色 |
AdminCtx(pc) + {productCode:pc, name:...,permsLevel:1} |
CodeError.Code()==403;文案含 "权限级别为 1 的顶格角色";DB 无新角色 |
安全/纵向越权 | P0 | 顶格角色只能由 SuperAdmin 创建;若放行 ADMIN,其可"建 R_super + BindRoles 给下属" 绕开 GuardRoleLevelAssignable 的同级拦截,形成等价横向提权链路 |
| TC-1198 | POST /api/role/create | product ADMIN 创建 permsLevel>=2 次级角色放行 |
AdminCtx(pc) + {productCode:pc, name:..., permsLevel:2} |
成功;DB sys_role.permsLevel=2 |
正向回归 | P0 | 防 GuardCreateRolePermsLevel 过度收紧把合法业务路径也打死 |
| TC-1199 | POST /api/role/create | SuperAdmin 不受 permsLevel=1 约束 |
SuperAdminCtx() + {..., permsLevel:1} |
成功;DB sys_role.permsLevel=1 |
正向回归 | P0 | SuperAdmin 是顶格角色的唯一合法来源;若回滚把超管也拦住,系统将没有任何路径能初始化 permsLevel=1 的角色 |
| TC-1204 | POST /api/role/update | UpdateRole 重命名后旧 name 索引缓存必须失效 | 超管创建角色 name=A;第一次 Load 让 sysRole:productCode:name:<pc>:A 写入缓存;UpdateRole 把 name 改为 B;再次 FindOneByProductCodeName(pc, "A") |
FindOneByProductCodeName 返回 sqlx.ErrNotFound(不得返回旧行数据);说明 post-commit InvalidateRoleCache(oldName) 已把 Redis 里的 <pc>:A 索引键清掉 |
缓存一致性/安全 | P0 | UpdateWithOptLock 内部只失效新 name 键;rename 路径的旧 name 键必须在 post-commit 由 InvalidateRoleCache(prevName) 显式清除,否则 Redis TTL 窗口内同名并发创建会命中幽灵快照 |
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条 |
| TC-1120 | POST /api/role/delete | L-R14-1 非超管 DeleteRole 别产品 roleId 必须 404(不是 403) | AdminCtx("test_product") 调 DeleteRole(id),该 roleId 归属 other_* |
CodeError.Code()==404,文案 "角色不存在";DB 未删 |
安全/枚举 | P0 | authHelper.ResolveOwnRoleOr404 统一收敛,与 UpdateRole / RoleDetail 对齐 |
| TC-1201 | POST /api/role/delete | 成功删除后 post-commit 必须同时失效 sysRole:id 与 sysRole:productCode:name 两把键 |
插角色 → 手工 SET ghost 到 <prefix>:cache:sysRole:id:<id> 与 <prefix>:cache:sysRole:productCode:name:<pc>:<name> → DeleteRole |
两把 Redis key 均被 DEL;若 id key 残留则 FindOne(id) 命中 ghost → CAS 撞不到行回 409;若 name key 残留则同名 role 在删除后反复被"已存在"误拒(uniqueness oracle) |
缓存一致性/契约 | P0 | sqlc.CachedConn.ExecCtx 的 "exec→DelCache" 在 commit 之前执行;commit 前并发 FindOne / FindOneByProductCodeName 会回填旧行;detached ctx 上补一次 InvalidateRoleCache 是闭环关键 |
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 |
| TC-1024 | POST /api/role/bindPerms | BindRolePerms 事务首步调用 LockByIdTx 锁 sys_role 行,再走 FindPermIdsByRoleIdTx 读 diff 基准 |
gomock 记录调用顺序 | TransactCtx → LockByIdTx → FindPermIdsByRoleIdTx → DeleteByRolePermTx → BatchInsertWithTx |
并发/契约 | P0 | bindRolePermsLogic_mock_test.go 已更新 |
| TC-1025 | POST /api/role/bindPerms | BindRolePerms post-commit cache 清理失败仍 Success |
cache Clean 返 error |
响应 Success;事务内 mock 顺序保持 M-R10-2 | 并发/契约 | P0 | postCommitCacheDegraded_audit_test.go 已按 M-R10-2 全量重写 mock |
| TC-1026 | POST /api/role/bindPerms | BindRoles 事务首步 FindOneForUpdateTx(memberId) 锁 sys_product_member 行,再走 FindRoleIdsByUserIdForProductTx |
gomock 记录调用顺序 | TransactCtx → FindOneForUpdateTx → FindRoleIdsByUserIdForProductTx → DeleteByUserIdAndRoleIdsTx → BatchInsertWithTx |
并发/契约 | P0 | bindRolesLogic_mock_test.go 已更新 |
| TC-1121 | POST /api/role/bindPerms | L-R14-1 非超管 BindRolePerms 别产品 roleId 必须 404(不是 403) | AdminCtx("test_product") 调 BindRolePerms(id, [...]),该 roleId 归属 other_* |
CodeError.Code()==404,文案 "角色不存在";sys_role_perm 无变更 |
安全/枚举 | P0 | 与 UpdateRole / DeleteRole 口径一致,避免已认证用户借 404 vs 403 枚举跨产品 roleId |
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 | 密码强度校验(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 | 密码强度校验 |
| 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 |
| TC-0818 | POST /api/user/create | SuperAdmin 创建用户未显式指定 mustChangePassword |
req 缺字段 | DB 落盘 MustChangePassword=1 |
默认值/安全 | P1 | 默认 Yes 才能保证账号发出后立刻被改密 |
| TC-0994 | POST /api/user/create | 产品 ADMIN 为非自己管辖部门创建用户 | caller DeptPath=/100/ 目标部门 /999/ |
403 "无权在非自己管辖的部门下创建用户" | 对抗/越权 | P0 | 防产品 ADMIN 预埋 admin*/ops* 等关键用户名合谋 AddMember |
| TC-0995 | POST /api/user/create | 产品 ADMIN 在自己子树下创建用户 | caller /200/ → 目标 /200/1/ |
正常创建,DB 落盘 | 正向 | P0 | 正向回归,不得误伤合法路径 |
| TC-0996 | POST /api/user/create | SuperAdmin 在任意部门 / DeptId=0 均可创建 | 超管两条路径 | 两条均成功,支撑跨组织系统账号语义 | 正向 | P0 | 超管豁免分层 |
| TC-0997 | POST /api/user/create | caller.DeptPath=="" 的 legacy 产品 ADMIN | DeptId 指向真实部门 | 403 "您未归属任何部门,无权创建用户" | 对抗 | P1 | legacy 账号 fail-close |
| TC-0998 | POST /api/user/create | 非超管 caller 传 DeptId=0 | 任意合法用户名 | 400 "必须指定部门" | 契约 | P1 | 阻断非超管在部门树外开口 |
| TC-0999 | POST /api/user/create | 目标部门 status=Disabled | 超管 → 已禁用部门 | 400 "目标部门已停用" | 契约 | P1 | 与 UpdateDept 闭环 |
| TC-1100 | POST /api/user/create | 拒绝 deptId<0(避免负数穿透) | 超管 + DeptId=-1 |
400 "部门ID必须为非负整数";sys_user 无新增行 | 输入校验 | P0 | 防 sys_user.deptId=-1 僵尸账号(FindOne(-1) → 5xx degrade) |
| TC-1122 | POST /api/user/create | H-R14-1 非超管 caller 把新用户建到 DEV 部门必须 403 | callerAdminCtx(ADMIN / DeptPath 包含目标)+ DeptId=<DeptType=DEV 且启用> |
CodeError.Code()==403,文案含 "仅超级管理员可将用户调入研发部门";sys_user 无新增行 |
安全/跨产品升权 | P0 | 镜像 updateUserLogic.go 的 H-R14-1 护栏,封死"加入 DEV 即全权"跨产品信任穿透 |
| TC-1123 | POST /api/user/create | H-R14-1 SuperAdmin 仍可把新用户建到 DEV 部门(正向回归) | 超管 + DeptId=<DEV 启用> |
创建成功,DB 落盘 deptId 与 deptType=DEV 对应;caller.IsSuperAdmin 被豁免 |
正常路径 | P0 | 防护栏误伤合法运维路径 |
| TC-1192 | POST /api/user/create | 非超管不得以保留前缀(admin_ / svc_ / root_ / sys_)占名 |
AdminCtx + {username:"admin_x", password:"Aa123456"} 等 4 组 |
每组 CodeError.Code()==400;文案含保留前缀提示;sys_user 均无新增行 |
安全/命名空间抢注 | P1 | 防产品 ADMIN 预置 admin_<code> 等"像系统账号"的用户名再通过 AddMember 等路径拔高成 ADMIN,形成命名空间阴谋 |
| TC-1193 | POST /api/user/create | SuperAdmin 允许使用保留前缀 | SuperAdminCtx() + {username:"admin_abc", password:"Aa123456"} |
创建成功;DB 正常落盘 | 正向回归 | P1 | 保留前缀仅限 SuperAdmin;CreateProduct 的 admin_<code> 初始化流程、运维脚本必须继续工作 |
| TC-1194 | POST /api/user/create | 未传 avatar 时 sys_user.avatar 必须写入 SQL NULL(而非空串) |
正常创建,入参不带 avatar |
创建成功;SELECT CASE WHEN avatar IS NULL THEN 1 ELSE 0 END 返回 1 |
契约/数据映射 | P1 | sql.NullString{Valid:false} 必须被序列化成 SQL NULL,否则将来 WHERE avatar IS NULL 判空分支与"空串 vs NULL"在 ORM 层混淆会触发静默偏差 |
| TC-1195 | POST /api/user/create | mock:InsertWithTx 必须在 TransactCtx 闭包内、并且发生在 FindOneForShareTx(sys_dept) 之后 |
gomock 记录 Insert 顺序;TransactCtx 执行闭包;闭包内 FindOneForShareTx 通过 |
SysUserModel.TransactCtx 恰好 1 次;SysDeptModel.FindOneForShareTx 在 SysUserModel.InsertWithTx 之前;插入的 *SysUser.Avatar.Valid==false |
并发/事务闭包 | P0 | 锁序:对 sys_dept[deptId] 取 S 锁后再 Insert sys_user,与 DeleteDept 的 X 锁串行;闭环"pre-check 通过→DeleteDept 提交→本 tx Insert 写入幽灵 deptId"的竞态 |
| TC-1196 | POST /api/user/create | mock:闭包内 FindOneForShareTx 返 sqlx.ErrNotFound(并发 DeleteDept 已提交) |
同上 mock,仅把 FOR SHARE 读改成 ErrNotFound | CodeError.Code()==400,文案含 "部门不存在或已删除";SysUserModel.InsertWithTx 绝不被调用;DB 无新 user |
并发/事务闭包 | P0 | DeleteDept 胜出时必须立即终止;若回退成"忽略 S 锁读的错"继续 Insert,就会产生挂在已删 deptId 上的 orphan user,其 DeptPath 校验永久失效 |
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 | IsSuperAdmin==Yes 保护 |
| TC-0170 | POST /api/user/update | updateUser-产品管理员可管理范围内用户 | ctx=ADMIN, target在管理范围内 | 更新成功 | 正常路径 | P0 | 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 | 状态变更强制下线 |
| TC-0174 | POST /api/user/update | updateUser 仅改 profile 不递增 tokenVersion | req.Nickname+Email | 更新成功, tokenVersion不变 | 正常路径 | P0 | 非状态字段不影响会话 |
| TC-0175 | POST /api/user/update | updateUser 乐观锁冲突 -> 409 | 基于过期 updateTime 更新 | 返回 CodeError(409, "数据已被其他操作修改...") | 并发/异常 | P0 | ErrUpdateConflict 透传 |
| TC-0746 | POST /api/user/update | DEVELOPER 把成员挪出自己子树 | Caller.DeptPath=/100/,targetNewDept.DeptPath=/999/ | 403 "无权将用户转移到该部门",DB 保持原 deptId | 安全 | P0 | strings.HasPrefix(newDept.DeptPath, caller.DeptPath) 前缀校验 |
| TC-0747 | POST /api/user/update | DEVELOPER 在自己子树内移动成员 | Caller=/100/,newDept=/100/200/ | 200 OK,DB 更新 deptId | 正常路径 | P0 | 子树内放行 |
| TC-0748 | POST /api/user/update | 产品 ADMIN 把 target 挪到子树外必须 403 | AdminCtx + DeptPath="/300/",req.DeptId=<非 DEV、DeptPath="/500/"> |
403 "无权将用户转移到该部门";DB sys_user.deptId 保持原值 |
安全 | P0 | ADMIN 必须与 DEVELOPER 同口径过 strings.HasPrefix(newDept.DeptPath, caller.DeptPath);不得借 ADMIN 身份绕过子树约束 |
| TC-0814 | POST /api/user/update | DEVELOPER 将他人 deptId 置 0 | caller=DEVELOPER | 403;目标 deptId 不变 | 安全/越权 | P0 | 防"把用户挪出部门树以逃出管理视野" |
| TC-0815 | POST /api/user/update | MEMBER 将他人 deptId 置 0 | caller=MEMBER | 403;目标 deptId 不变 | 安全/越权 | P0 | 同上 |
| TC-0816 | POST /api/user/update | 产品 ADMIN 将他人 deptId 置 0 | caller=ADMIN | 403 "仅超级管理员可将用户移出部门";目标 deptId 不变 | 安全/跨产品结构破坏 | P0 | 原"产品 ADMIN 可移出"契约被收回:sys_user.deptId 是全局字段,P1 ADMIN 原先可让 P2 视角下共有成员变"孤儿"(P2 的 MEMBER/DEVELOPER/子 ADMIN 均通不过 checkDeptHierarchy)。收敛给 SuperAdmin |
| TC-0817 | POST /api/user/update | SuperAdmin 将他人 deptId 置 0 | caller=SuperAdmin | 200;目标 deptId=0 | 正常路径 | P1 | 顶级权限链路通畅 |
| TC-1049 | POST /api/user/update | 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 | 核心反回归 |
| TC-1050 | POST /api/user/update | 非事务路径:deptId 未变的 UpdateUser 不触发 FindOneForShareTx 的 S 锁路径 |
构造"只改 nickname、deptId 不变" 的更新 | 事务只走 UpdateProfileWithTx;SysDeptModel.FindOneForShareTx 未被打到(观察事务 SQL / mock 无 expect) |
契约/性能 | P1 | 避免"无切换时也打 S 锁" 导致退化 |
| TC-1101 | POST /api/user/update | 拒绝 *req.DeptId < 0 透传成脏 deptId |
超管 + DeptId=Int64Ptr(-1) |
400 "部门ID必须为非负整数";DB sys_user.deptId 不变 |
输入校验 | P0 | 与 CreateUser 对齐;防 FindOne(-1) ErrNotFound → 5xx / 僵尸账号 |
| TC-1102 | POST /api/user/bindRoles | 非超管且 caller.MemberType==""("游离" JWT)不得通过 404 枚举 userId 存在性 |
自定义 caller:IsSuperAdmin=false, MemberType="";userId 取不存在的值 |
403 "缺少产品成员上下文"(不是 404 "用户不存在") | 安全/枚举 | P0 | 修复前:MEMBER 空上下文会先 FindOne(userId) 返 404,暴露 userId 空间 |
| TC-1103 | POST /api/user/bindRoles | 超管不传 productCode → 400(新增前置校验) |
超管 ctx + {"userId":999,"roleIds":[1]}(不传 productCode) |
400 "必须指定产品编码" | 输入校验 | P0 | 超管 JWT 无 productCode,必须显式传入;不再穿透到 FindOne |
| TC-1265 | POST /api/user/bindRoles | 非超管传入 req.ProductCode 指向其他产品时该字段必须被忽略 |
AdminCtx(productCode="test_product") + req.ProductCode="other_product" |
绑定成功,角色落在 JWT context 的 test_product 下,不跨产品 |
安全/产品隔离 | P0 | 非超管不得通过 req.ProductCode 绕过产品隔离;只有超管才允许显式覆盖 productCode |
| TC-1266 | POST /api/user/bindRoles | 非超管不传 req.ProductCode,使用 JWT context 中的 productCode 正常绑定 |
AdminCtx(productCode="test_product") + 不传 productCode |
绑定成功 | 正向回归 | P0 | 非超管正常路径回归 |
| TC-1124 | POST /api/user/update | H-R14-1 ADMIN 把目标调入 DEV 部门必须 403 | AdminCtx + DeptPath="/"(豁免子树校验)+ req.DeptId=<DEV 部门> |
403 "仅超级管理员可将用户调入研发部门";DB sys_user.deptId 不变 |
安全/跨产品升权 | P0 | 堵死 ADMIN 借 DeptPath 子树豁免 + DeptType=DEV 全权分支对他产品共有成员升权的攻击链 |
| TC-1125 | POST /api/user/update | H-R14-1 SuperAdmin 仍可把目标调入 DEV 部门(正向回归) | SuperAdmin + req.DeptId=<DEV 部门> |
200 OK;DB sys_user.deptId 落到 DEV 部门 id |
正常路径 | P0 | 保留 SuperAdmin 跨产品调度语义 |
| TC-1126 | POST /api/user/update | 产品 ADMIN 在自己子树内挪动 target(非 DEV)放行 | AdminCtx + DeptPath="/300/",req.DeptId=<非 DEV、DeptPath="/300/400/"> |
200 OK;DB 落盘新 deptId | 正常路径 | P0 | ADMIN 快速通道仅限"同子树内 + 非 DEV";保证合法子树内调动不被误伤 |
| TC-1127 | POST /api/user/bindRoles | L-R14-2 三路径(跨产品 / 已禁用 / 不存在)统一文案对比 | 同一 AdminCtx:分别构造 (A) 跨产品 roleId、(B) 本产品禁用 roleId、(C) 不存在 roleId |
三条 CodeError.Code()==400 全等;Error() 均为 "包含无效的角色ID";不依赖顺序 |
安全/Oracle | P0 | 阻断已认证调用方借文案差异枚举他产品 roleId 分布 / 启停状态 |
| TC-1170 | POST /api/user/update | 跨产品 P1 ADMIN 不得把共有 target 挪到 P1 子树之外的 NORMAL | AdminCtx(P1, DeptPath="/100/");target 是 P1+P2 共有成员;req.DeptId=<NORMAL, DeptPath="/200/"> |
CodeError.Code()==403,文案含 "无权将用户转移到该部门";DB sys_user.deptId 不变;sys_user.tokenVersion 不变 |
安全/跨产品升权 | P0 | 反回归:P1 ADMIN 原先被"豁免子树"时可把 P2 视角下的共有成员挪出 P2 ADMIN 管辖范围,形成 P2 侧不可管的孤儿 |
| TC-1171 | POST /api/user/update | target 从"DEV+Enabled 部门" → NORMAL 部门:sys_user.tokenVersion +1 | target seed 在 DeptType=DEV, Status=1 部门;req.DeptId=<NORMAL 部门> |
DB sys_user.tokenVersion 严格 +1;sys_user.deptId 落盘为新 deptId;两者同事务 |
安全/会话吊销 | P0 | 单人移出 DEV = 该用户 DEV 全权分支作废,必须把旧 access token 踢下线 |
| TC-1172 | POST /api/user/update | target 从"DEV+Enabled 部门" → deptId=0(移出部门树):sys_user.tokenVersion +1 | SuperAdmin + target 在 DEV+Status=1 部门;req.DeptId=Int64Ptr(0) |
sys_user.tokenVersion +1;sys_user.deptId=0 |
安全/会话吊销 | P0 | SuperAdmin 的"移出部门树"动作对 DEV 成员同样构成收窄;覆盖 newDeptId==0 分支 |
| TC-1173 | POST /api/user/update | NORMAL→NORMAL 不递增 tokenVersion | target 在 NORMAL 部门 A;req.DeptId=<NORMAL 部门 B> |
DB sys_user.tokenVersion 与初值严格相等;deptId 落盘为 B |
正向回归 | P0 | 非收窄方向不得把合法用户误踢下线;与 TC-1171 对偶 |
| 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-1267 | POST /api/user/detail | 超管不传 productCode → roleIds 含目标用户所有产品角色 | 超管 ctx + {"id":userId}(不传 productCode) |
含全量 roleIds(跨产品) | 正常路径 | P0 | userDetailLogic;超管无产品上下文时返回全量 |
| TC-1268 | POST /api/user/detail | 超管传 productCode → roleIds 只含该产品角色 | 超管 ctx + {"id":userId,"productCode":"test_product"} |
roleIds 仅含 test_product 下的角色 | 正常路径 | P0 | 超管显式指定产品时按产品过滤 |
| 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-1269 | POST /api/user/detail | 非超管不传 productCode → roleIds 只含 JWT context 产品角色 | AdminCtx("test_product") + {"id":userId}(不传 productCode) |
roleIds 仅含 test_product 下的角色;req.productCode 被忽略 | 正常路径 | P0 | 非超管始终用 JWT context productCode,req.productCode 无效 |
| TC-0184 | POST /api/user/bindRoles | 正常绑定(超管调用,显式传 productCode) | {"userId":1,"roleIds":[1,2],"productCode":"test_product"} |
code=0 | 正常路径 | P0 | TransactCtx;超管 JWT 无 productCode,需显式传入 |
| TC-0185 | POST /api/user/bindRoles | 用户不存在 | {"userId":9999,"roleIds":[1]} |
code=404, "用户不存在" | 存在性校验 | P0 | FindOne预检 |
| TC-0186 | POST /api/user/bindRoles | 清空角色(超管调用) | {"userId":1,"roleIds":[],"productCode":"test_product"} |
code=0 | 分支覆盖 | P1 | len==0 |
| TC-0187 | POST /api/user/bindRoles | 事务回滚 | 模拟失败 | 旧数据还原 | 事务验证 | P0 | TransactCtx |
| TC-0188 | POST /api/user/bindRoles | 角色不属于当前产品(超管调用) | {"userId":1,"roleIds":[otherId],"productCode":"test_product"} |
code=400, "包含无效的角色ID"(三路径折叠) | 安全 | P0 | L-R14-2:不再以独立文案暴露"跨产品"分支 |
| TC-0189 | POST /api/user/bindRoles | 角色已禁用(超管调用) | {"userId":1,"roleIds":[disabledId],"productCode":"test_product"} |
code=400, "包含无效的角色ID"(三路径折叠) | 安全 | P0 | L-R14-2:不再以独立文案暴露"已禁用"分支 |
| TC-0190 | POST /api/user/bindRoles | 角色不存在(超管调用) | {"userId":1,"roleIds":[9999],"productCode":"test_product"} |
code=400, "包含无效的角色ID" | 安全 | P0 | L-R14-2:与跨产品/禁用路径文案一致 |
| TC-0191 | POST /api/user/bindRoles | 非产品成员绑定角色被拒绝(超管调用) | {"userId":1,"roleIds":[],"productCode":"test_product"} 目标用户非当前产品成员 |
400 "不是当前产品的成员" | 安全 | P0 | 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 | Effect白名单 |
| TC-0197 | POST /api/user/setPerms | PermId不存在 | permId=99999 | code=400, "权限不存在" | 安全 | P0 | 校验PermId |
| TC-0198 | POST /api/user/setPerms | 权限不属于当前产品 | permId属于其他产品 | code=400, "权限不属于当前产品" | 安全 | P0 | 校验权限归属 |
| TC-0199 | POST /api/user/setPerms | 非产品成员设置权限被拒绝 | 目标用户非当前产品成员 | 400 "不是当前产品的成员" | 安全 | P0 | SetUserPerms |
| TC-0200 | POST /api/user/updateStatus | 正常冻结 | {"id":普通用户,"status":2} |
code=0;DB sys_user.status=2;DB sys_user.tokenVersion = before+1(冻结方向吊销) |
正常路径/会话吊销 | P0 | updateUserStatusLogic;UpdateStatus 底层 SET tokenVersion = tokenVersion + 1 与 jwtauthMiddleware 的 tokenVersion 比对契约对齐,冻结瞬间立刻使已签发 access/refresh token 失效 |
| TC-0201 | POST /api/user/updateStatus | 正常解冻(Disabled→Enabled) | {"id":普通用户,"status":1} |
code=0;DB sys_user.status=1;DB sys_user.tokenVersion = before+1(解冻方向同样 +1) |
正常路径/会话吊销 | P0 | UpdateStatus 的 SQL 无条件 +1,不论方向;解冻也 +1 是刻意设计,堵住"冻结→UD 缓存残留→解冻瞬间旧 access token 复活"的极端路径。严禁回滚成"条件 +1 / 仅冻结 +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 | FindListByProductMembers数据隔离 |
| TC-0206 | POST /api/user/list | userList-非超管未指定productCode被拒绝 | ctx=ADMIN(非超管), productCode="" | 403 "非超管用户必须指定产品编码" | 安全 | P0 | 强制productCode |
| TC-0207 | POST /api/user/list | userList-非超管使用错误productCode被拒绝 | ctx=ADMIN, productCode!=ctx.ProductCode | 403 | 安全 | P0 | productCode一致性校验 |
| TC-0208 | POST /api/user/bindRoles | bindRoles-permsLevel越权拒绝 | ctx=ADMIN(MinPermsLevel=50), role.permsLevel=1 | 403 "不能分配权限级别高于自身的角色" | 安全 | P0 | 角色权限级别越权防护 |
| TC-0209 | POST /api/user/bindRoles | bindRoles-超管可分配任意级别角色 | ctx=SuperAdmin, role.permsLevel=1 | 绑定成功 | 正常路径 | P0 | 超管无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 |
| TC-0711 | POST /api/user/* | ADMIN 调用者豁免 permsLevel 校验 | caller=ADMIN, MinPermsLevel=math.MaxInt64, 目标角色任意 permsLevel | 成功绑定 | 正常路径 | P0 | ADMIN/DEVELOPER 不再受 permsLevel 约束 |
| TC-0712 | POST /api/user/* | DEVELOPER 调用者豁免 permsLevel 校验 | caller=DEVELOPER, 目标角色任意 permsLevel | 成功绑定 | 正常路径 | P0 | DEVELOPER 豁免 |
| TC-0713 | POST /api/user/* | MinPermsLevel=MaxInt64 的 MEMBER 不被误阻断 | caller=MEMBER, MinPermsLevel=math.MaxInt64(未持角色) | 不触发 "不能分配权限级别高于自身" 错误 | 分支覆盖 | P0 | sentinel 值语义 |
| TC-0734 | POST /api/user/* | 产品已禁用 | product.status=2 | 400 "产品已被禁用,无法设置权限" | 安全 | P0 | 新增 product.Status 校验 |
| TC-0735 | POST /api/user/* | 产品不存在 | 虚构 productCode | 404 "产品不存在" | 错误路径 | P0 | FindOneByCode ErrNotFound |
| TC-0743 | POST /api/user/* | 普通 MEMBER 给自己授权 | MemberCtx, targetUserId=self | 403 "需要产品管理员权限",DB 中 userperm 无任何写入 | 安全 | P0 | RequireProductAdminFor 前置拦截 self-escalation |
| TC-0744 | POST /api/user/* | DEVELOPER 调用者(非 ADMIN)操作他人 | DeveloperCtx, targetUserId=other | 403 "需要产品管理员权限",DB 无写入 | 安全 | P0 | DEVELOPER 也必须被拦截(非仅 self 场景) |
| TC-0745 | POST /api/user/* | 同产品 ADMIN 操作合法 MEMBER 目标 | AdminCtx, targetUserId=member | 200 OK,userperm 插入成功 | 正常路径 | P0 | 修复后 admin 正向通路仍通畅 |
| TC-0772 | POST /api/user/* | 状态无实际变化(已启用→再启用) | user.Status=1, req.Status=1 | 返回成功,tokenVersion 不变,用户不被踢下线 | 功能 | P0 | user.Status == req.Status 时跳过写操作 |
| TC-0773 | POST /api/user/* | 状态实际变化(启用→冻结) | user.Status=1, req.Status=2 | 成功,tokenVersion+1,用户被踢下线 | 正常路径 | P0 | 真实变更时正常递增 |
| TC-0786 | POST /api/user/* | SetUserPerms 调用后 req.Perms 不变 | 带重复 permId 的请求 | 调用后 req.Perms 长度与调用前一致 | 代码质量 | P1 | 使用局部变量去重 |
| TC-0787 | POST /api/user/* | BindRoles 调用后 req.RoleIds 不变 | 带重复 roleId 的请求 | 调用后 req.RoleIds 长度与调用前一致 | 代码质量 | P1 | 使用局部变量去重 |
| TC-0788 | POST /api/user/* | BindRolePerms 调用后 req.PermIds 不变 | 带重复 permId 的请求 | 调用后 req.PermIds 长度与调用前一致 | 代码质量 | P1 | 使用局部变量去重 |
| TC-0813 | POST /api/user/* | MEMBER 调用者给他人赋予 "与自己 permsLevel 相同" 的角色 | caller level=50, role level=50 | 403(等级不允许);DB 的 sys_user_role 关系无变化 | 安全/越权 | P0 | GuardRoleLevelAssignable 的 >= 防自等升权 |
| TC-0988 | POST /api/user/* | FindByIds 前置校验通过(装饰器撒谎 status=1)但 DB 实际 status=2 | 正常请求 | 409 "部分权限在提交时已被禁用",sys_user_perm 必须 0 行脏数据 |
对抗/一致性 | P0 | COUNT 复核失效 → 立即可见 |
| TC-0989 | POST /api/user/* | 全部真实 Enabled 的正向基线 | 两条 perm + ALLOW/DENY 各一 | 2 行落盘 | 正向 | P0 | 防止误杀 |
| TC-0991 | POST /api/user/* | 自看 | caller.UserId == target.Id | 原样返回 Email/Phone/Remark | 正向/回归 | P0 | 业务契约已固定为"全员原值";守护未来误加脱敏不伤 self-view |
| TC-0992 | POST /api/user/* | SuperAdmin 看任何人 | caller.IsSuperAdmin | 原样返回 Email/Phone/Remark | 正向/回归 | P0 | 同上契约;守护未来误加脱敏不伤 SuperAdmin 分支 |
| TC-1011 | POST /api/user/* | 他人先冻结后本轮解冻 | 先跑 Update → UpdateTime 推进,本轮仍持旧 updateTime 直冲 model | model 层 ErrUpdateConflict;Logic happy path 解冻成功且 updateTime 推进 |
并发/CAS | P0 | CAS 失败路径 + 正向回归 |
| TC-1012 | POST /api/user/* | Logic 层错误映射 | model 层强制 ErrUpdateConflict |
映射为 response.ErrConflict(409, "数据已被其他操作修改,请刷新后重试") |
契约 | P1 | 文案与 code 对齐 |
| TC-1027 | POST /api/user/* | 登录时用户在 productCode 下非成员 |
用户在 productCode 下非成员 |
CodeError.Code()==403;文案 "您不是该产品的有效成员" |
安全/Oracle | P0 | 与"禁用成员"同文案 |
| TC-1028 | POST /api/user/* | 登录时用户成员资格 Status=Disabled |
用户成员资格 Status=Disabled |
同上 | 安全/Oracle | P0 | 两条分支合并成一条路径 |
| TC-1078 | POST /api/user/* | BindRoles 与 DeleteRole 并发 6 轮 | - | 每轮新建 user+member+role,两 goroutine 同起 | 终态二选一:(a) 两端都成功(BindRoles 先 → DeleteRole 级联把 UserRole 一并清掉),(b) DeleteRole 先成功 + BindRoles 400 "包含无效的角色ID"(事务外 FindByIds 抑或事务内 LockRolesForShareTx 的 sqlx.ErrNotFound 都折叠到同一文案);任何一轮都不得出现 "sys_role 已删、sys_user_role 仍有 (userId, roleId)" 的 orphan | P0 | 事务内 S 锁 vs DeleteRole 末尾的 sys_role[X] 锁之间的锁链;兼测 L-R14-2 统一文案 |
| TC-1104 | POST /api/user/setPerms | 非 ADMIN caller + 不存在的 userId 必须 403(而不是 404)以消除 userId 枚举 oracle | MemberCtx + UserId=999999999 |
CodeError.Code()==403,文案含 "仅超级管理员或该产品的管理员";DB sys_user_perm 无写入 |
安全/枚举 | P0 | 反回归:RequireProductAdminFor 必须先于 SysUserModel.FindOne(userId) |
| TC-1105 | POST /api/user/setPerms | DENY TOCTOU:预检读 member=MEMBER 通过,事务内 S 锁快照返回 ADMIN → 400 并回滚 | FindOneByProductCodeUserId → MEMBER;装饰 FindOneForShareTx → ADMIN 返回 |
CodeError.Code()==400,文案含 "产品管理员或开发者";sys_user_perm 无脏 DENY 行 |
对抗/一致性 | P0 | 若 L-R13-2 事务内复核被拆除,脏 DENY 行会落盘("能写永不生效") |
| TC-1106 | POST /api/user/setPerms | ALLOW-only 请求 不得 走 FindOneForShareTx S 锁路径(避免把热路径退化到锁链) |
Perms=[{PermId, ALLOW}];装饰 member model 断言 FindOneForShareTx 调用数=0 |
正常落盘 1 行 ALLOW;mock 上 FindOneForShareTx 未被调用 |
契约/性能 | P1 | 防把 S 锁挂到全量路径导致并发降级 |
| TC-1164 | POST /api/user/detail | 产品 ADMIN 看同产品他人:返回完整 Email/Phone/Remark | AdminCtx(P1);target 是 P1 MEMBER,带完整 Email/Phone/Remark |
响应中 Email/Phone/Remark 与 DB 原值严格相等 |
正向回归 | P0 | ADMIN 是 PII 最小授权白名单之一;守护默认脱敏上线后不伤 ADMIN 视角 |
| TC-1165 | POST /api/user/detail | 产品 DEVELOPER 看同产品他人:返回完整 Email/Phone/Remark | DeveloperCtx(P1);target 是 P1 MEMBER |
响应中 Email/Phone/Remark 与 DB 原值严格相等 |
正向回归 | P0 | DEVELOPER 被纳入 PII 白名单;与 ADMIN 同口径 |
| TC-1166 | POST /api/user/detail | 产品 MEMBER 看同产品他人:Email/Phone/Remark 必须为空字符串 | MemberCtx(P1);target 是 P1 的其他成员 |
Email==""、Phone==""、Remark=="";nickname/deptId 等非 PII 字段仍然返回;DB 原值未被改动 |
安全/最小授权 | P0 | 普通 MEMBER 视角不得窥视他人 PII;避免 PII 明文外泄 |
| TC-1167 | POST /api/user/list | 产品 ADMIN 列表视角:全部成员 PII 原值返回 | AdminCtx(P1);两个带 PII 的目标成员 |
列表响应里每条 Email/Phone/Remark 与 DB 严格相等 |
正向回归 | P0 | 列表入口与 detail 口径必须一致,否则 ADMIN 真实排障能力塌方 |
| TC-1168 | POST /api/user/list | 产品 DEVELOPER 列表视角:全部成员 PII 原值返回 | DeveloperCtx(P1) |
同上 | 正向回归 | P0 | 与 detail TC-1165 对偶 |
| TC-1169 | POST /api/user/list | 产品 MEMBER 列表视角:其他成员 PII 必须被置空 | MemberCtx(P1);列表包含 2 个他人 PII |
他人 Email/Phone/Remark 均为空字符串;其他字段(id / nickname / deptId / memberType)保持完整 | 安全/最小授权 | P0 | 列表是 PII 最大暴露面;必须与 TC-1166 共同守护 |
POST /api/user/userPerms| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1257 | POST /api/user/userPerms | 超管查任意用户权限覆盖 | SuperAdminCtx + {"userId": targetId};目标用户有 1 条 ALLOW 记录 |
code=0;perms 含该 ALLOW 项 |
正常路径 | P0 | SuperAdmin 不受产品限制;FindByUserIdForProduct 正向 |
| TC-1258 | POST /api/user/userPerms | 用户查询自己的权限覆盖 | MemberCtx + {"userId": self};自身有 ALLOW 和 DENY 各 1 条 |
code=0;perms 含 2 项(ALLOW + DENY) |
正常路径/self | P0 | isSelf 分支,跳过 RequireProductAdminFor + CheckManageAccess |
| TC-1259 | POST /api/user/userPerms | 产品 ADMIN 查同产品 MEMBER 的权限覆盖 | AdminCtx + {"userId": memberId} |
code=0;perms 返回目标用户在当前产品下的覆盖项 |
正常路径 | P0 | RequireProductAdminFor 通过 + CheckManageAccess 等级检查通过 |
| TC-1260 | POST /api/user/userPerms | 产品 ADMIN 查同级 ADMIN 被拒绝 | AdminCtx + {"userId": otherAdminId} |
code=403 | 安全/越权 | P0 | CheckManageAccess 等级相等不允许 |
| TC-1261 | POST /api/user/userPerms | 普通 MEMBER 查他人被拒绝 | MemberCtx + {"userId": otherUserId} |
code=403;文案含 "仅超级管理员或该产品的管理员" | 安全/枚举防护 | P0 | RequireProductAdminFor 前置拦截,防止 MEMBER 枚举 userId |
| TC-1262 | POST /api/user/userPerms | 非 ADMIN 查不存在的 userId 必须 403(消除枚举 oracle) | MemberCtx + {"userId": 999999999} |
code=403;文案含 "仅超级管理员或该产品的管理员";DB 无任何读取可见信息 | 安全/枚举 | P0 | RequireProductAdminFor 先于实体读取(审计 L-R13-1) |
| TC-1263 | POST /api/user/userPerms | ADMIN 查不是当前产品成员的用户 | AdminCtx + {"userId": userNotInProduct} |
code=403 | 安全 | P0 | CheckManageAccess 检测目标非成员 |
| TC-1264 | POST /api/user/userPerms | 无 JWT 请求被拒绝 | 无 Authorization Header | code=401 | 安全 | P0 | JwtAuth 中间件拦截 |
| 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 | 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 | 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 |
| TC-0723 | POST /api/member/* | 移除产品唯一 ADMIN | 1 个启用 ADMIN | 400 "不能移除该产品的最后一个管理员",ADMIN 仍存在 | 安全 | P0 | CountActiveAdmins<=1 拒绝 |
| TC-0724 | POST /api/member/* | 有 2 个 ADMIN 时移除其一 | 2 个 ADMIN | 成功删除 1 个,另一个保留 | 正常路径 | P0 | 非 last-admin 场景放行 |
| TC-0725 | POST /api/member/* | 降级产品唯一 ADMIN 为 MEMBER | 1 个启用 ADMIN | 400 "不能降级该产品的最后一个管理员",MemberType 不变 | 安全 | P0 | updateMember 同逻辑 |
| TC-0726 | POST /api/member/* | 有 2 个 ADMIN 时降级其一 | 2 个 ADMIN | 成功降级为 MEMBER | 正常路径 | P0 | 非 last-admin 允许 |
| TC-0727 | POST /api/member/* | 2 个 ADMIN 但只有 1 个启用,降级该启用 ADMIN | ADMIN(status=1)+ADMIN(status=2) | 400 "不能降级该产品的最后一个管理员" | 安全/边界 | P0 | CountActiveAdmins 只计 status=1 |
| TC-0728 | POST /api/member/* | 移除非 ADMIN(MEMBER) | 1 个 MEMBER | 成功删除,不受 last-admin 保护 | 正常路径 | P1 | 仅 ADMIN 触发校验 |
| TC-0729 | POST /api/member/* | 对禁用产品 addMember | product.status=2 | 400 "产品已被禁用,无法添加成员" | 安全 | P0 | addMemberLogic 新增 product.Status 校验 |
| TC-0760 | POST /api/member/* | 保持 memberType=ADMIN 但 status 改为 Disabled | 产品唯一 ADMIN,{"memberType":"ADMIN","status":2} |
400 "不能降级或禁用该产品的最后一个管理员" | 安全 | P0 | wasActiveAdmin && !willBeActiveAdmin 覆盖 status 变化 |
| TC-0761 | POST /api/member/* | 同时降级 + 禁用唯一 ADMIN | {"memberType":"MEMBER","status":2} |
400 同上 | 安全 | P0 | memberType + status 同时变化 |
| TC-0762 | POST /api/member/* | 有 2 个 ADMIN 时禁用其一 | 2 个启用 ADMIN | 成功,目标 status=2 | 正常路径 | P0 | 非 last-admin 场景放行 |
| TC-1270 | POST /api/member/userProducts | 超管查询他人产品列表 | SuperAdminCtx + userId=目标用户 | list 含该用户加入的产品,productCode/productName/memberType/status 正确 | 正常路径 | P0 | userProductsLogic 集成测试 |
| TC-1271 | POST /api/member/userProducts | 本人查询自己的产品列表 | CustomCtx(userId=X) + req.UserId=X | 正常返回,list 含自己加入的产品 | 正常路径 | P0 | 本人豁免 |
| TC-1272 | POST /api/member/userProducts | 非超管查询他人产品列表 | CustomCtx(userId=20) + req.UserId=99 | code=403,"无权查看他人" | 安全/IDOR | P0 | 防止枚举他人产品归属 |
| TC-1273 | POST /api/member/userProducts | 用户未加入任何产品 | SuperAdminCtx + userId=无成员用户 | list=[] | 边界 | P1 | 空列表正常返回 |
| TC-1274 | POST /api/member/userProducts | 超管查询多产品(mock) | FindByUserId 返回 2 条 | list 长度=2,productName 正确 | 正常路径 | P0 | mock 测试 |
| TC-1275 | POST /api/member/userProducts | 本人查询自己(mock) | CustomCtx(userId=20) + req.UserId=20 | 正常返回 | 正常路径 | P0 | mock 测试 |
| TC-1276 | POST /api/member/userProducts | 非超管查他人(mock) | CustomCtx(userId=20) + req.UserId=99 | code=403 | 安全/IDOR | P0 | mock 测试 |
| TC-1277 | POST /api/member/userProducts | DB 错误透传(mock) | FindByUserId 返回 error | 返回该 error | 异常路径 | P0 | mock 测试 |
| TC-1278 | POST /api/member/userProducts | 产品查询失败跳过(mock) | FindOneByCode 对某条返回 error | 跳过该条,其余正常返回 | 容错 | P1 | mock 测试 |
| TC-1279 | POST /api/member/userProducts | 空列表(mock) | FindByUserId 返回空 | list=[] | 边界 | P1 | mock 测试 |
| TC-0763 | POST /api/member/* | 移除活跃 ADMIN(事务内用 locked 数据判断) | 唯一 ADMIN | 400 "不能移除该产品的最后一个管理员" | 安全 | P0 | locked.MemberType 替代事务外 member.MemberType |
| TC-0764 | POST /api/member/* | 移除非 ADMIN 不触发 last-admin 校验 | MEMBER 身份 | 成功移除 | 正常路径 | P0 | locked.MemberType != ADMIN 跳过检查 |
| TC-0789 | POST /api/member/* | caller.MemberType="" 调用 CheckMemberTypeAssignment | 空 memberType 的 caller | 403 "缺少产品成员上下文" | 安全 | P0 | 显式分支替代 sentinel 值 |
| TC-0940 | POST /api/member/* | Product ADMIN 拉跨部门树的人 | caller path=/100/,target path=/200/201/ | 403,错误含 "其他部门" | 权限 | P0 | ADMIN 不再绕过部门树 |
| TC-0941 | POST /api/member/* | Product ADMIN 在自己部门树内 | target 在 /100/101/ | 放行 | 正常路径 | P0 | 正值域 |
| TC-0942 | POST /api/member/* | SuperAdmin 跨一切部门 | caller superAdmin | 放行,不查 dept | 正常路径 | P0 | 短路 |
| TC-0943 | POST /api/member/* | 自己加自己 | target.Id == caller.UserId | 放行 | 正常路径 | P0 | self-bypass |
| TC-0944 | POST /api/member/* | caller 没有部门(DeptId=0 或 DeptPath="") | 非超管 | 403 | 异常路径 | P0 | 无部门 caller 必须拒绝 |
| TC-0945 | POST /api/member/* | target 无部门(DeptId=0) | 非自己 | 403 | 异常路径 | P0 | 目标无部门视为不可纳管 |
| TC-0946 | POST /api/member/* | ctx 无 caller | context.Background() | 401 | 权限 | P0 | H-3 |
| TC-0947 | POST /api/member/* | dept.FindOne 报错 | mock 返回 err | 403(fail-close,文案不泄细节) | 容错 | P0 | H-3 |
| TC-0948 | POST /api/member/* | target 为 nil | pass nil | 400 BadRequest | 契约 | P0 | H-3 |
| TC-0949 | POST /api/member/* | AddMember 集成:跨部门被拒 | 真实 DB,Product ADMIN 拉树外 | 403 + 不写 sys_product_member | 集成 | P0 | H-3 端到端 |
| TC-0950 | POST /api/member/* | AddMember 集成:target=SuperAdmin | superAdmin 被作为 MEMBER 加入 | 403 "超级管理员" + 不写 sys_product_member | 安全 | P0 | 超管防混入 |
| TC-1055 | POST /api/member/* | MemberType==nil && Status==nil → 400 "请至少提供一个要更新的字段" |
UpdateMemberReq{productCode, userId} 两字段都不传 |
CodeError.Code()==400,msg 含 "至少提供一个要更新的字段" |
契约 | P0 | nil 判定入口 |
| TC-1056 | POST /api/member/* | 只传 Status,MemberType 保持不变 |
{Status: Int64Ptr(2)},原 member.MemberType="ADMIN" |
DB:memberType 仍是 "ADMIN",status=2 |
契约 | P0 | 部分更新语义 |
| TC-1057 | POST /api/member/* | 只传 MemberType,Status 保持不变 |
{MemberType: StrPtr("DEVELOPER")},原 member.Status=1 |
DB:memberType 变为 "DEVELOPER",status 仍是 1 |
契约 | P0 | 镜像对称 |
| TC-1058 | POST /api/member/* | DEVELOPER → 只改 Status 时跳过"分配校验" | 只传 Status=1,member.MemberType="DEVELOPER" |
不走分配校验分支;memberType 保持 DEVELOPER;状态落盘为 1 |
契约/性能 | P1 | DEVELOPER 分支被误挂会立即红 |
| TC-1059 | POST /api/member/* | 非法 Status 值(例如 7)→ 400 | {Status: Int64Ptr(7)} |
CodeError.Code()==400 |
边界 | P0 | Status 枚举防御 |
| TC-1060 | POST /api/member/* | 完全 no-op(传进来的值与 DB 现值相同)→ 返 nil 且 updateTime 不前进 | 传 {Status: Int64Ptr(member.Status)} |
err==nil;DB updateTime 保持原值 | 契约/幂等 | P1 | MySQL 行为——值未变 RowsAffected=0,不被误升格为冲突 |
| TC-1130 | POST /api/member/update | 降级 ADMIN→MEMBER:sys_user.tokenVersion +1 | seed ADMIN(产品内有其他启用 ADMIN 以绕过 last-admin),{MemberType:"MEMBER"} |
DB sys_user.tokenVersion 严格 +1;sys_product_member.memberType=="MEMBER";两者原子同事务落盘 |
安全/会话吊销 | P0 | 哪怕 UserDetailsLoader.Del 失败,中间件 claims.TokenVersion != ud.TokenVersion 也会 401 旧 token |
| TC-1131 | POST /api/member/update | 禁用启用成员:sys_user.tokenVersion +1 | seed MEMBER(Status=1),{Status:2} |
tokenVersion +1;sys_product_member.status==2 |
安全/会话吊销 | P0 | 冻结的成员不应继续持有生效 token |
| TC-1132 | POST /api/member/update | 降级 DEVELOPER→MEMBER:sys_user.tokenVersion +1 | seed DEVELOPER,{MemberType:"MEMBER"} |
tokenVersion +1;memberType=="MEMBER" |
安全/会话吊销 | P0 | DEVELOPER 同样算"特权身份",降为 MEMBER 必须视作"权限收窄" |
| TC-1133 | POST /api/member/update | 升权 MEMBER→ADMIN:sys_user.tokenVersion 不变 | seed MEMBER,{MemberType:"ADMIN"} |
tokenVersion 与之前完全相等;memberType=="ADMIN" |
正向回归 | P0 | 升权不构成对被管理方的实际损害,不应把目标用户误踢下线 |
| TC-1134 | POST /api/member/update | 重启用 Disabled→Enabled:sys_user.tokenVersion 不变 | seed MEMBER(Status=2),{Status:1} |
tokenVersion 不变;status==1 |
正向回归 | P0 | 解冻不需要递增,维持既有会话有效 |
| TC-1135 | POST /api/member/update | 降级事务失败(last-admin 400):sys_user.tokenVersion 不变 | 唯一启用 ADMIN,{MemberType:"MEMBER"} |
返回 400 "最后一个管理员";DB sys_user.tokenVersion 严格等于初值;sys_product_member 行内容也保持原状 |
事务回滚 | P0 | 关键:tokenVersion 增量必须与 member 更新在同一事务里;业务失败不得污染 tokenVersion |
| TC-1136 | POST /api/member/update | no-op 更新不递增 tokenVersion | 传进来的 memberType/status 与 DB 现值相同 | tokenVersion 不变;早退分支不进事务 |
正向/幂等 | P0 | locked.MemberType==nextType && locked.Status==nextStatus 早退 |
| TC-1137 | POST /api/member/update | 降级成功后 post-commit 失效 sysUser id-key / username-key 两把缓存 | seed ADMIN→降级 MEMBER,先预热 FindOne(id) + FindOneByUsername(name) 把缓存灌入 Redis |
事务成功返回后两把 cache key 均被 DEL;下一次 FindOne 取到 DB 中递增后的 tokenVersion | 缓存一致性 | P0 | UD loader 下次 cache-miss 重建时不得从旧 sysUser 缓存把 tokenVersion 抹回 |
| TC-1107 | POST /api/member/add | 非 ADMIN caller + 不存在的 productCode:必须 403(不是 404)以消除 productCode 枚举 oracle | MemberCtx("other_product") + ProductCode="does_not_exist" |
CodeError.Code()==403(不是 404 "产品不存在");DB 无 sys_product_member 新增 |
安全/枚举 | P0 | 反回归:RequireProductAdminFor 必须先于 SysProductModel.FindOneByCode |
| TC-1108 | POST /api/member/add | 非 ADMIN caller + 非法 MemberType:返回 403 而不是 400(权限优先于字面校验) |
MemberCtx + MemberType="INVALID" |
CodeError.Code()==403(不是 400 "无效的成员类型") |
安全/枚举 | P0 | 防通过 400/404 差分探测产品/用户存在性 |
| TC-1109 | POST /api/member/add | 超管 + 非法 MemberType:正常 400 |
SuperAdminCtx + MemberType="INVALID"(产品存在) |
CodeError.Code()==400,文案含 "无效的成员类型" |
正向回归 | P0 | 确认权限通过后仍走字面 400 检查,不误伤合法路径 |
| TC-1162 | POST /api/member/remove | 移除成员后被移除用户 sys_user.tokenVersion 必须 +1 | seed 2 个 ADMIN 绕过 last-admin,{id: targetMemberId} |
DB sys_user.tokenVersion 严格 +1;sys_product_member 行被删;post-commit 产品成员缓存失效 |
安全/会话吊销 | P0 | 镜像 updateMember 的 tokenVersion 契约,避免被踢出产品后旧 access token 仍能访问该产品 |
| TC-1163 | POST /api/member/remove | 移除失败(last-admin 场景)时 tokenVersion 绝不得 +1 | 唯一启用 ADMIN,{id: adminMemberId} |
返回 400 "不能移除该产品的最后一个管理员";DB sys_user.tokenVersion 与初值严格相等;sys_product_member 行仍在 |
事务回滚 | P0 | tokenVersion 增量必须与 member 删除同事务;失败路径不得污染 tokenVersion 让合法会话被无故踢下线 |
POST /api/captcha/get| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1208 | POST /api/captcha/get | 正常获取(默认宽高) | {} |
200, id 非空, base64image 非空 | 正常路径 | P0 | captchaLogic 默认参数 |
| TC-1209 | POST /api/captcha/get | 自定义宽高 | {"width":200,"height":80} |
200, 返回数据正常 | 正常路径 | P1 | 自定义尺寸分支 |
| TC-1210 | POST /api/captcha/get | 宽高为 0 或负数,退化为默认值 | {"width":0,"height":-1} |
200, 不报错,使用默认宽高 | 边界 | P1 | 负值/零值兜底 |
| TC-1252 | VerifyCaptcha | 正确码消费后不可重用 | Set(id, code) → Verify(id, code) ×2 | 首次 true,二次 false | 单元/安全 | P0 | 一次性消费语义防重放 |
| TC-1253 | VerifyCaptcha | 错误码不消费 | Set(id, "5678") → Verify(id, "0000") | false | 单元 | P0 | 错误码不影响 store 状态 |
| TC-1254 | VerifyCaptcha | 不存在的 id | Verify("non_existent", "1234") | false | 单元/边界 | P0 | 无条目直接拒绝 |
POST /api/capjs/endpoint| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1211 | POST /api/capjs/endpoint | cap.js 已启用 | Capjs.Enable=1 + EndpointURL 非空 | 200, data=EndpointURL | 正常路径 | P0 | capEndpointLogic Enable=1 |
| TC-1212 | POST /api/capjs/endpoint | cap.js 未启用 | Capjs.Enable=0 | 200, data="" | 分支覆盖 | P0 | capEndpointLogic Enable!=1 |
| TC-1255 | POST /api/capjs/endpoint | cap.js 启用但 EndpointURL 为空 | Capjs.Enable=1, EndpointURL="" | 200, data="" | 边界 | P0 | 启用但 URL 缺失时兜底返回空 |
| TC-1256 | POST /api/capjs/endpoint | cap.js 启用但 Key 为空 | Capjs.Enable=1, Key="" | 200, data="" | 边界 | P0 | 启用但 Key 缺失时兜底返回空 |
POST /api/auth/login/cap| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1219 | POST /api/auth/login/cap | cap.js 未启用时调用 | Capjs.Enable=0 | 400 "当前未启用人机验证" | 前置校验 | P0 | Enable!=1 分支 |
| TC-1220 | POST /api/auth/login/cap | capToken 为空 | {"capToken":""} |
400 "人机验证不能为空" | 输入校验 | P0 | 空 token 校验 |
| TC-1221 | POST /api/auth/login/cap | capToken 无效(远端校验失败) | mock server 返回 {"success":false} |
400 "人机验证失败" | 异常路径 | P0 | 远端验证失败分支 |
| TC-1222 | POST /api/auth/login/cap | capToken 有效 + 正常登录 | mock server 返回 {"success":true} + 有效凭证 |
200 + accessToken/refreshToken | 正常路径 | P0 | 全路径(mock HTTP 服务端) |
| TC-1223 | POST /api/auth/login/cap | capToken 有效 + 密码错误 | mock 成功 + 错误密码 | 401 | 异常路径 | P0 | 验证通过后密码校验 |
| TC-1224 | POST /api/auth/login/cap | capToken 有效 + 超管被拒绝 | mock 成功 + 超管用户 | 403 | 安全 | P0 | 超管不允许产品端登录 |
POST /api/auth/adminLogin/cap| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1225 | POST /api/auth/adminLogin/cap | cap.js 未启用时调用 | Capjs.Enable=0 | 400 "当前未启用人机验证" | 前置校验 | P0 | Enable!=1 分支 |
| TC-1226 | POST /api/auth/adminLogin/cap | capToken 为空 | {"capToken":""} |
400 "人机验证不能为空" | 输入校验 | P0 | 空 token 校验 |
| TC-1227 | POST /api/auth/adminLogin/cap | capToken 有效 + managementKey 无效 | mock 成功 + 错误 managementKey | 401 "managementKey无效" | 安全 | P0 | managementKey 校验 |
| TC-1228 | POST /api/auth/adminLogin/cap | capToken 有效 + 超管正常登录 | mock 成功 + 超管凭证 + 正确 managementKey | 200 + accessToken/refreshToken | 正常路径 | P0 | 全路径 |
| TC-1229 | POST /api/auth/adminLogin/cap | capToken 有效 + 非超管被拒绝 | mock 成功 + 普通用户 + 正确 managementKey | 401 | 安全 | P0 | 非超管拒绝管理后台登录 |
POST /api/auth/updateInfo| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1230 | POST /api/auth/updateInfo | 未登录 | ctx 无 UserDetails | 401 "未登录" | 安全 | P0 | caller==nil |
| TC-1231 | POST /api/auth/updateInfo | 所有字段为 nil | {} |
400 "至少需要修改一个字段" | 输入校验 | P0 | 全 nil 校验 |
| TC-1232 | POST /api/auth/updateInfo | 正常更新 nickname | {"nickname":"new_nick"} |
200, DB 中 nickname 已更新 | 正常路径 | P0 | 单字段更新 |
| TC-1233 | POST /api/auth/updateInfo | 正常更新 avatar | {"avatar":"https://..."} |
200, DB 中 avatar 已更新 | 正常路径 | P0 | avatar 更新 |
| TC-1234 | POST /api/auth/updateInfo | 正常更新 email + phone | {"email":"[email protected]","phone":"138..."} |
200, DB 中 email/phone 更新 | 正常路径 | P0 | 多字段更新 |
| TC-1235 | POST /api/auth/updateInfo | nickname 超过 64 字符 | {"nickname":"x*65"} |
400 "昵称长度不能超过64个字符" | 边界 | P0 | 长度校验 |
| TC-1236 | POST /api/auth/updateInfo | avatar 超过 255 字符 | {"avatar":"a*256"} |
400 "头像地址长度不能超过255个字符" | 边界 | P0 | 长度校验 |
| TC-1237 | POST /api/auth/updateInfo | email 超过 64 字符 | {"email":"e*65"} |
400 "邮箱长度不能超过64个字符" | 边界 | P0 | 长度校验 |
| TC-1238 | POST /api/auth/updateInfo | phone 超过 32 字符 | {"phone":"1*33"} |
400 "手机号长度不能超过32个字符" | 边界 | P0 | 长度校验 |
| TC-1239 | POST /api/auth/updateInfo | 并发更新冲突 | 两 session 用相同旧 updateTime 提交 | 第二个返回错误 ErrUpdateConflict | 并发 | P0 | 乐观锁 WHERE updateTime=? |
| TC-1240 | POST /api/auth/updateInfo | 更新后 UserDetails 缓存失效 | 更新 nickname 后 Load | 再次 Load 应读到新值 | 缓存一致性 | P0 | InvalidateProfileCache |
POST /api/minio/upload| TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1242 | POST /api/minio/upload | fileType 为空 | multipart 无 fileType 字段 | 400 "fileType is required" | 输入校验 | P0 | handler fileType 校验 |
| TC-1243 | POST /api/minio/upload | fileType 不在配置中 | fileType=unknown_type |
400 "fileType not configured" | 输入校验 | P0 | 配置映射查找失败 |
| TC-1244 | POST /api/minio/upload | Content-Type 不在白名单中 | fileType=avatar, Content-Type=application/zip |
400 "invalid contentType" | 安全 | P0 | AllowedContentTypes 白名单 |
| TC-1248 | POST /api/minio/upload | parseDir 模板替换 {yyyy}/{mm}/{dd} | "avatar/{yyyy}/{mm}/{dd}" |
路径包含当前日期 | 单元测试 | P0 | parseDir 逻辑 |
| TC-1249 | POST /api/minio/upload | handler 缺少 file 字段 | multipart 仅有 fileType 无 file | 400 | 输入校验 | P0 | FormFile 解析失败 |
| 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 |
| 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 | 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 | 实时查DB |
| TC-0253 | VerifyToken | 非成员token返回Invalid | user非产品成员 | Valid=false | 安全 | P0 | 实时查成员状态 |
| TC-0254 | VerifyToken | 返回实时MemberType和Perms | DB中ADMIN+自定义权限 | 返回实时数据而非token中旧数据 | 安全 | P0 | 实时数据 |
| 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认证前置 |
| TC-0700 | gRPC PermService | 冻结用户 (Status=Disabled) | GetUserPerms 请求冻结账号 | gRPC PermissionDenied,msg 含"冻结" | 安全 | P0 | 对齐 VerifyToken 的 StatusEnabled 判定 |
| TC-0701 | gRPC PermService | 非产品成员 | 启用用户但非目标产品成员 | gRPC PermissionDenied,msg 含"成员" | 安全 | P0 | MemberType=="" 拒绝 |
| TC-0702 | gRPC PermService | DEV 部门但产品成员被禁用 | dept.DeptType=DEV & member.Status=Disabled | gRPC PermissionDenied | 安全 | P0 | DEV 部门不再旁路已禁用成员校验 |
| TC-0703 | gRPC PermService | 启用 ADMIN 成员(正向回归) | 正常启用成员,产品存在权限 | 成功,返回 MemberType=ADMIN 且 Perms 含已配置项 | 正常路径 | P0 | H-2 修复后正常路径未被误伤 |
| TC-0704 | gRPC PermService | Loader 层:DEV 部门 + 产品成员禁用 | DEV 启用,member.Status=Disabled | UserDetails.MemberType="",Perms=[] | 安全 | P0 | 禁用成员走入 MemberType 清空分支后不再命中全量权限 |
| TC-0782 | gRPC PermService | VerifyToken 各失败分支记录日志 | invalid token / disabled user / version mismatch 等 | 日志含 verifyToken fail reason= |
可观测 | P1 | 每个失败分支有 logx.Infof |
| TC-0783 | gRPC PermService | peer.FromContext 失败时仍限流 | 无 peer 的 ctx | 限流 key 使用 "unknown",超限仍返回 ResourceExhausted |
安全 | P0 | fail-closed 不跳过限流 |
| TC-0794 | gRPC PermService | VerifyToken 对任意畸形 AccessToken | 13 条种子(空串/alg=none/长串/Unicode/控制符)+ 运行期可扩 -fuzz=... |
never-panic, (resp,nil), resp.Valid=false |
协议健壮性 | P0 | gRPC 契约:畸形令牌不得使服务端返回 err != nil 或崩溃 |
| TC-0795 | gRPC PermService | GetUserPerms 任意 (appKey,appSecret,productCode,userId) | 6 条种子(空/不存在/SQL 注入样本/Unicode) | 错误码只能在 Unauthenticated/PermissionDenied/InvalidArgument/NotFound/Internal 中 | 协议健壮性 | P0 | 错误码 taxonomy 冻结, 产品侧权限网关依赖此集合 |
| TC-0828 | gRPC PermService | 同 IP 两次 gRPC RefreshToken,quota=1 | peer 10.1.2.3:11111 → 10.1.2.3:22222 |
第 1 次 Unauthenticated(业务放行);第 2 次 ResourceExhausted + "过于频繁" | 安全/限流 | P0 | 端口变化不得绕过限流 |
| TC-0829 | gRPC PermService | 同 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 | gRPC PermService | extractClientIP 对 "host:port" 剥离 |
192.168.0.1:54321 |
返回 192.168.0.1;无 peer 时 error |
契约 | P0 | 剥端口契约不得回退 |
| TC-0831 | gRPC PermService | gRPC refresh 成功后重放旧 rt(换端口) | quota 宽松 | 第 2 次 Unauthenticated + "登录状态已失效" | 安全/并发 | P0 | H-1 + M-7 纵深交叉 |
| TC-1036 | gRPC PermService | GetUserPerms 查询 userId 全局不存在 |
userId 全局不存在 | status.Code()==NotFound + "用户不是该产品的有效成员" |
安全/Oracle | P0 | 与"非成员"同响应 |
| TC-1037 | gRPC PermService | GetUserPerms 查询 userId 存在但非成员 |
userId 存在但非成员 | 同上(status 码由 PermissionDenied 改为 NotFound) |
安全/Oracle | P0 | 与"userId 不存在"同响应 |
| TC-1038 | gRPC PermService | GetUserPerms 查询成员 Status=Disabled |
成员 Status=Disabled |
同上 | 安全/Oracle | P0 | 禁用成员走 loadMembership 清空 MemberType → 同路径 |
| TC-1051 | gRPC PermService | SyncPermissions 同 appKey 连打 quota+1 次触发 ResourceExhausted |
GrpcSyncLimiter = NewPeriodLimit(60, 1, rds, uniqPrefix);同一 appKey 连续 2 次调用 |
第 1 次走业务层(Unauthenticated,因 appKey 不真实);第 2 次 codes.ResourceExhausted |
安全/限流 | P0 | appKey 桶命中上限 |
| TC-1052 | gRPC PermService | GetUserPerms 同 appKey 连打 quota+1 次触发 ResourceExhausted |
同上 limiter 套给 GrpcGetUserPermsLimiter;同一 appKey 连续 2 次 |
第 2 次 codes.ResourceExhausted |
安全/限流 | P0 | GetUserPerms 双桶中的 appKey 桶 |
| TC-1053 | gRPC PermService | 空 AppKey 不消耗 limiter 配额 |
AppKey="" 连打 3 次;然后真实 realKey 首次请求 |
3 次空串请求都 codes.Unauthenticated(走 FindOneByAppKey("") → ErrNotFound),realKey 首次仍命中业务层 codes.Unauthenticated(不是 ResourceExhausted) |
安全/防污染 | P0 | 空串前置分支缺失会把 limiter 计数器打到上限 |
| TC-1054 | gRPC PermService | GetUserPerms 同一 IP、多个不同 appKey → 命中 IP 桶 |
GrpcGetUserPermsLimiter=NewPeriodLimit(60, 2, ...);同 IP 依次用 appKey "a"/"b"/"c" 发请求 |
第 3 次命中 IP 桶上限 codes.ResourceExhausted(尽管每个 appKey 桶都还有额度) |
安全/限流 | P0 | appKey + IP 双桶叠加 |
| 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 |
| TC-0716 | access token payload 中不得含 perms | 生成 access token 后 base64 解码 payload | JSON 中不存在 "perms" key | 安全 | P0 | Dead field 清理 |
| TC-0749 | token.tokenVersion != DB & 产品被禁用 | 同时具备两个失败条件 | 返回 401 "令牌已失效"(而非 403 "产品已禁用") | 安全/顺序 | P0 | TokenVersion 比 ProductStatus 先判,避免客户端看到"用户被踢出"后误以为是产品被禁用 |
| TC-0750 | token.tokenVersion == DB,产品被禁用 | TokenVersion 对齐,product.status=2 | 403 "该产品已被禁用" | 安全 | P0 | TokenVersion 通过后才看 ProductStatus |
| 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 |
| TC-0951 | 合法 HS256 + 正确 secret | 正常 token | 解析成功,claims 可读 | 正常路径 | P0 | good path |
| TC-0952 | alg=none 伪造 | 手工拼无签名段 | 拒绝,错误含 "unexpected signing method" | 安全 | P0 | alg=none 零信任 |
| TC-0953 | alg=RS256 但用 HS secret 签名 | RSA→HMAC 混淆攻击 | 拒绝 | 安全 | P0 | 算法混淆防御 |
| TC-0954 | alg=ES256 header | 非 HMAC 族 | 拒绝 | 安全 | P0 | 非 HMAC 一律拒 |
| TC-0955 | HS256 但 secret 错 | 同算法错密钥 | 拒绝(签名校验失败) | 安全 | P0 | 基础用例 |
| TC-0956 | ParseRefreshToken 复用 ParseWithHMAC | 伪造 alg=none | 拒绝 | 安全 | P0 | 上层也防 |
| TC-0957 | 乱码 token | "abc.def" | 拒绝(malformed) | 容错 | P1 | H-4 |
| TC-0958 | ParseRefreshToken 的 tokenType 校验 | AccessToken 类型 | 拒绝 TokenTypeMismatch | 契约 | P0 | 不被 H-4 修复破坏 |
| TC-0959 | 合法 HS256 + 非标 typ header |
typ=JWT-X | 解析成功(只严格校验 alg) | 兼容 | P1 | 不过度收紧 |
| TC-0960 | 回放过期 token | exp 已过 | 拒绝(解析器底层校验) | 安全 | P0 | H-4 |
| TC-1003 | 合法 HS256 token | 正常 Claims |
解析通过,token.Valid == true |
正向 | P0 | 中央入口正向路径 |
| TC-1004 | alg=RS256(公钥→HMAC 混淆)伪造 | 攻击者用 secret 当 HS256 密钥签署 | unexpected signing method 错误 |
安全 | P0 | CVE-2016-10555 同类防御 |
| TC-1005 | alg=none 伪造 | 空签名段 | 错误返回 | 安全 | P0 | 深度防御 |
| TC-1006 | HS256 但错误 secret | 合法结构 + 猜测 secret | 签名校验失败 | 安全 | P0 | 签名错误必拦 |
| TC-1007 | 任意 jwt.Claims 结构体 |
自定义 customClaims |
解析通过,字段可转型回取 | 契约 | P1 | 与具体 claims 类型解耦,所有调用方可复用 |
| 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 | 两条件同时 |
| 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 | 空 |
| 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 层中的核心共享函数,使用 mock Model 接口进行纯单元测试。
| 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 |
| 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 | 空字符串 |
| 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字段校验 |
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 |
所有 9 个 Model 的
_gen.go均由自定义模板 (cli/goctl/model/) 生成,包含非标准方法(批量操作、事务变体、buildBatchUpdateQuery等)。 以下以 通用测试模式 列出,适用于全部 9 个 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 |
| 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 |
| 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 | 参数计数 |
| 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 |
| 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 |
| 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 |
| 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个变量均修改 |
| 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 | 非状态字段更新不影响会话 |
| TC-0419 | UpdateProfile | 状态变更-tokenVersion+1 | statusChanged=true | 成功, tokenVersion+1 | 正常路径 | P0 | 状态变更使会话失效 |
| TC-0420 | UpdateProfile | 乐观锁冲突 | expectedUpdateTime 与DB不符 | 返回ErrUpdateConflict | 异常路径 | P0 | WHERE updateTime=? |
| TC-0421 | UpdateProfile | 并发场景 | 两个 goroutine 基于同一 updateTime 并发更新 | 仅一方成功, 另一方得到 ErrUpdateConflict | 并发 | P0 | 乐观锁仅允许一个成功 |
| TC-0422 | UpdateProfile | userId不存在 | id=9999999 | 返回 ErrUpdateConflict (affected=0) | 异常路径 | P1 | WHERE 不匹配 |
| TC-0736 | SysUserModel | 单次 Increment 返回值 == DB 持久值 | 初始 tokenVersion=v0 | 返回值 = v0+1 == DB 读取值 | 正常路径 | P0 | LAST_INSERT_ID(tokenVersion+1) 原子自增,不依赖缓存旧值 |
| TC-0737 | SysUserModel | Increment 后缓存必须被主动清理 | 先 Load 预热缓存,再 Increment | 再次 Load 读到的 TokenVersion 为自增后值(非 stale) | 缓存一致性 | P0 | 事务成功后 DelCacheCtx |
| TC-0738 | SysUserModel | 10 goroutine 并发自增同一用户 | 起始 v0,并发 Increment×10 | 10 次返回值互不重复,最终 DB = v0+10 | 并发/竞态 | P0 | 原子 UPDATE,不会丢失更新 |
| TC-0784 | SysUserModel | MySQL 1062 错误正确识别 | mysql.MySQLError{Number: 1062} |
IsDuplicateEntryErr 返回 true |
功能 | P0 | 类型断言替代字符串匹配 |
| TC-0785 | SysUserModel | 非 1062 错误不误判 | 其他 MySQL 错误或普通 error | 返回 false | 功能 | P0 | 只匹配 1062 |
| TC-0802 | SysUserModel | expected == DB.tokenVersion |
expected=5, DB=5 | 返回 6;DB 落盘 6 | 正常路径 | P0 | CAS 成功分支 |
| TC-0803 | SysUserModel | expected != DB.tokenVersion |
expected=9, DB=10 | ErrTokenVersionMismatch;DB 零副作用 |
安全/并发 | P0 | 会话劫持窗口拦截 |
| TC-0805 | SysUserModel | 8 goroutine 同时 CAS 同 expected | N=8 | 恰好 1 成功 + 7 ErrTokenVersionMismatch;DB tokenVersion 只递增 1 |
并发/竞态 | P0 | 原子性外部可观察证据 |
| TC-0806 | SysUserModel | 成功后 id-key / username-key 缓存一致性 | CAS→1 | 再读两路都看到 1(非 stale 0) | 缓存 | P0 | 防 middleware 读 stale tokenVersion 放行旧 token |
| TC-0812 | SysUserModel | logic 层 6 goroutine 并发 RefreshToken 同一旧 rt | N=6 | 1 成功 + 5 × 401 "登录状态已失效";DB 递增 1 | 并发/协议 | P0 | H-1 纵深,覆盖 logic 层分支到 CodeError |
| TC-0867 | SysUserModel | 透传 username 与 DB 一致 | CAS 正常 | 成功且 username-key 缓存也失效(沿用 TC-0806 验证) | 契约 | P0 | 签名扩展不破坏既有 CAS 语义 |
| TC-0924 | SysUserModel | UpdatePassword:FindOne 填缓存后行被并发删除 | 直接 SQL 删行绕过缓存失效 | ErrUpdateConflict,不得静默成功 |
并发/TOCTOU | P0 | RowsAffected=0 必须升格 |
| TC-0925 | SysUserModel | UpdatePassword:正常写入 | 存在且未并发 | 持久化 + tokenVersion 递增 | 正常路径 | P0 | hot path 不回归 |
| TC-0926 | SysUserModel | UpdatePassword:user 不存在 | id=非法 | ErrNotFound(FindOne 先挂) |
异常路径 | P0 | 直接失败 |
| TC-0927 | SysUserModel | UpdateStatus:行被并发删除 | 同 TC-0924 手法 | ErrUpdateConflict |
并发/TOCTOU | P0 | 对称覆盖 |
| TC-0928 | SysUserModel | UpdateStatus:正常禁用 | status=2 | 持久化 + tokenVersion 递增(用户被踢) | 正常路径 | P0 | 禁用副作用 |
| TC-0929 | SysUserModel | UpdateStatus:user 不存在 | id=非法 | ErrNotFound |
异常路径 | P0 | M-2 |
| TC-1044 | SysUserModel | UpdateStatus 用"错误 username" 调用 → Model 层仍按错误 key 清理 |
真实 username="u1";调 UpdateStatus(id, "WRONG", ...);预置 u1 和 WRONG 两个 cache 槽 |
只有 WRONG 对应的 cache 槽被删;u1 的 cache 槽仍在(证明 Model 层没有自己 FindOne 纠正 username) |
契约/反向 | P0 | 内部二次 FindOne 回退一跑即红 |
| TC-1045 | SysUserModel | IncrementTokenVersion 用"错误 username" 调用 → 同上 |
同上 | 同上;DB tokenVersion 正常前进 | 契约/反向 | P0 | IncrementTokenVersion 分支 |
| TC-1046 | SysUserModel | IncrementTokenVersion 对"已被删除的行" 仍正常走 RowsAffected=0 → ErrUpdateConflict 分支 |
先 Insert 再 Delete,随后调 IncrementTokenVersion(deletedId, "anyName", deletedUpdateTime) |
errors.Is(err, ErrUpdateConflict);不触发 panic / nil user 崩溃(证明没有 FindOne 前置) |
契约/边界 | P0 | 删后 CAS 的边界 |
| TC-1047 | SysUserModel | Logic 层 Logout 必须把 ud.Username 透传到 Model |
mock IncrementTokenVersion(id, "u1", _),ud.Username="u1" |
mock EXPECT 命中,不得出现任何 FindOne mock 调用 |
契约 | P0 | Logout 口径 |
| TC-1048 | SysUserModel | Logic 层 Logout 即使 ud.Username=="" 也必须透传(空串) |
mock IncrementTokenVersion(id, "", _) |
mock EXPECT 命中,空串仍被透传(Model 层自负其责) | 契约/边界 | P1 | 不在 Logic 层搬运内部补全 |
| TC-1079 | SysUserModel | UpdateProfileWithTx 成功后 id/username 两把 sysUser 缓存仍持旧值 |
- | 预热缓存 → 事务改 nickname/remark 并 commit | Redis 两把 key 仍存在且与预热 payload 等价;DB 已落新值 | P0 | 证明 pre-commit DelCache 已被移除(修复前会在此处失败) |
| TC-1080 | SysUserModel | InvalidateProfileCache 一次性失效 id / username 两把 key |
- | 预热后调 InvalidateProfileCache | 两把 key 均被 DEL 掉 | P0 | post-commit 显式失效入口契约 |
| TC-1081 | SysUserModel | 两段式 E2E:UpdateProfileWithTx(不碰缓存) + InvalidateProfileCache(清缓存) | - | 事务 commit 后 (a) 立即 FindOne (b) 调 invalidate 后再 FindOne | (a) 命中缓存返回旧值;(b) 回源 DB 取到新值 | P0 | 锁死"事务提交 → 缓存权威"的正确顺序,防止未来有人漏掉 invalidate |
| TC-1082 | SysUserModel | UpdateUser tx 分支(改 deptId)post-commit 失效 sysUser 两级缓存 | - | 超管改 deptId 触发 tx 分支 | sysUser:id / sysUser:username / ud:{userId}:{product} 三把 key 均为空;下一轮 FindOne 返回新 deptId | P0 | Logic 层在 TransactCtx 返回后先 SysUserModel.InvalidateProfileCache 再 UserDetailsLoader.Clean |
| TC-1083 | SysUserModel | (保留编号) | - | — | — | — | — |
| TC-1143 | SysUserModel | IncrementTokenVersionWithTx 正常路径:事务内 tokenVersion +1 返回新值 |
seed user(tv=5),TransactCtx 内调用 | 返回 6;事务 commit 后 DB 落盘为 6;updateTime 前进 |
正常路径 | P0 | LAST_INSERT_ID(tokenVersion+1) 原子自增契约 |
| TC-1144 | SysUserModel | IncrementTokenVersionWithTx 目标行不存在 → ErrUpdateConflict |
调用不存在的 id | 返回 (0, ErrUpdateConflict);事务内未生成 LAST_INSERT_ID |
异常路径 | P0 | affected==0 必须升格为 ErrUpdateConflict,不得静默返回 0 |
| TC-1145 | SysUserModel | IncrementTokenVersionWithTx session==nil 报错 |
session=nil | 返回错误("requires a non-nil session");不发 SQL | 契约/防御 | P0 | 非事务场景请改走 IncrementTokenVersion;不应被静默降级 |
| TC-1146 | SysUserModel | IncrementTokenVersionWithTx 事务 rollback 时 DB 不落盘 |
TransactCtx 内先 Increment 再 return err | DB 中 tokenVersion 保持初值;updateTime 不变 |
事务 | P0 | tokenVersion 增量必须能随业务事务整体回滚 |
| TC-1147 | SysUserModel | BatchIncrementTokenVersionWithTx 正常批量 |
3 个 user,tv=0/5/10 | 三者 tokenVersion 各 +1(1/6/11) | 正常路径 | P0 | IN(?,?,?) 一次性批量递增 |
| TC-1148 | SysUserModel | BatchIncrementTokenVersionWithTx 空 ids 直接返回 nil 且不发 SQL |
ids=[] | err==nil;DB 任何行 tokenVersion 不变;事务内没有任何 UPDATE |
边界 | P0 | 空集合直接短路 |
| TC-1149 | SysUserModel | BatchIncrementTokenVersionWithTx session==nil 报错 |
session=nil | 返回错误;不发 SQL | 契约 | P0 | 与单个版本对称 |
| TC-1150 | SysUserModel | BatchIncrementTokenVersionWithTx 事务 rollback 时全体回滚 |
TransactCtx 内先 batch 再 return err | 所有目标 tokenVersion 保持初值 | 事务 | P0 | 原子性跨多行 |
| TC-1151 | SysProductModel | UpdateWithOptLockTx 正常事务内 CAS |
seed product,TransactCtx 内用正确 expectedUpdateTime | err==nil;commit 后 DB 落盘新 name/status;updateTime 推进 |
正常路径 | P0 | 事务版本复现 UpdateWithOptLock 的 CAS 语义 |
| TC-1152 | SysProductModel | UpdateWithOptLockTx expectedUpdateTime 错位 → ErrUpdateConflict |
传入过期的 expectedUpdateTime | 返回 ErrUpdateConflict;事务 rollback 后 DB 无变更 |
异常/并发 | P0 | WHERE updateTime=? 打空 → affected=0 |
| TC-1153 | SysProductModel | UpdateWithOptLockTx session==nil 报错 |
session=nil | 返回错误;不发 SQL | 契约 | P0 | 与 sysUserModel 对称 |
| TC-1154 | SysProductModel | InvalidateProductCache 一次性失效 id/appKey/code 三把 key |
预热三把缓存后调用 | 三把 key 均为空;Get 返回空串 |
缓存一致性 | P0 | post-commit 显式失效入口 |
| TC-1155 | SysProductMemberModel | FindActiveMemberUserIdsByProductCodeTx 返回启用成员 userId |
产品下 3 个 Status=1 + 1 个 Status=2 + 1 个他产品 | 返回 3 个启用成员 userId(disabled 与他产品均不在列) | 正常路径 | P0 | WHERE productCode=? AND status=1;跨产品 / 跨状态隔离 |
| TC-1156 | SysProductMemberModel | FindActiveMemberUserIdsByProductCodeTx 空产品 / 无启用成员 → 空切片 |
产品下 0 启用成员 | 返回 len==0;err==nil |
边界 | P0 | 与 UpdateProductLogic 的 len(ids)==0 分支对接 |
| TC-1157 | SysProductMemberModel | FindActiveMemberUserIdsByProductCodeTx 按 id 升序输出 |
乱序插入多个成员 | 返回切片严格按 id 升序 |
契约/死锁防护 | P1 | ORDER BY id:锁获取顺序稳定,避免与按主键扫描的其它事务互相死锁 |
| 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 |
| 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 过滤, 全禁用时返回空 |
| TC-0986 | SysPermModel | 产品含 status ∈ {1, 2, 99} 三类行 | FindAllCodesByProductCode | 只返回 status=1 的 code | 契约 | P1 | 严格 = 不模糊 |
| TC-0987 | SysPermModel | DisableNotInCodesWithTx 只禁用 status=1 且不在白名单的行 | 同上数据 + 白名单含 status=1 的一条 | 仅 1 行被 1→2,99 和既有 2 均不动 | 契约 | P1 | WHERE 严格等值,未来枚举值不被误伤 |
| 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 |
| 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 |
| TC-1072 | SysRoleModel | Happy path:所有 roleId 存在且 Enabled | - | 2 个 Enabled role | 返回 nil;事务成功 | P0 | SELECT ... LOCK IN SHARE MODE 命中率 = len(ids) |
| TC-1073 | SysRoleModel | 存在一个被删除的 roleId | - | 1 Enabled + 1 已 DELETE | 返回 sqlx.ErrNotFound |
P0 | 命中行数 != len(ids) → ErrNotFound |
| TC-1074 | SysRoleModel | 存在一个 Disabled 的 role | - | 1 Enabled + 1 Disabled | 返回 sqlx.ErrNotFound |
P0 | WHERE status=Enabled 过滤掉禁用项 |
| TC-1075 | SysRoleModel | 空 / nil 切片 | - | []int64{} / nil |
返回 nil,不发 SQL | P1 | 提前 return 短路 |
| TC-1076 | SysRoleModel | 入参含重复 id | - | [r1,r1,r2] 全 Enabled |
去重后比较 len(ids)==2,返回 nil |
P1 | 内部 sort+dedup 避免假阳 ErrNotFound |
| TC-1077 | SysRoleModel | 不按 productCode 过滤 | - | role 属 productA,lock 调用不传 productCode | 返回 nil(只管 id + status) | P1 | productCode 校验是上游 BindRoles 职责,LockRolesForShareTx 保持通用 |
| 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 不报错 |
| TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-0462 | FindPermIdsByUserIdAndEffectForProduct | ALLOW-指定产品 | userId=1, effect="ALLOW", productCode="p1" | 返回该用户在该产品下ALLOW的permIds | 正常路径 | P0 | 跨产品权限隔离 |
| TC-0463 | FindPermIdsByUserIdAndEffectForProduct | DENY-指定产品 | userId=1, effect="DENY", productCode="p1" | 返回DENY的permIds | 正常路径 | P0 | 跨产品权限隔离 |
| 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=? 隔离其他产品 |
| 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 | 差量更新依赖此接口 |
| TC-0706 | SysUserRoleModel | 同一产品下同时存在启用/禁用角色 | user 绑定启用+禁用 2 个角色 | 仅返回启用角色的 id | 安全/过滤 | P0 | SQL 加入 r.status=1 |
| TC-0707 | SysUserRoleModel | DeleteByUserIdAndRoleIdsTx 批量删除 | 插入 3 条 (user, roleX),批量删除其中 2 条 | 仅保留未被删除的 1 条 | 正常路径 | P0 | 循环 DELETE → 批量 IN |
| TC-0708 | SysUserRoleModel | 批量删除空列表为 no-op | roleIds=[] | 无任何删除,原记录保留 | 边界 | P0 | 空集合保护 |
| TC-0709 | SysUserRoleModel | 批量删除仅作用于指定 userId | 同 roleId 下两个 user,仅删 user1 | user2 的绑定不受影响 | 约束 | P0 | WHERE userId 严格约束 |
| TC-1061 | SysUserRoleModel | DeleteByRoleIdTx 删除多行后,id 级和组合 key 缓存全部失效 |
预置 3 条 sys_user_role(roleId=R),先 FindOne / 组合 key FindOne 把所有 cache 槽填热 |
删除后 FindOne(id) 均返 ErrNotFound;FindOneByUserIdRoleId(userId, R) 也均返 ErrNotFound;无脏读 |
契约/缓存 | P0 | 两套 key 都得失效 |
| TC-1062 | SysUserRoleModel | DeleteByUserIdAndRoleIdsTx 只删指定 (userId, roleIds) 集合后同上 |
预置多条;删除部分;剩余部分保留 | 被删的全返 ErrNotFound;未被删的 FindOne 正常命中;两层 cache key 对齐 |
契约/缓存 | P0 | 组合 key 的定点失效不误伤 |
| 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] |
| TC-0765 | SysProductMemberModel | CountActiveAdminsTx 返回正确计数 | 产品下 2 个启用 ADMIN + 1 个禁用 ADMIN | 返回 2 | 功能验证 | P0 | SELECT id ... FOR UPDATE 仅计活跃行 |
| TC-0868 | SysProductMemberModel | 产品内 3 个 active admin,排除其中 1 | excludeId=第二个 admin | 返回 2 | 计数 | P0 | 排除目标后正确计数 |
| TC-0869 | SysProductMemberModel | 唯一 active admin,排除他自己 | excludeId=唯一 admin | 返回 0 → 上层识别为"最后一个" | 语义 | P0 | removeMember/updateMember 据此防"降级/移除最后一个 admin" |
| TC-0870 | SysProductMemberModel | 存在 1 个 active + 1 个 disabled admin | excludeId=active | 返回 0(disabled 不计入) | 语义 | P0 | 仍需状态=enabled 才算 |
| TC-1110 | FindOneForShareTx | 事务内按 id 读到最新行并持有 S 锁 | 先 Insert(member),后在 TransactCtx 中调 FindOneForShareTx(id) |
返回与插入一致的 *SysProductMember,Id/ProductCode/UserId/MemberType/Status 全部对齐 |
正常路径 | P0 | L-R13-2 新 API 契约——不走缓存、参数直传事务 session |
| TC-1111 | FindOneForShareTx | id 不存在时返回 sqlx.ErrNotFound |
事务内调 FindOneForShareTx(99999999) |
errors.Is(err, sqlx.ErrNotFound)==true |
边界 | P0 | 让上层能区分"目标成员被并发删除"与其它 DB 错误,不被误吞为 5xx |
| 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 |
| 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不匹配→拒绝 |
| 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 |
| 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 且文案含"未归属任何部门"(与 TC-0993 合一) | 安全 | P0 | DeptPath 空串与 DeptId=0 合一文案,防回归 |
| 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,保证可审计 |
| TC-0860 | 传入 prefetched target,不再查 FindOne | caller=MEMBER,prefetched.DeptId 合法 | SysUserModel.FindOne 次数 = 0;业务结果同无 option 版本 |
性能/契约 | P1 | 避免重复 FindOne |
| TC-0861 | prefetched target.Id 与参数 targetUserId 不一致 | 被 defensive 忽略 | 依然触发一次 FindOne 真实查询(option 失效) | 安全 | P1 | prefetched 自洽校验,不让调用方传错 id 绕过访问控制 |
| TC-0930 | caller 的 MinPermsLevel 缓存过期(偏高) | mock 返回 DB=100,caller 缓存=5,role=50 | 403(以 DB=100 判,50 ≤ 100 越级) | TOCTOU | P0 | 不得信任缓存高权值 |
| TC-0931 | 同级分配 | DB=50,role=50 | 403 "不能分配权限级别高于自身的角色(含同级)" | 契约 | P0 | 含同级 |
| TC-0932 | 严格低一级分配 | DB=50,role=51 | 放行 | 正常路径 | P0 | 正值域 |
| TC-0933 | SuperAdmin 绕过 DB | superAdmin caller | 不查 DB,直接放行 | 正常路径 | P0 | 短路 |
| TC-0934 | Product ADMIN 绕过 DB | memberType=ADMIN | 不查 DB,直接放行 | 正常路径 | P0 | 短路 |
| TC-0935 | DEVELOPER 绕过 DB | memberType=DEVELOPER | 不查 DB,直接放行 | 正常路径 | P0 | 短路 |
| TC-0936 | caller 在 DB 没有任何角色 | FindMin... 返回 ErrNotFound | 403 "您没有可分配的角色等级" | 异常路径 | P0 | fail-close |
| TC-0937 | caller 查询 DB 遇到一般错误 | 非 ErrNotFound | 500(fail-close,不透传原文) | 容错 | P0 | 不泄细节 |
| TC-0938 | caller 为 nil(无 UserDetails) | ctx 未带 | 401(未授权) | 异常路径 | P0 | nil 保护 |
| TC-0969 | caller 缓存级别高(10)但 DB 已降级(20),target 级别 15 | 管理高级目标 | 403 "无权管理权限级别高于或等于您的用户" | 对抗/安全 | P0 | 钉死缓存 TOCTOU |
| TC-0970 | caller 在 DB 中无任何角色(ErrNotFound) |
非超管/非 self 管理路径 | 403(按"无角色 = 最低等级"处理) | 安全 | P0 | ErrNotFound 等同最低 |
| TC-0971 | caller DB 级别(5)严格高于 target(15) | 正常管理 | 放行 | 正向 | P0 | H-2 正向不回归 |
| TC-0972 | caller 侧 FindMinPermsLevelByUserIdAndProductCode 通用 DB 错 |
— | 500 "校验权限级别失败"(fail-close) | 容错 | P0 | 一般错误非 ErrNotFound → 500 |
| TC-0973 | caller 是 SuperAdmin | CheckManageAccess 链路 | 短路放行,不发生 caller-side FindMin 查询 | 正向/优化 | P1 | SuperAdmin 必短路 |
| TC-0974 | caller.UserId == targetUserId(自操作) | 同上 | 短路放行,不发生 caller-side FindMin 查询 | 正向/优化 | P1 | self 必短路 |
| TC-0975 | 共享 helper loadFreshMinPermsLevel 的契约对齐 |
通用 err / ErrNotFound | 分别返回 (0, false, err) 与 (0, true, nil) |
契约 | P0 | helper 契约与 GuardRoleLevelAssignable 同步 |
| TC-0993 | legacy DEVELOPER(DeptId=0、DeptPath="")去管理他人 | 合法 target + productCode | response.CodeError{Code:403},文案含 "未归属任何部门"(与 TC-0504 同路径) |
契约/回归 | P0 | 文案与错误结构已合一,防再次分叉 |
| TC-1017 | SuperAdmin / ADMIN / DEVELOPER 走 HasFullPerms 短路 |
3 条子用例分别构造不同 caller | HasFullPerms=true;FindMinPermsLevelByUserIdAndProductCode 不得被调用(gomock 无 EXPECT 命中即 fail) |
性能/契约 | P0 | 全权调用者零 DB 成本 |
| TC-1018 | MEMBER caller 仅打 1 次 DB,循环内对 5 个角色走 CheckRoleLevelAgainst 不再打 DB |
mock FindMin Times(1);本地用 5 个 role level 做比较 |
Times(1) 断言命中;同级/更高级角色拒 403;严格低级角色通过 | 性能/安全 | P0 | Times(1) 是核心断言,一旦循环内误打 DB 会命中"unexpected call" |
| TC-1019 | caller ErrNotFound → NoRole=true,不翻 500 |
mock 返 sqlx.ErrNotFound |
snap.NoRole=true;CheckRoleLevelAgainst(999) 仍 403 "没有可分配的角色等级" |
契约 | P1 | 与 loadFreshMinPermsLevel 的口径对称,保留 ErrNotFound → 最低级 的原契约 |
| TC-1020 | caller 其他 DB 错误 → fail-close 500 | mock 返通用 error |
CodeError.Code()==500 |
安全 | P0 | DB 抖动不得被同化为"无角色 → 最低级"放行,保持与 L-4 修复一致 |
| TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|
| TC-0505 | 各类型优先级正确 | 全部4种+未知 | SA=0,A=1,D=2,M=3,unknown=MaxInt32 | 白盒 | P0 | switch分支 |
| 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 |
| TC-0705 | Load 不存在用户 | userId=999999999 | 返回空 Username;第二次 Load 行为与首次一致(无缓存污染) | 边界/缓存 | P1 | !ok 分支不缓存零值 |
| TC-0792 | 50 goroutine barrier 同时 Load 同 userId | 缓存已清空 | 每个 goroutine 都拿到完整数据; FindOne 调用次数 ≤ workers/5, >0 |
并发/缓存 | P0 | singleflight.Group.Do 合并冷启动击穿 |
| TC-0793 | 首次 Load 后再 20 次 Load | 缓存已预热 | 首次 DB 命中 1 次, 后续 0 次(全部走 Redis cache) | 缓存命中 | P0 | 写 Redis 成功后应走 fast-path 不再打 DB |
| 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 | 防并发惊群 + 负缓存收敛 |
| TC-0913 | 用户不存在 → (ud, nil) 且 ud.Username == "" |
userId=999999999 | 无 err,Username 空,caller 自行映射 401 | 契约 | P0 | 401 vs 503 区分 |
| TC-0914 | 新建用户刚落地 CreateUser 后立刻 Load |
新 username | 读到真实 user,不命中负缓存哨兵 | 契约 | P0 | CreateUser 反向清除负缓存 |
| TC-0915 | partial load 失败(幽灵 deptId) | 人为把 user.deptId 改成不存在 | 返回 ud(含错误子加载),但不写 5 分钟正缓存 | 契约 | P0 | 局部失败不污染主缓存 |
| TC-0916 | 全绿 MEMBER 正路径 | 正常 user + product + 无角色 | loadOk=true,命中 5 分钟正缓存 |
契约 | P0 | 保证好路径仍能缓存 |
| TC-0917 | ErrLoaderDegraded 作为 sentinel 可 errors.Is 断言 |
直接断言导出符号 | errors.Is(ErrLoaderDegraded, ErrLoaderDegraded) == true |
契约 | P2 | 调用方可稳定识别 |
| 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 |
| TC-1013 | N=2 的真实缓存场景 | 2 用户各 Load 预热后 BatchDel | 主 key DEL、userIndex/productIndex 中 2×3 元素全部 SREM | 契约/缓存 | P1 | 同步清理不能被回退 |
| TC-1014 | productCode="" 分支 |
无效 uid + 空 productCode | 不 panic / 不报错 | 契约/防御 | P2 | pipeline 分支 fail-safe |
| TC-1112 | DetachCacheCleanCtx:parent 取消后 detached ctx 仍存活 |
ctx, cancel := context.WithCancel(parent); cleanCtx, _ := DetachCacheCleanCtx(ctx); cancel() |
cleanCtx.Err() == nil;<-cleanCtx.Done() 只在 3s timeout 后触发 |
契约 | P0 | 防 client 断连把 post-commit 缓存失效一并带走 |
| TC-1113 | DetachCacheCleanCtx:硬 3s 超时兜底 |
观察 cleanCtx 的 deadline |
ok==true 且 deadline ∈ [now+2.5s, now+3.5s] |
契约 | P0 | 防后台 goroutine 悬挂;窗口必须被锁定 |
| TC-1114 | DetachCacheCleanCtx:parent 的 Value 透传 |
parent := context.WithValue(context.Background(), k, "v"); cleanCtx,_ := ... |
cleanCtx.Value(k) == "v" |
契约 | P1 | trace id / tenant id 等日志上下文不被剥离 |
| TC-1115 | isCtxCanceledErr 对 Canceled/DeadlineExceeded 返回 true,其它错误 false |
context.Canceled、context.DeadlineExceeded、errors.New("redis down") |
true / true / false |
契约 | P0 | 审计 L-R13-5 方案 B 分类口径冻结 |
| TC-1116 | logCacheInvalidationErr 对 nil 错误早退 |
err=nil |
不触发任何日志写入,函数瞬时返回 | 契约 | P1 | 避免误写 nil 日志干扰排查 |
| TC-1117 | InvalidateProfileCache 在 ctx canceled 下仍不 panic、不阻断主流程 |
传入已 cancel 的 ctx + id + username | 函数返回 nil(方法签名本就无返回),Redis 键可能未删除,但无 panic/无 error | 容错 | P0 | 修复前的 _ = DelCacheCtx 会吞 error,修复后分类记录 |
| 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 | FindPermIdsByUserIdAndEffectForProduct |
| TC-0521 | 禁用DEV部门成员无全量权限 | dept.type=DEV, dept.status=Disabled | ud.Perms为空 | 安全 | P0 | DeptStatus检查 |
| TC-1205 | NORMAL 部门冻结(Status Disabled)后成员 Perms 为空 | DeptType=NORMAL, DeptStatus=Disabled, MemberType=MEMBER | ud.Perms 等于 []string{}(非 nil);不得包含任何权限码 |
安全/业务约束 | P0 | loadPerms 在 if !ud.IsSuperAdmin && ud.DeptId>0 && ud.DeptStatus!=Enabled { return nil } 前置拦截;冻结部门成员重登后应立即无权,而非等 JWT 过期 |
| TC-1206 | loadPerms 出口 Perms 恒为非 nil 数组 | 普通成员无角色无附加权限 | ud.Perms 为 []string{}(json.Marshal 输出 [] 而非 null) |
接口契约 | P0 | ud.Perms = []string{} 初始化保证下游前端/gRPC 客户端无需做 nil vs [] 的双重 defensive check |
| 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 |
| 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 | loadMembership |
| TC-1207 | errors.Is 语义稳健性:ErrNotFound 包装后仍被正确识别 |
productmember.ErrNotFound 来自 sqlx.ErrNotFound;若 model 层未来包装为 fmt.Errorf("%w", err),errors.Is 仍应成立 |
errors.Is(productmember.ErrNotFound, sqlx.ErrNotFound) == true;errors.Is(fmt.Errorf("wrap: %w", productmember.ErrNotFound), sqlx.ErrNotFound) == true;两层包装均不应把"用户非成员"退化成 503 |
契约/健壮性 | P1 | 防止未来 model 层引入包装错误时 loadMembership 的 ErrNotFound 分支悄悄失配、把合法的"不是成员"判定退化为 ErrLoaderDegraded |
| 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 |
| 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 | CheckManageAccess权限校验 |
| TC-0543 | updateUser自己修改DeptId被拒绝 | ctx含userId=X, req.Id=X, req.DeptId!=nil | 403 "不允许修改自己的部门和状态" | 安全 | P0 | 自编辑限制DeptId |
| TC-0544 | updateUser自己修改Status被拒绝 | ctx含userId=X, req.Id=X, req.Status!=0 | 403 "不允许修改自己的部门和状态" | 安全 | P0 | 自编辑限制Status |
| TC-0545 | updateUser未登录被拒绝 | ctx无UserDetails | 401 "未登录" | 安全 | P0 | caller==nil |
| 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 畸形时等价于未登录 |
| 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 |
| — | — | — | — | 已删除: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 |
| TC-0739 | quota=2 的 limiter,第 3 次 logout | 同一 userId 连续 logout 3 次 | 前 2 次成功 + tokenVersion+2;第 3 次 429 "请求过于频繁",tokenVersion 不再递增 | 安全/限流 | P0 | 超限请求不得进入业务层,否则攻击者可反复"自增搅乱缓存" |
| TC-0740 | 限流按 userId 隔离 | userA 打满配额后 userB 登出 | userB 正常 logout,不受 userA 限流影响 | 安全/隔离 | P0 | 限流 key 必须按 userId 分桶 |
| TC-0741 | quota=1 limiter,burst 第 2 次 | 同一 userId 刷新 2 次 | 第 1 次 200;第 2 次 429,tokenVersion 不得递增(避免攻击者持续废除 refresh token) | 安全/限流 | P0 | refresh 同样走 TokenOpLimiter |
| TC-0742 | 限流按 userId 隔离(productCode 无关) | userA(p1) 满额 → userB(p1) 刷新 | userB 正常 200 | 安全/隔离 | P0 | key 不含 productCode,不会跨用户误伤 |
| TC-0790 | period=1s quota=1, 打满后 sleep 1.2s 再调 | 同一 userId 连续 logout | 第 1 次 200; 第 2 次 429; sleep 后第 3 次 200 且 tokenVersion=2 | 安全/限流 | P0 | 限流必须是滚动窗口, 不能退化成永久 deny |
| TC-0791 | Redis 不可达(127.0.0.1:1 + NonBlock) |
limit.Take 返回 err | logout 仍成功(fail-OPEN), tokenVersion=1 | 容错契约 | P0 | code, _ := 的工程取舍被冻结, 未来改 fail-close 需 code review |
| TC-0862 | behindProxy=true + X-Forwarded-For: 1.1.1.1, 2.2.2.2 |
首段合法 | 返回 1.1.1.1 |
契约 | P0 | 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 | 非法段跳过后走 XRI |
| TC-0864 | behindProxy=true + 两头均空 | 无 XFF / 无 XRI | 回落到 RemoteAddr 剥端口后的 host |
容错 | P1 | 降级路径 |
| TC-0865 | behindProxy=true + X-Forwarded-For: " 3.3.3.3 " (空白) |
首段带空白 | 返回 3.3.3.3(trim 后合法) |
边界 | P1 | trim 后再解析 |
| TC-0866 | behindProxy=false + XFF=1.1.1.1 |
RemoteAddr=5.5.5.5:8080 |
忽略 XFF,返回 5.5.5.5 |
安全/伪造 | P0 | 不信任客户端注入的头部 |
| TC编号 | 接口/方法 | 测试场景 | 输入参数 | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
|---|---|---|---|---|---|---|---|
| TC-1280 | POST /api/user/create | 正常创建用户,返回ticket | SuperAdminCtx + valid username/deptId | code=200, resp含id/credentialsTicket/credentialsExpiresAt | 正常路径 | P0 | 服务端生成密码+ticket |
| TC-1281 | POST /api/user/create | 用ticket领取凭证后可登录 | 创建→fetchCredentials→用返回密码登录 | 登录成功 | 端到端 | P0 | 密码可用性验证 |
| TC-1282 | POST /api/user/create | 生成密码满足强度要求 | 创建→fetchCredentials→ValidatePassword | ValidatePassword返回"" | 功能验证 | P0 | 密码强度 |
| TC-1283 | POST /api/user/create | 用户名已存在 | 重复username | code=409 "用户名已存在" | 异常路径 | P0 | 唯一约束 |
| TC-1284 | POST /api/user/create | 创建后mustChangePassword=1 | 创建→查DB | mustChangePassword=1 | 功能验证 | P0 | 强制改密 |
| TC-1285 | POST /api/user/resetPassword | 超管重置普通用户密码 | SuperAdminCtx + userId=普通用户 | code=200, resp含ticket | 正常路径 | P0 | 基本重置流程 |
| TC-1286 | POST /api/user/resetPassword | 产品ADMIN重置本产品成员密码 | AdminCtx + userId=本产品member | code=200 | 正常路径 | P0 | ADMIN权限 |
| TC-1287 | POST /api/user/resetPassword | 不能重置超管密码 | AdminCtx + userId=superadmin | code=403 "不能重置超级管理员的密码" | 安全 | P0 | 超管保护 |
| TC-1288 | POST /api/user/resetPassword | MEMBER无权重置 | MemberCtx + userId=其他用户 | code=403 | 安全 | P0 | 权限校验 |
| TC-1289 | POST /api/user/resetPassword | 重置后旧token失效 | 重置前记录tokenVersion,重置后比对 | tokenVersion递增 | 安全 | P0 | 会话吊销 |
| TC-1290 | POST /api/user/resetPassword | 重置后mustChangePassword=1 | 重置→查DB | mustChangePassword=1 | 功能验证 | P0 | 强制改密 |
| TC-1291 | POST /api/user/resetPassword | 目标用户不存在 | userId=999999 | code=404 | 异常路径 | P0 | 用户不存在 |
| TC-1292 | POST /api/user/resetPassword | 乐观锁冲突 | 并发修改同一用户 | code=409 | 并发安全 | P1 | ErrUpdateConflict |
| TC-1293 | POST /api/user/resetPassword | 重置后用新密码可登录 | 重置→fetchCredentials→登录 | 登录成功 | 端到端 | P0 | 新密码可用 |
| TC-1294 | POST /api/user/fetchCredentials | 正常消费ticket | 有效ticket | code=200, resp含username+password | 正常路径 | P0 | 一次性消费 |
| TC-1295 | POST /api/user/fetchCredentials | 二次消费同一ticket | 消费后再次请求 | code=400 "凭证票据无效或已过期" | 安全 | P0 | 一次性语义 |
| TC-1296 | POST /api/user/fetchCredentials | 空ticket | ticket="" | code=400 "ticket 不能为空" | 边界 | P0 | 参数校验 |
| TC-1297 | POST /api/user/fetchCredentials | 非法ticket | ticket="random_garbage" | code=400 "凭证票据无效或已过期" | 安全 | P0 | 无效ticket |
| TC-1298 | POST /api/user/fetchCredentials | 非ADMIN无权消费 | MemberCtx + valid ticket | code=403 | 安全 | P0 | 权限校验 |
| TC-1299 | POST /api/user/fetchCredentials | 并发消费同一ticket | 10 goroutine同时消费 | 恰好1个成功,其余400 | 并发安全 | P0 | GetDelCtx原子性 |