test-design.md 217 KB

权限管理系统 (perms-system-server) — 全路径覆盖测试设计

测试范围: API (go-zero REST, 全 POST) + gRPC (status codes) + Model 层 (_gen.go 模板生成 + 自定义方法) + Logic 单元测试 + util 层 + 访问控制 + UserDetailsLoader 测试报告与代码审计详见 test-report.md


一、系统架构与逻辑链路

1.1 整体调用链路

HTTP Client                                  gRPC Client
     │                                            │
     ▼                                            ▼
rest.Server (go-zero)                    zrpc.Server (go-zero)
     │ (全部 POST 路由)                            │
     ▼                                            ▼
Handler 层 (JSON Body 解析)              PermServer (permserver.go)
     │                                   (status.Error + codes.Xxx)
     ▼                                            │
JwtAuth Middleware (鉴权/上下文注入)                │
     │                                            │
     ▼                                            ▼
Logic 层 (业务逻辑)  ◄────── 共享 ────►  authHelper (jwt.go / perms.go)
     │                                            │
     ▼                                            ▼
util 层 (NormalizePage / IsValidEmail / IsValidPhone)
     │
     ▼
Model 层 (go-zero sqlc + cache + TransactCtx + 批量查询)
     │  ├── _gen.go (自定义模板生成: CRUD/Batch/WithTx/缓存管理)
     │  └── 自定义方法 (分页/按条件查询/级联删除等)
     ▼
MySQL (InnoDB) + Redis Cache

1.2 Model 层接口全景

共 9 个 Model,每个包含:

层级 方法类别 数量/模型 来源
_gen.go 基础 CRUD Insert, InsertWithTx, FindOne, FindOneWithTx, Update, UpdateWithTx, Delete, DeleteWithTx 8 自定义模板
_gen.go 批量操作 BatchInsert, BatchInsertWithTx, BatchUpdate, BatchUpdateWithTx, BatchDelete, BatchDeleteWithTx 6 自定义模板
_gen.go 唯一索引查询 FindOneBy{UniqueField}, FindOneBy{UniqueField}WithTx (因表而异) 0~2 组 自定义模板
_gen.go 内部辅助 TransactCtx, TableName, findListByPrimaryKeys, getPrimaryKeyValue, buildBatchUpdateQuery, formatPrimary, queryPrimary 7 自定义模板
自定义方法 分页查询/按条件查询/级联删除/批量ID查询等 3~7 手写

1.3 权限计算逻辑链路

输入: userId + deptId + productCode + isSuperAdmin(bool)
    │
    ├─ isSuperAdmin=true → 产品全部启用权限 + "SUPER_ADMIN"
    ├─ 非产品成员 → nil + ""
    ├─ DEVELOPER/ADMIN → 产品全部启用权限 + memberType
    ├─ MEMBER + deptId>0 + dept.DeptType="DEV" → 产品全部启用权限 + "MEMBER"
    ├─ MEMBER + deptId>0 + dept查询失败/DeptType≠"DEV" → 继续走角色权限流程
    └─ MEMBER → (角色权限 ∪ ALLOW) - DENY → 过滤 status=1

二、REST API 测试用例

注意: 所有路由统一为 POST 方法,请求参数均通过 JSON Body 传递。

2.1 产品端登录 POST /api/auth/login

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0001 POST /api/auth/login 正常登录(普通用户+productCode) {"username":"user1","password":"123456","productCode":"test"} code=0, accessToken/refreshToken/userInfo 正常路径 P0 loginLogic全路径
TC-0002 POST /api/auth/login 正常登录-带productCode+ADMIN成员 {"username":"user1","password":"123456","productCode":"test"} code=0, perms含用户可用权限, memberType="ADMIN" 正常路径 P0 GetUserPerms(false) MEMBER分支
TC-0003 POST /api/auth/login 超管通过产品端登录被拒绝 {"username":"super","password":"x","productCode":"p1"} code=403, "超级管理员不允许通过产品端登录,请使用管理后台" 安全 P0 IsSuperAdmin==1 → ErrForbidden
TC-0004 POST /api/auth/login 超管无productCode被拒绝 {"username":"super","password":"x"} code=403, "超级管理员不允许通过产品端登录,请使用管理后台" 安全 P0 IsSuperAdmin==1 → ErrForbidden
TC-0005 POST /api/auth/login 用户不存在 {"username":"notexist","password":"x"} code=401, "用户名或密码错误" 异常路径 P0 ErrNotFound分支
TC-0006 POST /api/auth/login DB异常(非ErrNotFound) FindOneByUsername连接失败 code=500, "服务器内部错误" 异常路径 P1 透传err→Setup兜底
TC-0007 POST /api/auth/login 密码错误 {"username":"admin","password":"wrong"} code=401 异常路径 P0 bcrypt比对失败
TC-0008 POST /api/auth/login 账号冻结 status=2用户 code=403, "账号已被冻结" 分支覆盖 P0 u.Status!=1
TC-0009 POST /api/auth/login 非产品成员 productCode指向用户不属于的产品 code=403, "您不是该产品的成员" 安全 P0 非成员禁止登录
TC-0010 POST /api/auth/login DEVELOPER成员 DEVELOPER类型成员 perms全量, memberType="DEVELOPER" 分支覆盖 P1 perms.go DEVELOPER分支
TC-0011 POST /api/auth/login SQL注入 {"username":"' OR 1=1 --","password":"x"} code=401 安全 P0 参数化查询
TC-0012 POST /api/auth/login 缺少必填字段 {} HTTP 400 边界 P1 httpx.Parse校验(productCode现为必填)
TC-0013 POST /api/auth/login 产品成员被禁用时拒绝登录 member.status=Disabled 403 "您的产品成员资格已被禁用" 安全 P0 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

2.1b 管理后台登录 POST /api/auth/adminLogin

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0015 POST /api/auth/adminLogin 超管正常登录 {"username":"super","password":"x","managementKey":"valid"} code=0, accessToken/refreshToken/userInfo, isSuperAdmin=1, memberType="SUPER_ADMIN", perms为空 正常路径 P0 adminLoginLogic全路径
TC-0016 POST /api/auth/adminLogin 普通用户被拒绝 {"username":"user1","password":"x","managementKey":"valid"} code=401, "用户名或密码错误" 安全 P0 统一错误消息防用户枚举
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

2.2 刷新Token POST /api/auth/refreshToken

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0026 POST /api/auth/refreshToken 正常刷新 Header Authorization: Bearer <refreshToken> code=0, 新accessToken, 新refreshToken(token轮转,保留原始过期时间) 正常路径 P0 refreshTokenLogic全路径
TC-0027 POST /api/auth/refreshToken 不带productCode(回退) Header Authorization, 无productCode 使用claims.ProductCode 分支覆盖 P1 productCode=""回退
TC-0028 POST /api/auth/refreshToken token无效 Header Authorization: Bearer invalid code=401 异常路径 P0 ParseRefreshToken失败
TC-0029 POST /api/auth/refreshToken 用户已删除 token中userId不存在 code=403, "账号已被冻结" 异常路径 P1 UserDetailsLoader返回Status=0
TC-0030 POST /api/auth/refreshToken 账号冻结 冻结用户 code=403 分支覆盖 P0 Status!=1
TC-0031 POST /api/auth/refreshToken 超管+productCode(token中已含相同pc) isSuperAdmin=1, token中productCode=pc, req.ProductCode=pc refreshToken原样返回, SUPER_ADMIN权限 分支覆盖 P1 isSuperAdmin分支+productCode不变
TC-0032 POST /api/auth/refreshToken 尝试切换产品被拒绝 token中productCode="p1", req.ProductCode="p2" code=400, "刷新令牌不允许切换产品" 安全 P0 禁止跨产品切换
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 复活

2.3 同步权限 POST /api/perm/sync

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0036 POST /api/perm/sync 全部新增 {"appKey":"ak","appSecret":"as","perms":[{"code":"x","name":"y"}]} code=0, added=1, updated=0, disabled=0 正常路径 P0 toInsert→BatchInsert
TC-0037 POST /api/perm/sync 更新已有(名称变更) 已存在code但name不同 updated=1 正常路径 P0 toUpdate→BatchUpdate
TC-0038 POST /api/perm/sync 无变化 已存在且name/remark/status均相同 added=0, updated=0 分支覆盖 P1 跳过更新
TC-0039 POST /api/perm/sync 禁用权限重启 已status=2的权限在列表中 updated=1, status恢复1 分支覆盖 P1 Status!=1条件
TC-0040 POST /api/perm/sync 移除不在列表的权限 DB有多余权限 disabled>0 正常路径 P0 DisableNotInCodes
TC-0041 POST /api/perm/sync 空perms数组被拒绝 {"...","perms":[]} code=400, "权限列表不能为空" 输入校验 P0 空列表校验,防止意外批量禁用
TC-0042 POST /api/perm/sync 验证disabled返回值 已知DB有5条,perms仅含2条 disabled=3 功能验证 P0 RowsAffected()
TC-0043 POST /api/perm/sync appKey无效 {"appKey":"invalid"} code=401 异常路径 P0 FindOneByAppKey失败
TC-0044 POST /api/perm/sync appSecret错误 secret不匹配 code=401 异常路径 P0 AppSecret比对
TC-0045 POST /api/perm/sync 产品已禁用 product.Status!=1 code=403 分支覆盖 P0 Status!=1
TC-0046 POST /api/perm/sync 大批量(1000条) 1000条perms added=1000 性能 P2 BatchInsert性能
TC-0047 POST /api/perm/sync 重复code去重 perms中包含两个相同code 仅处理一次, added=1(而非2) 分支覆盖 P0 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==nilupdated==1productIndexKey canary 已被删除 —— 说明 CleanByProduct 跑在 DetachCacheCleanCtx 上、与 parent cancel 解耦;若回退为"直接用 parent ctx",Redis DEL 会被 cancel 打断 → canary 残留 安全/生命周期 P0 M-R14-1:与 TC-1117 对偶,堵住"事务已提交但 UD 仍在 5 分钟 TTL 内挂着被禁用 perm" 的窗口

2.4 获取用户信息 POST /api/auth/userInfo

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0049 POST /api/auth/userInfo 正常获取-含productCode Bearer token (含productCode) code=0, 完整UserInfo+实时perms 正常路径 P0 userInfoLogic全路径
TC-0050 POST /api/auth/userInfo 不含productCode Bearer token (无productCode) perms为空 分支覆盖 P1 productCode=""
TC-0051 POST /api/auth/userInfo 未登录 无Authorization头 code=401, "未登录" 异常路径 P0 middleware拦截
TC-0052 POST /api/auth/userInfo token过期 过期token code=401 异常路径 P0 middleware
TC-0053 POST /api/auth/userInfo userId=0 伪造claims code=401, "未登录" 分支覆盖 P1 userId==0

2.5 修改密码 POST /api/auth/changePassword

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0054 POST /api/auth/changePassword 正常修改 {"oldPassword":"123456","newPassword":"654321"} code=0 正常路径 P0 changePasswordLogic全路径
TC-0055 POST /api/auth/changePassword mustChangePassword重置 正常修改后 DB中mustChangePassword=2 功能验证 P0 user.MustChangePassword=2
TC-0056 POST /api/auth/changePassword 原密码错误 {"oldPassword":"wrong","newPassword":"newpwd"} code=400, "原密码错误" 异常路径 P0 bcrypt失败
TC-0057 POST /api/auth/changePassword 新密码少于8字符 {"oldPassword":"old","newPassword":"Pas1234"} code=400, "密码长度不能少于8个字符" 输入校验 P0 len<8
TC-0058 POST /api/auth/changePassword 新密码恰好8字符(含大小写+数字) {"oldPassword":"old","newPassword":"Abcdef1x"} code=0 边界 P1 len==8,含大小写+数字
TC-0059 POST /api/auth/changePassword 新密码空字符串 {"oldPassword":"old","newPassword":""} code=400 边界 P0 len("")=0<8
TC-0060 POST /api/auth/changePassword 新密码超过72字符 {"oldPassword":"old","newPassword":"a*73"} code=400, "密码长度不能超过72个字符" 输入校验 P0 len>72
TC-0061 POST /api/auth/changePassword 新密码恰好72字符 {"oldPassword":"old","newPassword":"a*72"} code=0 边界 P1 len==72
TC-0062 POST /api/auth/changePassword 新旧密码相同 {"oldPassword":"123456","newPassword":"123456"} code=400, "新密码不能与原密码相同" 输入校验 P0 OldPassword==NewPassword
TC-0063 POST /api/auth/changePassword 用户不存在 token中userId已删除 code=404 异常路径 P1 FindOne失败
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

2.6 创建产品 POST /api/product/create

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0064 POST /api/product/create 正常创建 {"code":"new","name":"新产品"} code=0, id/appKey/appSecret/adminUser/adminPassword 正常路径 P0 TransactCtx全路径
TC-0065 POST /api/product/create 事务回滚-用户创建失败 模拟InsertWithTx User失败 返回错误, DB无新产品 事务验证 P0 TransactCtx回滚
TC-0066 POST /api/product/create 事务回滚-成员创建失败 模拟InsertWithTx Member失败 产品和用户均回滚 事务验证 P0 TransactCtx回滚
TC-0067 POST /api/product/create 编码已存在 {"code":"existing","name":"x"} code=409 异常路径 P0 FindOneByCode成功
TC-0068 POST /api/product/create 并发创建同编码 两请求同时 一成功一冲突 并发 P1 uk_code
TC-0069 POST /api/product/create createProduct 含空格被拒绝 code="abc def" 400 "产品编码格式不合法" 输入校验 P0 productCodeRegexp
TC-0070 POST /api/product/create createProduct 含特殊字符被拒绝 code="abc@def" 400 输入校验 P0 productCodeRegexp
TC-0071 POST /api/product/create createProduct 全中文被拒绝 code="产品一" 400 输入校验 P0 productCodeRegexp
TC-0072 POST /api/product/create createProduct 纯数字开头被拒绝 code="1abc" 400 输入校验 P0 productCodeRegexp 首字符限定
TC-0073 POST /api/product/create createProduct 空字符串被拒绝 code="" 400 边界 P0
TC-0074 POST /api/product/create createProduct 长度>64 被拒绝 code="a"*65 400 "产品编码长度不能超过64个字符" 边界 P0 len>64
TC-0075 POST /api/product/create createProduct 合法编码(含下划线/中划线/数字) code="pc_01-test" 创建成功 正常路径 P0 Regexp 正向匹配
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_membersys_usersys_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.FindOneStatus=1AdminDeptId 透传 mock SysDeptModel.FindOneStatus=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 执行闭包;闭包内 FindOneForShareTxsqlx.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 校验全部失效

2.7 产品更新/列表/详情

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0076 POST /api/product/update 正常更新 {"id":1,"name":"新名","status":1} code=0 正常路径 P0 updateProductLogic
TC-0077 POST /api/product/update 不存在 {"id":9999,"name":"x"} code=404 异常路径 P0 FindOne失败
TC-0078 POST /api/product/update 不传status {"id":1,"name":"x"} status不变 分支覆盖 P1 Status>0
TC-0079~0083 POST /api/product/list 分页边界 正常/默认/超限/0/负值 已删除:分页边界由 util.TestNormalizePage 单元测试覆盖;列表语义被 M-2 拆分为"超管走 FindList / 非超管只看自己"两条独立契约(TC-0850、TC-0871)
TC-0084~0085 POST /api/product/detail 正常/不存在 已删除:被 M-2 契约合并改写为 TC-0852(他人产品 → 404)、TC-0853(自己产品 AppKey 脱敏)、TC-0872(FindOne 错误 → 404 无差别响应)
TC-0086 / TC-0088 非超管 AppKey 隐藏 已删除:由 TC-0850(list)+ TC-0853(detail)覆盖
TC-0087 / TC-0089 超管可见 AppKey 已删除:由 TC-0854(detail)+ TC-0871(list)覆盖
TC-0090 POST /api/product/update updateProduct 非法状态值被拒绝 status=99 400 "产品状态值无效" 输入校验 P0 仅允许 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=2BatchIncrementTokenVersionWithTx 的空分支被走过 边界 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 枚举全组织结构

2.8 创建部门 POST /api/dept/create

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0091 POST /api/dept/create 创建顶级部门 {"parentId":0,"name":"总部"} code=0, path="/{id}/" 正常路径 P0 TransactCtx, parentPath="/"
TC-0092 POST /api/dept/create 创建子部门 {"parentId":1,"name":"技术部"} code=0, path=parent.path+id+"/" 正常路径 P0 parentId>0分支
TC-0093 POST /api/dept/create 父部门不存在 {"parentId":9999,"name":"x"} code=404, "父部门不存在" 异常路径 P0 FindOneWithTx失败
TC-0094 POST /api/dept/create 不传DeptType默认NORMAL {"parentId":0,"name":"x"} DB deptType="NORMAL" 分支覆盖 P0 deptType=""→DeptTypeNormal
TC-0095 POST /api/dept/create 传DeptType=DEV {"parentId":0,"name":"x","deptType":"DEV"} DB deptType="DEV" 正常路径 P0 req.DeptType赋值
TC-0096 POST /api/dept/create 事务内FindOneWithTx可见性 TransactCtx内InsertWithTx后FindOneWithTx 事务内可读到未提交数据 事务验证 P0 FindOneWithTx(session)
TC-0097 POST /api/dept/create 事务回滚-Insert失败 模拟InsertWithTx失败 DB无新记录 事务验证 P0 TransactCtx回滚
TC-0098 POST /api/dept/create 事务回滚-UpdateWithTx失败 模拟UpdateWithTx失败 Insert也回滚 事务验证 P1 TransactCtx回滚
TC-0099 POST /api/dept/create 多层嵌套(5层) 递归创建5层 path正确拼接 深度测试 P2 path逻辑
TC-0100 POST /api/dept/create 通过Logic创建+验证Path CreateDeptLogic.CreateDept→FindOne path包含/{id}/ 集成验证 P0 FindOneWithTx修复后端到端
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 同口径校验

2.9 部门更新/删除/树

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0101 POST /api/dept/update 正常更新 {"id":1,"name":"新名","sort":5} code=0 正常路径 P0 updateDeptLogic
TC-0102 POST /api/dept/update 不存在 {"id":9999,"name":"x"} code=404 异常路径 P0 FindOne失败
TC-0103 POST /api/dept/update DeptType NORMAL→DEV {"id":1,"deptType":"DEV"} DB deptType="DEV" 正常路径 P0 DeptType合法值更新
TC-0104 POST /api/dept/update DeptType无效值返回错误 {"id":1,"deptType":"INVALID"} code=400, "部门类型无效", DB deptType不变 输入校验 P0 DeptType校验,仅NORMAL/DEV
TC-0105 POST /api/dept/update DeptType变更时级联清除子部门用户缓存 部门从NORMAL改为DEV,有子部门含用户 code=0, 子部门下用户缓存被清除 缓存验证 P0 级联缓存失效
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 检查关联用户
TC-0110~0112 POST /api/dept/tree 正常/空/孤儿 已删除: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 UpdateDeptFindIdsByDeptId 失败 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 值破坏部门树排序稳定性

2.10 权限列表 POST /api/perm/list

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0113 POST /api/perm/list 正常查询 {"productCode":"p1","page":1,"pageSize":10} code=0, total/list 正常路径 P0 permListLogic
TC-0114 POST /api/perm/list 默认分页 {"productCode":"p1"} page=1, pageSize=20 分支覆盖 P1 NormalizePage
TC-0115 POST /api/perm/list pageSize超过上限 {"productCode":"p1","pageSize":200} 实际pageSize=100 边界 P0 NormalizePage cap
TC-0116 POST /api/perm/list 不存在的productCode {"productCode":"notexist"} total=0, list=[] 边界 P1 空结果

2.11 角色管理

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0117 POST /api/role/create 正常创建 {"productCode":"p1","name":"管理员","permsLevel":1} code=0, id>0 正常路径 P0 createRoleLogic
TC-0118 POST /api/role/create 重复角色名 同产品同名 code=409, "该产品下角色名已存在" 业务约束 P0 Duplicate entry→ErrConflict
TC-0119 POST /api/role/create 并发同名创建 两请求同时 一成功一冲突409 并发 P1 唯一索引+1062捕获
TC-0120 POST /api/role/update 正常更新 {"id":1,"name":"新名","permsLevel":2} code=0 正常路径 P0 updateRoleLogic
TC-0121 POST /api/role/update 不存在 {"id":9999,...} code=404 异常路径 P0 FindOne失败
TC-0122 POST /api/role/list 正常查询 {"productCode":"p1","page":1,"pageSize":10} code=0 正常路径 P0 roleListLogic
TC-0123 POST /api/role/list pageSize超过上限 {"productCode":"p1","pageSize":200} 实际pageSize=100 边界 P0 NormalizePage cap
TC-0124 POST /api/role/detail 正常查询 {"id":1} code=0, 含permIds 正常路径 P0 roleDetailLogic
TC-0125 POST /api/role/detail 不存在 {"id":9999} code=404 异常路径 P0 FindOne失败
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 窗口内同名并发创建会命中幽灵快照

2.12 删除角色 POST /api/role/delete

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0126 POST /api/role/delete 正常删除+级联 {"id":5} (含权限/用户绑定) code=0, role_perm/user_role同步清理 正常+事务 P0 TransactCtx全路径
TC-0127 POST /api/role/delete 事务回滚 模拟DeleteWithTx失败 级联删除回滚 事务验证 P0 TransactCtx
TC-0128 POST /api/role/delete 无关联数据 新角色无绑定 code=0 分支覆盖 P1 删0条
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:idsysRole: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 是闭环关键

2.13 绑定角色权限 POST /api/role/bindPerms

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0129 POST /api/role/bindPerms 正常绑定 {"roleId":1,"permIds":[1,2,3]} code=0 正常路径 P0 TransactCtx
TC-0130 POST /api/role/bindPerms 角色不存在 {"roleId":9999,"permIds":[1]} code=404, "角色不存在" 存在性校验 P0 FindOne预检
TC-0131 POST /api/role/bindPerms 清空权限 {"roleId":1,"permIds":[]} code=0, 全清空 分支覆盖 P1 len==0→return
TC-0132 POST /api/role/bindPerms 重复permId {"roleId":1,"permIds":[1,1]} DB唯一索引→事务回滚 边界 P1 uk_role_perm
TC-0133 POST /api/role/bindPerms 事务回滚 模拟BatchInsertWithTx失败 旧数据回滚还原 事务验证 P0 TransactCtx
TC-1024 POST /api/role/bindPerms BindRolePerms 事务首步调用 LockByIdTxsys_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

2.14 创建用户 POST /api/user/create

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0134 POST /api/user/create 正常创建 {"username":"new","password":"123456"} code=0, id>0 正常路径 P0 createUserLogic
TC-0135 POST /api/user/create 用户名已存在(预检) {"username":"existing","password":"x"} code=409, "用户名已存在" 异常路径 P0 FindOneByUsername成功
TC-0136 POST /api/user/create 带完整可选字段 含nickname/email/phone/remark/deptId code=0 正常路径 P1 各字段赋值
TC-0137 POST /api/user/create 非法email格式 {"...","email":"not-an-email"} code=400, "邮箱格式不正确" 输入校验 P0 util.IsValidEmail
TC-0138 POST /api/user/create 合法email {"...","email":"[email protected]"} code=0 正常路径 P1 IsValidEmail通过
TC-0139 POST /api/user/create email为空(可选) {"...","email":""} code=0, 跳过校验 分支覆盖 P1 email!=""判断
TC-0140 POST /api/user/create 非法phone格式 {"...","phone":"abc"} code=400, "手机号格式不正确" 输入校验 P0 util.IsValidPhone
TC-0141 POST /api/user/create 合法phone(国际) {"...","phone":"+8613800138000"} code=0 正常路径 P1 IsValidPhone通过
TC-0142 POST /api/user/create phone为空(可选) {"...","phone":""} code=0, 跳过校验 分支覆盖 P1 phone!=""判断
TC-0143 POST /api/user/create 并发同username(TOCTOU) 两请求同时 一成功一冲突(1062) 并发 P0 Duplicate entry→ErrConflict
TC-0144 POST /api/user/create 唯一索引冲突消息 预检通过后DB冲突 code=409, "用户名已存在" 异常路径 P0 strings.Contains "1062"
TC-0145 POST /api/user/create 密码少于8字符 {"username":"x","password":"Pas1234"} code=400, "密码长度不能少于8个字符" 输入校验 P0 密码强度校验(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 落盘 deptIddeptType=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 未传 avatarsys_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.FindOneForShareTxSysUserModel.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:闭包内 FindOneForShareTxsqlx.ErrNotFound(并发 DeleteDept 已提交) 同上 mock,仅把 FOR SHARE 读改成 ErrNotFound CodeError.Code()==400,文案含 "部门不存在或已删除";SysUserModel.InsertWithTx 绝不被调用;DB 无新 user 并发/事务闭包 P0 DeleteDept 胜出时必须立即终止;若回退成"忽略 S 锁读的错"继续 Insert,就会产生挂在已删 deptId 上的 orphan user,其 DeptPath 校验永久失效

2.15 用户更新 POST /api/user/update (指针类型+DeptId可清零)

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0156 POST /api/user/update 正常更新 {"id":1,"nickname":"n","email":"[email protected]"} code=0 正常路径 P0 updateUserLogic
TC-0157 POST /api/user/update 不存在 {"id":9999} code=404 异常路径 P0 FindOne失败
TC-0158 POST /api/user/update 仅传id {"id":1} 仅updateTime变 分支覆盖 P1 所有指针nil
TC-0159 POST /api/user/update 清空nickname {"id":1,"nickname":""} DB nickname→空字符串 功能 P0 *string非nil+空值
TC-0160 POST /api/user/update 清空email {"id":1,"email":""} DB email→空字符串(跳过校验) 功能 P0 *email!=""判断
TC-0161 POST /api/user/update 清空remark {"id":1,"remark":""} DB remark清空 功能 P1 *string→""
TC-0162 POST /api/user/update 非法email格式 {"id":1,"email":"bad-email"} code=400, "邮箱格式不正确" 输入校验 P0 util.IsValidEmail
TC-0163 POST /api/user/update 非法phone格式 {"id":1,"phone":"12345"} code=400, "手机号格式不正确" 输入校验 P0 util.IsValidPhone
TC-0164 POST /api/user/update 合法phone {"id":1,"phone":"+8613800138000"} code=0 正常路径 P1 IsValidPhone通过
TC-0165 POST /api/user/update 不传email(nil) {"id":1,"nickname":"x"} email不变 分支覆盖 P1 req.Email==nil
TC-0166 POST /api/user/update DeptId设为0(取消部门) {"id":1,"deptId":0} DB deptId→0 功能 P0 *int64, *req.DeptId=0
TC-0167 POST /api/user/update DeptId设为正值 {"id":1,"deptId":5} DB deptId→5 正常路径 P0 *int64指针
TC-0168 POST /api/user/update DeptId不传(nil) {"id":1,"nickname":"x"} deptId不变 分支覆盖 P1 req.DeptId==nil
TC-0169 POST /api/user/update 超管不能冻结另一超管 caller=超管A, target=超管B, status=2 403 "不能通过此接口修改超级管理员的状态" 安全 P0 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 不变" 的更新 事务只走 UpdateProfileWithTxSysDeptModel.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 +1sys_user.deptId=0 安全/会话吊销 P0 SuperAdmin 的"移出部门树"动作对 DEV 成员同样构成收窄;覆盖 newDeptId==0 分支
TC-1173 POST /api/user/update NORMAL→NORMAL 不递增 tokenVersion target 在 NORMAL 部门 Areq.DeptId=<NORMAL 部门 B> DB sys_user.tokenVersion 与初值严格相等;deptId 落盘为 B 正向回归 P0 非收窄方向不得把合法用户误踢下线;与 TC-1171 对偶

2.16 用户列表/详情/状态 及其他用户操作

TC编号 接口/方法 测试场景 输入参数 (JSON) 预期结果 测试类型 优先级 覆盖说明
TC-0176 POST /api/user/list 含productCode {"productCode":"p1","page":1,"pageSize":10} 每用户含memberType(批量查) 正常路径 P0 FindMapByProductCodeUserIds
TC-0177 POST /api/user/list 不含productCode {"page":1} memberType全空,不调批量查 分支覆盖 P1 productCode=""
TC-0178 POST /api/user/list pageSize超过上限 {"pageSize":500} 实际pageSize=100 边界 P0 NormalizePage cap
TC-0179 POST /api/user/list 用户不在产品中 productCode指定,部分用户不是成员 memberType为空 分支覆盖 P1 memberMap无对应key
TC-0180 POST /api/user/list 批量查询DB异常 FindMapByProductCodeUserIds失败 code=500 异常路径 P1 err→透传
TC-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 + 1jwtauthMiddleware 的 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 共同守护

2.17 获取用户权限覆盖 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 中间件拦截

2.18 成员管理

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/* 只传 StatusMemberType 保持不变 {Status: Int64Ptr(2)},原 member.MemberType="ADMIN" DB:memberType 仍是 "ADMIN",status=2 契约 P0 部分更新语义
TC-1057 POST /api/member/* 只传 MemberTypeStatus 保持不变 {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 +1sys_product_member.status==2 安全/会话吊销 P0 冻结的成员不应继续持有生效 token
TC-1132 POST /api/member/update 降级 DEVELOPER→MEMBER:sys_user.tokenVersion +1 seed DEVELOPER,{MemberType:"MEMBER"} tokenVersion +1memberType=="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 让合法会话被无故踢下线

2.19 获取验证码 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 无条目直接拒绝

2.20 Cap.js 端点 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 缺失时兜底返回空

2.21 产品端 Cap.js 登录 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 超管不允许产品端登录

2.22 管理后台 Cap.js 登录 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 非超管拒绝管理后台登录

2.23 更新用户信息 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

2.24 MinIO 文件上传 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 解析失败

三、gRPC 接口测试用例

3.1 gRPC SyncPermissions

TC编号 接口/方法 测试场景 输入 预期结果 测试类型 优先级 覆盖说明
TC-0230 SyncPermissions 正常同步 valid req added/updated/disabled计数正确 正常路径 P0 permserver.go SyncPermissions
TC-0231 SyncPermissions appKey无效 invalid appKey codes.Unauthenticated 异常路径 P0 status.Error
TC-0232 SyncPermissions appSecret错误 wrong secret codes.Unauthenticated 异常路径 P0 status.Error
TC-0233 SyncPermissions 产品已禁用 disabled product codes.PermissionDenied 分支覆盖 P0 status.Error
TC-0234 SyncPermissions 验证disabled计数 DB有5条,perms含2条 disabled=3 功能验证 P0 RowsAffected

3.2 gRPC Login / RefreshToken / VerifyToken / GetUserPerms

TC编号 接口/方法 测试场景 输入 预期结果 测试类型 优先级 覆盖说明
TC-0235 Login 正常登录(普通用户+productCode) valid credentials + productCode token对+userInfo(含nickname) 正常路径 P0 permserver.go Login; resp.Nickname应返回用户昵称
TC-0236 Login 用户不存在 wrong username codes.Unauthenticated 异常路径 P0 status.Error
TC-0237 Login 密码错误 wrong password codes.Unauthenticated 异常路径 P0 status.Error
TC-0238 Login 账号冻结 frozen user codes.PermissionDenied 分支覆盖 P0 status.Error
TC-0239 Login 超管被拒绝 isSuperAdmin=1+productCode codes.PermissionDenied, "超级管理员不允许通过产品端登录" 安全 P0 IsSuperAdmin==1 → 拒绝
TC-0240 Login 普通用户+productCode 普通MEMBER+productCode perms含角色权限, memberType="MEMBER" 分支覆盖 P0 !isSuperAdmin && productCode!=""
TC-0241 Login 产品成员被禁用时拒绝登录 member.status=Disabled PermissionDenied 安全 P0 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:1111110.1.2.3:22222 第 1 次 Unauthenticated(业务放行);第 2 次 ResourceExhausted + "过于频繁" 安全/限流 P0 端口变化不得绕过限流
TC-0829 gRPC PermService 同 IP 两次 gRPC VerifyToken,quota=1 peer 10.9.8.7:3000110.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 双桶叠加

四、JWT中间件 / 统一响应测试用例

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0258 正常Bearer token Authorization: Bearer {valid} 通过, ctx注入5个值 正常路径 P0 middleware全路径
TC-0259 无Authorization头 无Header code=401, "未登录" 异常 P0 authHeader==""
TC-0260 无Bearer前缀 Authorization: xxx code=401, "token格式错误" 异常 P0 TrimPrefix相等
TC-0261 token签名错误 错误secret code=401, "token无效或已过期" 异常 P0 !Valid
TC-0262 token过期 expired code=401 异常 P0 jwt过期
TC-0263 claims类型断言失败 非标准claims code=401, "token无效或类型错误" 异常 P1 !ok 防御性分支,jwt.ParseWithClaims(&Claims{}) 下不可达;TokenType 检查由 TC-0264 覆盖
TC-0264 refresh token被拒绝 用refresh token访问API code=401, "token无效或类型错误" 安全 P0 TokenType="refresh"时拒绝
TC-0265 业务错误(CodeError) 触发404等 {code:业务码, msg:业务消息} 正常 P0 errors.As成功
TC-0266 内部错误 DB异常 {code:500, msg:"服务器内部错误"} 安全 P0 logx.Errorf+兜底
TC-0267 成功(有data) 正常请求 {code:0, msg:"ok", data:{...}} 正常 P0 v!=nil
TC-0268 成功(无data) 返回nil {code:0, msg:"ok"} 正常 P0 v==nil
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 类型解耦,所有调用方可复用

五、util 层测试用例

5.1 NormalizePage

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0269 正常值 page=2, pageSize=10 (2, 10) 正常路径 P0 无修正
TC-0270 page<=0 page=0, pageSize=10 (1, 10) 边界 P0 page<=0→1
TC-0271 page=-1 page=-1, pageSize=10 (1, 10) 边界 P0 page<=0→1
TC-0272 pageSize<=0 page=1, pageSize=0 (1, 20) 边界 P0 pageSize<=0→20
TC-0273 pageSize>100 page=1, pageSize=500 (1, 100) 边界-上限 P0 pageSize>100→100
TC-0274 pageSize=100 page=1, pageSize=100 (1, 100) 边界 P1 恰好不触发
TC-0275 pageSize=101 page=1, pageSize=101 (1, 100) 边界 P1 恰好触发
TC-0276 双零 page=0, pageSize=0 (1, 20) 边界 P1 两条件同时

5.2 IsValidEmail

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0277 正常邮箱 [email protected] true 正常路径 P0 标准格式
TC-0278 含点号 [email protected] true 正常路径 P1 允许点号
TC-0279 含加号 [email protected] true 正常路径 P1 允许加号
TC-0280 缺少@ userexample.com false 异常路径 P0 无@
TC-0281 缺少域名 user@ false 异常路径 P0 无域名
TC-0282 缺少TLD user@example false 异常路径 P0 TLD<2字符
TC-0283 空字符串 "" false 边界 P1

5.3 IsValidPhone

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0284 国内手机号 13800138000 true 正常路径 P0 11位数字
TC-0285 带+国际码 +8613800138000 true 正常路径 P0 +前缀
TC-0286 太短(6位) 123456 false 边界 P0 <7位
TC-0287 恰好7位 1234567 true 边界 P1 最小长度
TC-0288 最长15位 +123456789012345 true 边界 P1 最大长度
TC-0289 超长16位 1234567890123456 false 边界 P1 超限
TC-0290 包含字母 1380013abc false 异常路径 P0 非数字
TC-0291 空字符串 "" false 边界 P1

六、Logic 层单元测试用例

以下针对 Logic 层中的核心共享函数,使用 mock Model 接口进行纯单元测试。

6.1 auth/jwt.go — GenerateAccessToken

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0292 正常生成 secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"] 返回非空token, err=nil 正常路径 P0 jwt.NewWithClaims(HS256)
TC-0293 解析token验证claims 上述token ParseWithClaims可解析出正确userId/username/productCode/memberType/perms 功能验证 P0 claims完整性
TC-0294 空secret secret="" 仍能生成token(空key签名) 边界 P2 HS256 允许空key
TC-0295 空perms perms=nil token生成成功, 解析后perms=nil 边界 P1 nil slice
TC-0296 过期时间验证 expireSeconds=1, sleep 2s ParseWithClaims返回过期错误 功能验证 P0 ExpiresAt

6.2 auth/jwt.go — GenerateRefreshToken

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0297 正常生成 secret="s", expire=86400, userId=1, productCode="p" 返回非空token 正常路径 P0 RefreshClaims
TC-0298 解析验证 上述token ParseRefreshToken解析出userId=1, productCode="p" 功能验证 P0 往返一致
TC-0299 productCode为空 productCode="" 生成成功, 解析后productCode="" 边界 P1 空字符串

6.3 auth/jwt.go — ParseRefreshToken

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0300 正常解析 有效token+正确secret 返回RefreshClaims, err=nil 正常路径 P0 token.Valid
TC-0301 错误secret 有效token+错误secret err!=nil 异常路径 P0 签名验证失败
TC-0302 无效token字符串 "invalid-token" err!=nil 异常路径 P0 解析失败
TC-0303 空token "" err!=nil 边界 P1 空字符串
TC-0304 过期token 已过期的token err!=nil (token expired) 异常路径 P0 ExpiresAt已过
TC-0305 AccessToken误用 用AccessToken当RefreshToken解析 err!=nil (TokenType="access"≠"refresh") 安全 P0 TokenType字段校验

6.4 middleware — 辅助函数单元测试

M-5/M-6重构GetUserPerms (auth/perms.go) 以及 GetUsername / GetMemberType / IsSuperAdmin 等 context helper 作为死代码被移除;统一使用 GetUserDetails 读取完整 UserDetails 后访问字段。

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0306 GetUserId-正常 ctx含userId=100 100 正常路径 P0 类型断言成功
TC-0307 GetUserId-空ctx 空ctx 0 边界 P0 断言失败→零值
TC-0308 GetProductCode-正常 ctx含productCode="p1" "p1" 正常路径 P0 类型断言
TC-0309 GetUserDetails 返回完整字段 ctx含UserDetails{UserId,Username,ProductCode,MemberType,IsSuperAdmin} 读出字段全部一致, 空ctx返回nil 正常路径 P0 替代已移除的 GetUsername/GetMemberType/IsSuperAdmin

七、Model 层 _gen.go 模板生成方法测试用例

所有 9 个 Model 的 _gen.go 均由自定义模板 (cli/goctl/model/) 生成,包含非标准方法(批量操作、事务变体、buildBatchUpdateQuery 等)。 以下以 通用测试模式 列出,适用于全部 9 个 Model(注明差异部分)。

7.1 通用 CRUD 方法 (每个 Model 均需测试)

适用: SysUser, SysProduct, SysPerm, SysDept, SysRole, SysRolePerm, SysUserPerm, SysUserRole, SysProductMember

TC编号 方法 测试场景 预期结果 类型 优先级 覆盖说明
TC-0310 Insert 正常插入 返回Result+nil, DB有新记录 正常路径 P0 ExecCtx+缓存key清理
TC-0311 Insert 正常插入含TokenVersion err=nil, DB中tokenVersion=0(默认) 正常路径 P0 验证Insert包含tokenVersion
TC-0312 Insert 唯一索引冲突 返回DB错误(1062) 异常路径 P0 MySQL uk
TC-0313 Insert 缓存key生成正确 验证清理的缓存key包含主键和唯一索引 功能验证 P0 cacheSys*Prefix
TC-0314 InsertWithTx 事务内插入 使用session执行, 返回Result 正常路径 P0 session.ExecCtx
TC-0315 InsertWithTx 事务内插入含TokenVersion err=nil, 事务内可读到正确tokenVersion 正常路径 P0 验证InsertWithTx包含tokenVersion
TC-0316 InsertWithTx 事务回滚后无数据 事务内Insert+外部回滚→DB无记录 事务验证 P0 TransactCtx
TC-0317 FindOne 正常查询(缓存未命中) 返回记录, 缓存已写入 正常路径 P0 QueryRowCtx→DB
TC-0318 FindOne 正常查询(缓存命中) 不触发DB查询, 返回缓存数据 正常路径 P0 QueryRowCtx→cache
TC-0319 FindOne 记录不存在 返回ErrNotFound 异常路径 P0 sqlc.ErrNotFound→ErrNotFound
TC-0320 FindOne DB异常(非ErrNotFound) 返回原始error 异常路径 P1 default分支
TC-0321 FindOneWithTx 事务内正常查询 使用session.QueryRowCtx, 返回记录 正常路径 P0 session直查无缓存
TC-0322 FindOneWithTx 事务内记录不存在 返回ErrNotFound 异常路径 P0 sqlx.ErrNotFound
TC-0323 FindOneWithTx 事务内可见性 InsertWithTx后FindOneWithTx可读到 事务验证 P0 同session内可见
TC-0324 Update 正常更新 旧缓存key+新缓存key均被清理 正常路径 P0 FindOne→ExecCtx
TC-0325 Update 正常更新含TokenVersion err=nil, DB中tokenVersion正确更新 正常路径 P0 验证Update包含tokenVersion
TC-0326 Update 记录不存在 FindOne失败→返回ErrNotFound 异常路径 P0 FindOne err
TC-0327 UpdateWithTx 事务内更新 使用session, 缓存被清理 正常路径 P0 session.ExecCtx
TC-0328 Delete 正常删除 记录被删, 缓存key被清理 正常路径 P0 FindOne→ExecCtx DELETE
TC-0329 Delete 记录不存在 FindOne失败→返回ErrNotFound 异常路径 P0 FindOne err
TC-0330 DeleteWithTx 事务内删除 使用session, 缓存被清理 正常路径 P0 session.ExecCtx
TC-0331 TransactCtx 正常事务 fn执行成功→提交 正常路径 P0 conn.TransactCtx
TC-0332 TransactCtx fn返回错误 自动回滚 异常路径 P0 回滚
TC-0333 TableName 获取表名 返回正确表名(如 `sys_user`) 正常路径 P0 m.table

7.2 批量插入方法

TC编号 方法 测试场景 预期结果 类型 优先级 覆盖说明
TC-0334 BatchInsert 空列表 直接返回nil, 不执行SQL 边界 P0 len==0 early return
TC-0335 BatchInsert 单条记录 生成1组VALUES, 执行成功 正常路径 P0 单条
TC-0336 BatchInsert 多条记录(3条) 生成3组VALUES, SQL正确, 缓存key全清理 正常路径 P0 多条+缓存
TC-0337 BatchInsert 批量插入含TokenVersion err=nil, 所有记录tokenVersion正确 正常路径 P0 验证BatchInsert包含tokenVersion
TC-0338 BatchInsert 唯一索引冲突 全部失败, 返回DB错误 异常路径 P0 MySQL uk
TC-0339 BatchInsert 大批量(1000条) SQL长度合理, 执行成功 性能 P2 拼接性能
TC-0340 BatchInsertWithTx 空列表 直接返回nil 边界 P0 len==0
TC-0341 BatchInsertWithTx 正常多条 使用session执行 正常路径 P0 session.ExecCtx
TC-0342 BatchInsertWithTx 事务回滚 外部回滚→无新记录 事务验证 P0 TransactCtx

7.3 批量更新方法

TC编号 方法 测试场景 预期结果 类型 优先级 覆盖说明
TC-0343 BatchUpdate 空列表 直接返回nil 边界 P0 len==0 early return
TC-0344 BatchUpdate 单条记录 CASE-WHEN SQL正确, 更新成功 正常路径 P0 buildBatchUpdateQuery 单条
TC-0345 BatchUpdate 多条记录(3条) CASE-WHEN生成3个WHEN子句, 旧缓存key全清理 正常路径 P0 buildBatchUpdateQuery 多条
TC-0346 BatchUpdate 批量更新不污染数据 err=nil, tokenVersion/createTime/updateTime均正确 正常路径 P0 验证buildBatchUpdateQuery值对齐
TC-0347 BatchUpdate 部分id不存在 findListByPrimaryKeys返回部分→仅清理存在的缓存 边界 P1 oldList可能少于dataList
TC-0348 BatchUpdateWithTx 空列表 直接返回nil 边界 P0 len==0
TC-0349 BatchUpdateWithTx 正常多条 使用session执行 正常路径 P0 session.ExecCtx
TC-0350 buildBatchUpdateQuery 单条 SQL: UPDATE SET field=CASE WHEN id=? THEN ? ELSE field END WHERE id IN (?) 功能验证 P0 SQL结构
TC-0351 buildBatchUpdateQuery 多条 每个字段均有多个WHEN子句, WHERE IN含全部id 功能验证 P0 SQL正确性
TC-0352 buildBatchUpdateQuery vals数量正确 vals = N*(fields*2) + N (WHERE IN) 功能验证 P0 参数计数

7.4 批量删除方法

TC编号 方法 测试场景 预期结果 类型 优先级 覆盖说明
TC-0353 BatchDelete 空ids 直接返回nil 边界 P0 len==0 early return
TC-0354 BatchDelete 单个id DELETE WHERE id IN (?), 缓存清理 正常路径 P0 单条
TC-0355 BatchDelete 多个id(3个) 3个占位符, 旧数据查询→缓存key全清理 正常路径 P0 findListByPrimaryKeys
TC-0356 BatchDelete 包含不存在id findListByPrimaryKeys返回部分, 不报错 边界 P1 部分存在
TC-0357 BatchDeleteWithTx 空ids 直接返回nil 边界 P0 len==0
TC-0358 BatchDeleteWithTx 正常多条 使用session执行 正常路径 P0 session.ExecCtx

7.5 唯一索引查询方法 (按 Model 差异)

TC编号 Model 方法 测试场景 预期结果 优先级 覆盖说明
TC-0359 SysUser FindOneByUsername 正常查询 返回用户, 缓存写入 (索引缓存→主键缓存双层) P0 QueryRowIndexCtx
TC-0360 SysUser FindOneByUsername 不存在 返回ErrNotFound P0 sqlc.ErrNotFound
TC-0361 SysUser FindOneByUsernameWithTx 事务内正常查询 返回用户, 使用session直查 P0 session.QueryRowCtx
TC-0362 SysUser FindOneByUsernameWithTx 事务内不存在 返回ErrNotFound P0 sqlx.ErrNotFound
TC-0363 SysProduct FindOneByAppKey 正常查询 返回产品 P0 appKey唯一索引
TC-0364 SysProduct FindOneByAppKey 不存在 返回ErrNotFound P0
TC-0365 SysProduct FindOneByAppKeyWithTx 事务内正常查询 返回产品 P0 session直查
TC-0366 SysProduct FindOneByAppKeyWithTx 事务内不存在 返回ErrNotFound P0
TC-0367 SysProduct FindOneByCode 正常查询 返回产品 P0 code唯一索引
TC-0368 SysProduct FindOneByCode 不存在 返回ErrNotFound P0
TC-0369 SysProduct FindOneByCodeWithTx 事务内正常查询 返回产品 P0 session直查
TC-0370 SysProduct FindOneByCodeWithTx 事务内不存在 返回ErrNotFound P0
TC-0371 SysPerm FindOneByProductCodeCode 正常查询 返回权限(复合唯一索引) P0 productCode+code
TC-0372 SysPerm FindOneByProductCodeCode 不存在 返回ErrNotFound P0
TC-0373 SysPerm FindOneByProductCodeCodeWithTx 事务内正常查询 返回权限 P0 session直查
TC-0374 SysPerm FindOneByProductCodeCodeWithTx 事务内不存在 返回ErrNotFound P0
TC-0375 SysRole FindOneByProductCodeName 正常查询 返回角色(复合唯一索引) P0 productCode+name
TC-0376 SysRole FindOneByProductCodeName 不存在 返回ErrNotFound P0
TC-0377 SysRole FindOneByProductCodeNameWithTx 事务内正常查询 返回角色 P0 session直查
TC-0378 SysRole FindOneByProductCodeNameWithTx 事务内不存在 返回ErrNotFound P0
TC-0379 SysRolePerm FindOneByRoleIdPermId 正常查询 返回关联记录 P0 roleId+permId
TC-0380 SysRolePerm FindOneByRoleIdPermId 不存在 返回ErrNotFound P0
TC-0381 SysRolePerm FindOneByRoleIdPermIdWithTx 事务内正常查询 返回关联记录 P0 session直查
TC-0382 SysRolePerm FindOneByRoleIdPermIdWithTx 事务内不存在 返回ErrNotFound P0
TC-0383 SysUserPerm FindOneByUserIdPermId 正常查询 返回关联记录 P0 userId+permId
TC-0384 SysUserPerm FindOneByUserIdPermId 不存在 返回ErrNotFound P0
TC-0385 SysUserPerm FindOneByUserIdPermIdWithTx 事务内正常查询 返回关联记录 P0 session直查
TC-0386 SysUserPerm FindOneByUserIdPermIdWithTx 事务内不存在 返回ErrNotFound P0
TC-0387 SysUserRole FindOneByUserIdRoleId 正常查询 返回关联记录 P0 userId+roleId
TC-0388 SysUserRole FindOneByUserIdRoleId 不存在 返回ErrNotFound P0
TC-0389 SysUserRole FindOneByUserIdRoleIdWithTx 事务内正常查询 返回关联记录 P0 session直查
TC-0390 SysUserRole FindOneByUserIdRoleIdWithTx 事务内不存在 返回ErrNotFound P0
TC-0391 SysProductMember FindOneByProductCodeUserId 正常查询 返回成员记录 P0 productCode+userId
TC-0392 SysProductMember FindOneByProductCodeUserId 不存在 返回ErrNotFound P0
TC-0393 SysProductMember FindOneByProductCodeUserIdWithTx 事务内正常查询 返回成员记录 P0 session直查
TC-0394 SysProductMember FindOneByProductCodeUserIdWithTx 事务内不存在 返回ErrNotFound P0

7.6 内部辅助方法

TC编号 方法 测试场景 预期结果 类型 优先级 覆盖说明
TC-0395 findListByPrimaryKeys 空ids 返回空slice, 不执行SQL 边界 P0 len==0
TC-0396 findListByPrimaryKeys 正常ids 返回匹配记录(无缓存) 正常路径 P0 QueryRowsNoCacheCtx
TC-0397 findListByPrimaryKeys 部分不存在 仅返回存在的记录 边界 P1 IN查询
TC-0398 findListByPrimaryKeys DB异常 返回nil, err 异常路径 P1 err透传
TC-0399 getPrimaryKeyValue 正常 返回data.Id 功能验证 P0 interface{}
TC-0400 formatPrimary 正常 返回 "cache:sysXxx:id:{id}" 功能验证 P0 缓存key格式
TC-0401 queryPrimary 正常 执行 SELECT WHERE id=? 功能验证 P0 SQL

7.7 缓存key与前缀初始化

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0402 cachePrefix为空 cachePrefix="" 使用默认前缀 (如 "cache:sysUser:id:") 分支覆盖 P0 if cachePrefix!="" 未进入
TC-0403 cachePrefix非空 cachePrefix="test" 前缀变为 "test:cache:sysUser:id:" 分支覆盖 P0 if cachePrefix!="" 进入
TC-0404 多唯一索引前缀(SysProduct) cachePrefix="test" 3个缓存前缀均更新: id/appKey/code 功能验证 P0 3个变量均修改

八、Model 层自定义方法测试用例

8.1 SysUserModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0405 FindListByPage 正常分页 page=1, pageSize=10, DB有20条 返回10条+total=20 正常路径 P0 count+limit offset
TC-0406 FindListByPage 第二页 page=2, pageSize=10 offset=10, 返回后10条 正常路径 P0 (page-1)*pageSize
TC-0407 FindListByPage 空表 无数据 total=0, list为空 边界 P0 count=0
TC-0408 FindListByPage count查询失败 DB异常 返回0,0,err 异常路径 P1 第一个err
TC-0409 FindListByPage list查询失败 DB异常 返回0,total,err 异常路径 P1 第二个err
TC-0410 FindListByProductMembers 正常查询 productCode="p1", page=1, pageSize=10 返回该产品所有成员用户, total正确 正常路径 P0 替代FindListByDeptIds: INNER JOIN sys_product_member
TC-0411 FindListByProductMembers productCode不存在 productCode="no_such_pc" total=0, list空 边界 P1 JOIN 无匹配
TC-0412 FindByIds 正常批量查询 ids=[1,2,3] 返回3条 正常路径 P0 IN查询
TC-0413 FindByIds 空ids ids=[] 返回nil,nil 边界 P0 len==0
TC-0414 FindByIds 部分id不存在 ids=[1,9999] 仅返回存在的 边界 P1 IN不报错
TC-0415 FindByIds DB异常 连接失败 返回nil,err 异常路径 P1 err透传
TC-0416 FindIdsByDeptId 有用户的部门 deptId=1(有用户) 返回id列表 正常路径 P0 WHERE deptId=?
TC-0417 FindIdsByDeptId 无用户部门 deptId=999 空slice 边界 P1
TC-0418 UpdateProfile 状态未变-不递增tokenVersion statusChanged=false 成功, tokenVersion不变 正常路径 P0 非状态字段更新不影响会话
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", ...);预置 u1WRONG 两个 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.InvalidateProfileCacheUserDetailsLoader.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 FindActiveMemberUserIdsByProductCodeTxid 升序输出 乱序插入多个成员 返回切片严格按 id 升序 契约/死锁防护 P1 ORDER BY id:锁获取顺序稳定,避免与按主键扫描的其它事务互相死锁

8.2 SysProductModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0423 FindList 正常分页 page=1, pageSize=10 返回list+total 正常路径 P0 count+limit
TC-0424 FindList 空表 无数据 total=0, list空 边界 P0
TC-0425 FindList count失败 DB异常 返回err 异常路径 P1 第一个err

8.3 SysPermModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0426 FindListByProductCode 正常分页 productCode="p1", page=1, pageSize=10 list+total 正常路径 P0 WHERE productCode=?
TC-0427 FindListByProductCode 不存在的productCode "notexist" total=0, list空 边界 P1
TC-0428 FindAllCodesByProductCode 正常查询 DB有3条启用权限 返回3个code 正常路径 P0 SELECT code WHERE status=1
TC-0429 FindAllCodesByProductCode 空结果 无匹配 空slice 边界 P1
TC-0430 FindByIds 正常 ids=[1,2] 返回2条 正常路径 P0 IN查询
TC-0431 FindByIds 空ids [] 返回nil,nil 边界 P0 len==0
TC-0432 FindMapByProductCode 正常查询 productCode="p1" map[code]*SysPerm, key为code 正常路径 P0 result[p.Code]=p
TC-0433 FindMapByProductCode 空结果 无匹配 空map 边界 P1
TC-0434 FindMapByProductCode key唯一性 同productCode有3条 map长度=3 功能验证 P0 code唯一
TC-0435 DisableNotInCodesWithTx codes非空-正常 session+productCode="p1", codes=["a","b"], DB有a/b/c 禁用c, 返回affected=1 正常路径 P0 事务内 NOT IN
TC-0436 DisableNotInCodesWithTx codes为空-全部禁用 codes=[] 全部已启用的被禁用 分支覆盖 P0 len==0分支
TC-0437 DisableNotInCodesWithTx 无需禁用 codes包含所有已启用 affected=0 边界 P1 0行更新
TC-0438 DisableNotInCodesWithTx DB异常 session.Exec报错 返回0,err 异常路径 P1 err
TC-0439 FindAllCodesByProductCode 有权限产品 productCode="p1" 返回code列表(仅status=1) 正常路径 P0 WHERE status=1
TC-0440 FindAllCodesByProductCode 无权限产品 productCode="notexist" 空slice 边界 P1
TC-0441 FindAllCodesByProductCode 全部已禁用 所有perm.status=2 空slice 边界 P1 WHERE status=1 过滤, 全禁用时返回空
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 严格等值,未来枚举值不被误伤

8.4 SysDeptModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0442 FindAll 正常查询 DB有5条 返回5条, 按sort asc排序 正常路径 P0 ORDER BY sort, id
TC-0443 FindAll 空表 无数据 空slice 边界 P0
TC-0444 FindByParentId 正常查询 parentId=1 返回子部门列表 正常路径 P0 WHERE parentId=?
TC-0445 FindByParentId 无子部门 parentId=999 空slice 边界 P1
TC-0446 FindByPathPrefix 正常查询 pathPrefix="/1/" 返回路径以/1/开头的部门 正常路径 P0 LIKE pathPrefix%
TC-0447 FindByPathPrefix LIKE注入已阻止 pathPrefix含% 空slice(%和_已转义,不作为通配符) 安全 P1 NewReplacer转义
TC-0448 FindByPathPrefix 无匹配 "/999/" 空slice 边界 P1

8.5 SysRoleModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0449 FindListByProductCode 正常分页 productCode="p1" 按permsLevel asc排序 正常路径 P0 ORDER BY permsLevel, id
TC-0450 FindListByProductCode 空结果 无匹配 total=0 边界 P1
TC-0451 FindByIds 正常 ids=[1,2] 返回2条 正常路径 P0 IN查询
TC-0452 FindByIds 空ids [] 返回nil,nil 边界 P0 len==0
TC-0453 FindMinPermsLevelByUserIdAndProductCode 有角色用户 userId=1, productCode="p1" 返回最小permsLevel 正常路径 P0 MIN聚合
TC-0454 FindMinPermsLevelByUserIdAndProductCode 无角色用户 userId=无角色用户 error(ErrNotFound) 边界 P0 IFNULL返回-1→level<0→ErrNotFound
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 保持通用

8.6 SysRolePermModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0455 FindPermIdsByRoleId 正常查询 roleId=1, DB有3条 返回3个permId 正常路径 P0 SELECT permId WHERE roleId=?
TC-0456 FindPermIdsByRoleId 无绑定 roleId=999 空slice 边界 P1
TC-0457 FindPermIdsByRoleIds 正常查询 roleIds=[1,2] 返回去重后的permId 正常路径 P0 DISTINCT + IN
TC-0458 FindPermIdsByRoleIds 空roleIds [] 返回nil,nil 边界 P0 len==0
TC-0459 FindPermIdsByRoleIds 去重验证 两角色有相同permId 结果中permId不重复 功能验证 P0 DISTINCT
TC-0460 DeleteByRoleIdTx 正常事务内删除 session+roleId 使用session执行 正常路径 P0 session.ExecCtx
TC-0461 DeleteByRoleIdTx 无绑定 session+roleId=999 删0行, 不报错 边界 P1 session.ExecCtx, affected=0 不报错

8.7 SysUserPermModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0462 FindPermIdsByUserIdAndEffectForProduct ALLOW-指定产品 userId=1, effect="ALLOW", productCode="p1" 返回该用户在该产品下ALLOW的permIds 正常路径 P0 跨产品权限隔离
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=? 隔离其他产品

8.8 SysUserRoleModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0467 FindRoleIdsByUserId 正常查询 userId=1, DB有3条 返回3个roleId 正常路径 P0 SELECT roleId
TC-0468 FindRoleIdsByUserId 无绑定 userId=999 空slice 边界 P1
TC-0469 DeleteByRoleIdTx 正常删除 session+roleId 删除该角色的所有用户绑定 正常路径 P0 session.ExecCtx
TC-0470 DeleteByUserIdForProductTx 事务内跨产品删除 session+userId+productCode 使用session 正常路径 P0 session.ExecCtx
TC-0471 DeleteByUserIdForProductTx 跨产品隔离 用户在多产品有角色 仅删目标产品的 深度业务 P0 WHERE userId=? AND productCode=? 子查询过滤
TC-0472 FindUserIdsByRoleId 有绑定的角色 roleId=1 返回userId列表 正常路径 P0 WHERE roleId=?
TC-0473 FindUserIdsByRoleId 无绑定角色 roleId=999 空slice 边界 P1
TC-0474 FindRoleIdsByUserIdForProduct 跨产品过滤 userId=1, productCode="p1" 仅返回该产品下绑定的roleId 深度业务 P0 差量更新依赖此接口
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) 均返 ErrNotFoundFindOneByUserIdRoleId(userId, R) 也均返 ErrNotFound;无脏读 契约/缓存 P0 两套 key 都得失效
TC-1062 SysUserRoleModel DeleteByUserIdAndRoleIdsTx 只删指定 (userId, roleIds) 集合后同上 预置多条;删除部分;剩余部分保留 被删的全返 ErrNotFound;未被删的 FindOne 正常命中;两层 cache key 对齐 契约/缓存 P0 组合 key 的定点失效不误伤

8.9 SysProductMemberModel

TC编号 方法 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0475 FindListByProductCode 正常分页 productCode="p1" list+total 正常路径 P0 WHERE productCode=?
TC-0476 FindListByProductCode 空结果 无匹配 total=0 边界 P1
TC-0477 FindMapByProductCodeUserIds 正常批量 productCode="p1", userIds=[1,2] map key=userId 正常路径 P0 IN+productCode
TC-0478 FindMapByProductCodeUserIds 空userIds [] 返回空map 边界 P0 len==0
TC-0479 FindMapByProductCodeUserIds 部分不是成员 userIds含非成员 map仅含成员 边界 P1
TC-0480 FindMapByProductCodeUserIds map key正确 查询结果 key=userId, val=*SysProductMember 功能验证 P0 result[pm.UserId]
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) 返回与插入一致的 *SysProductMemberId/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

九、访问控制 (auth/access.go)

9.1 RequireSuperAdmin

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0481 超管通过 ctx含SuperAdmin UserDetails nil (允许) 正常路径 P0 caller.IsSuperAdmin
TC-0482 非超管拒绝 ctx含ADMIN UserDetails 403 "仅超级管理员" 异常路径 P0 !IsSuperAdmin
TC-0483 MEMBER拒绝 ctx含MEMBER UserDetails 403 "仅超级管理员" 异常路径 P0
TC-0484 未登录 ctx无UserDetails 401 "未登录" 边界 P0 caller==nil

9.2 RequireProductAdminFor(ctx, targetProductCode)

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0485 超管通过 ctx含SuperAdmin, productCode="p1" nil 正常路径 P0 IsSuperAdmin
TC-0486 ADMIN通过(同产品) ctx含ADMIN(p1), productCode="p1" nil 正常路径 P0 MemberType==ADMIN且productCode匹配
TC-0487 DEVELOPER拒绝 ctx含DEVELOPER(p1), productCode="p1" 403 异常路径 P0 非Admin
TC-0488 MEMBER拒绝 ctx含MEMBER(p1), productCode="p1" 403 异常路径 P0 非Admin
TC-0489 未登录 ctx无UserDetails, productCode="p1" 401 边界 P0 caller==nil
TC-0490 ADMIN跨产品拒绝 ctx含ADMIN(p1), productCode="other" 403 安全 P0 productCode不匹配→拒绝

9.3 CheckMemberTypeAssignment

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0491 超管可分配任何类型 caller=SuperAdmin, assigned=ADMIN nil 正常路径 P0 IsSuperAdmin豁免
TC-0492 ADMIN分配DEVELOPER caller=ADMIN, assigned=DEVELOPER nil 正常路径 P0 callerPri(1) < assignPri(2)
TC-0493 ADMIN分配ADMIN(同级拒绝) caller=ADMIN, assigned=ADMIN 403 深度业务 P0 callerPri >= assignPri
TC-0494 DEVELOPER分配ADMIN(越级拒绝) caller=DEVELOPER, assigned=ADMIN 403 深度业务 P0 callerPri > assignPri
TC-0495 MEMBER分配MEMBER(同级拒绝) caller=MEMBER, assigned=MEMBER 403 深度业务 P0
TC-0496 未登录 ctx无UserDetails 401 边界 P0

9.4 CheckManageAccess

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0497 超管可管理任何人 caller=SuperAdmin nil 正常路径 P0 IsSuperAdmin豁免
TC-0498 操作自己 caller.UserId==targetUserId nil 正常路径 P0 self豁免
TC-0499 ADMIN跳过部门检查 caller=ADMIN nil (直接比级别) 深度业务 P0 checkDeptHierarchy ADMIN豁免
TC-0500 非ADMIN无部门拒绝 caller.DeptId=0 403 "未归属部门" 边界 P0 caller.DeptId==0
TC-0501 目标用户无部门 target.DeptId=0 403 "目标用户未归属部门" 边界 P0 target.DeptId==0
TC-0502 目标在不同部门 目标不在caller子部门 403 "无权管理其他部门" 深度业务 P0 !HasPrefix
TC-0503 未登录 ctx无UserDetails 401 边界 P0
TC-0504 caller.DeptPath为空时拒绝 caller有DeptId但DeptPath="" 403 且文案含"未归属任何部门"(与 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=trueFindMinPermsLevelByUserIdAndProductCode 不得被调用(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 ErrNotFoundNoRole=true,不翻 500 mock 返 sqlx.ErrNotFound snap.NoRole=trueCheckRoleLevelAgainst(999) 仍 403 "没有可分配的角色等级" 契约 P1 loadFreshMinPermsLevel 的口径对称,保留 ErrNotFound → 最低级 的原契约
TC-1020 caller 其他 DB 错误 → fail-close 500 mock 返通用 error CodeError.Code()==500 安全 P0 DB 抖动不得被同化为"无角色 → 最低级"放行,保持与 L-4 修复一致

9.5 memberTypePriority

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0505 各类型优先级正确 全部4种+未知 SA=0,A=1,D=2,M=3,unknown=MaxInt32 白盒 P0 switch分支

十、UserDetailsLoader (loaders/userDetailsLoader.go)

10.1 Load / 缓存

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0506 DB加载(缓存miss) 有效userId+productCode 返回完整UserDetails 正常路径 P0 loadFromDB全链路
TC-0507 缓存命中 第二次Load同key 从Redis返回,不查DB 正常路径 P0 GetCtx hit
TC-0508 用户不存在 userId=999999 返回零值UserDetails(Status=0) 边界 P0 loadUser失败
TC-0509 productCode为空 productCode="" 跳过产品/成员/角色/权限加载 边界 P1 各方法guard
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 调用方可稳定识别

10.2 缓存失效

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0510 Del删除指定缓存 Del(uid, pc) 缓存被删除,下次Load查DB 正常路径 P0 DelCtx
TC-0511 Clean清除用户所有产品缓存 Clean(uid) 该用户所有key被删 正常路径 P0 KEYS pattern
TC-0512 CleanByProduct清除产品所有用户 CleanByProduct(pc) 该产品所有key被删 正常路径 P0 KEYS pattern
TC-0513 BatchDel批量删除 BatchDel([uid1,uid2], pc) 多个key被删 正常路径 P0 DelCtx多key
TC-0514 BatchDel空数组 BatchDel([], pc) 无操作 边界 P1 len==0 guard
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==truedeadline ∈ [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.Canceledcontext.DeadlineExceedederrors.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,修复后分类记录

10.3 loadPerms权限计算

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0515 超管全量权限 IsSuperAdmin=true Perms=全部启用的权限码 正常路径 P0 超管分支
TC-0516 ADMIN全量权限 MemberType=ADMIN Perms=全量 正常路径 P0 ADMIN分支
TC-0517 DEVELOPER全量权限 MemberType=DEVELOPER Perms=全量 正常路径 P0 DEVELOPER分支
TC-0518 DEV部门全量权限 DeptType=DEV Perms=全量 正常路径 P0 DeptTypeDev分支
TC-0519 MEMBER角色权限+ALLOW-DENY 有角色+ALLOW+DENY 正确计算 深度业务 P0 denySet过滤
TC-0520 用户ALLOW权限不跨产品泄漏 用户在产品A/B各有ALLOW权限 加载产品A时仅含A权限,不含B权限 安全 P0 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 loadPermsif !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

10.4 loadRoles + MinPermsLevel

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0522 多角色取最小permsLevel 用户有level=10和level=5的角色 MinPermsLevel=5 正常路径 P0 min计算
TC-0523 无角色 用户无角色 MinPermsLevel=MaxInt64 边界 P0 默认值
TC-0524 角色跨产品过滤 角色在不同产品 仅加载当前产品角色 深度业务 P0 productCode过滤
TC-0525 禁用角色不计入 角色status=2 不在Roles列表中 深度业务 P0 Status==Enabled

10.5 loadMembership

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0526 超管自动设置SUPER_ADMIN IsSuperAdmin=true MemberType=SUPER_ADMIN, 不查DB 正常路径 P0 早期return
TC-0527 非成员MemberType为空 用户非该产品成员 MemberType="" 边界 P0 ErrNotFound
TC-0528 禁用成员MemberType为空 member.status=Disabled ud.MemberType="" 安全 P0 loadMembership
TC-1207 errors.Is 语义稳健性:ErrNotFound 包装后仍被正确识别 productmember.ErrNotFound 来自 sqlx.ErrNotFound;若 model 层未来包装为 fmt.Errorf("%w", err)errors.Is 仍应成立 errors.Is(productmember.ErrNotFound, sqlx.ErrNotFound) == trueerrors.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

十二、Logic层 — 访问控制负面测试

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0532 createDept非超管拒绝 ctx=ADMIN 403 "仅超级管理员" 安全 P0 RequireSuperAdmin
TC-0533 updateDept非超管拒绝 ctx=ADMIN 403 "仅超级管理员" 安全 P0 RequireSuperAdmin
TC-0534 deleteDept非超管拒绝 ctx=ADMIN 403 "仅超级管理员" 安全 P0 RequireSuperAdmin
TC-0535 createProduct非超管拒绝 ctx=ADMIN 403 "仅超级管理员" 安全 P0 RequireSuperAdmin
TC-0536 updateProduct非超管拒绝 ctx=ADMIN 403 "仅超级管理员" 安全 P0 RequireSuperAdmin
TC-0537 createUser非产品管理员拒绝 ctx=MEMBER 403 "仅超级管理员或产品管理员" 安全 P0 RequireProductAdminFor(ctx, productCode)
TC-0538 createRole非产品管理员拒绝 ctx=MEMBER 403 安全 P0 RequireProductAdminFor(ctx, productCode)
TC-0539 updateRole非产品管理员拒绝 ctx=MEMBER 403 安全 P0 RequireProductAdminFor(ctx, productCode)
TC-0540 deleteRole非产品管理员拒绝 ctx=MEMBER 403 安全 P0 RequireProductAdminFor(ctx, productCode)
TC-0541 bindRolePerms非产品管理员拒绝 ctx=MEMBER 403 安全 P0 RequireProductAdminFor(ctx, productCode)
TC-0542 updateUser-MEMBER不能管理他人 ctx=MEMBER, id!=self 403 (CheckManageAccess拒绝) 安全 P0 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 畸形时等价于未登录

十三、限流中间件 (middleware/ratelimitMiddleware.go)

TC编号 测试场景 输入 预期结果 类型 优先级 覆盖说明
TC-0546 正常请求(未超限) 首次请求 请求正常通过, next被调用 正常路径 P0 code!=OverQuota→next
TC-0547 超限请求被拒绝 超出配额后的请求 code=429, "请求过于频繁,请稍后再试" 异常路径 P0 code==OverQuota→ErrTooManyRequests
TC-0548 behindProxy=false时XFF被忽略 behindProxy=false, 不同XFF头+相同RemoteAddr 仍被限流, nextCount保持为1 安全 P0 behindProxy=false: 仅用RemoteAddr
TC-0549 behindProxy=false时X-Real-IP被忽略 behindProxy=false, 不同XRI头+相同RemoteAddr 仍被限流, nextCount保持为1 安全 P0 behindProxy=false: 仅用RemoteAddr
TC-0550 IP从RemoteAddr解析 无代理头, RemoteAddr="ip:port" 使用SplitHostPort解析host作为限流key 分支覆盖 P0 SplitHostPort解析host
TC-0551 不同IP独立限流 两个不同IP 各自独立计数, 互不影响 功能验证 P0 key隔离
TC-0552 behindProxy=true时信任X-Real-IP behindProxy=true, 不同X-Real-IP头 按X-Real-IP独立限流 正常路径 P0 behindProxy=true: X-Real-IP优先
TC-0553 behindProxy=true时无X-Real-IP回退RemoteAddr behindProxy=true, 无X-Real-IP头 使用RemoteAddr作为限流key 分支覆盖 P0 X-Real-IP为空→fallback RemoteAddr
TC-0554 behindProxy=true 时 XFF 仍被忽略 已删除:M-6 显式反转契约 —— behindProxy=true 时 XFF 首段合法应优先;新契约由 TC-0862~0866(ratelimitMiddlewareXff_audit_test.go)覆盖
TC-0555 RemoteAddr无端口格式 RemoteAddr="1.2.3.4"(无端口) 返回原始RemoteAddr "1.2.3.4" 边界 P1 SplitHostPort失败→r.RemoteAddr
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 不信任客户端注入的头部

用户凭证票据机制(CreateUser / ResetPassword / FetchUserCredentials)

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原子性