userDetailsLoader_batchdel_mn2_audit_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. package loaders
  2. import (
  3. "context"
  4. "testing"
  5. "perms-system-server/internal/consts"
  6. productModel "perms-system-server/internal/model/product"
  7. userModel "perms-system-server/internal/model/user"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. )
  11. // ---------------------------------------------------------------------------
  12. // 覆盖目标:审计 M-N2 修复 —— BatchDel 必须把 userIndex / productIndex 的 SREM
  13. // 合进一次 Pipelined RTT,历史 per-user 串行路径下"角色绑千人"时尾延迟会抬到秒级。
  14. // 本测试以"主缓存 + user/product 索引集合"的最终一致性为切入点,在 N 条数据下验证:
  15. // (1) 主 cacheKey 全量清空
  16. // (2) 每个 userIndex 集合中对应的 cacheKey 已经被 SREM
  17. // (3) productIndex 集合中所有 cacheKey 也已经被 SREM
  18. // 相比只断言 (1) 的 TC-0513,本 TC 把 index 一致性钉死,防止 pipelined 分支被回退到
  19. // "只 DEL 主 key、遗漏 SREM"的静默回归。
  20. // ---------------------------------------------------------------------------
  21. // TC-1013: M-N2 —— BatchDel 必须同步清理 userIndex / productIndex 中的 cacheKey 集合(Pipelined)。
  22. func TestUserDetailsLoader_MN2_BatchDelClearsUserAndProductIndexes(t *testing.T) {
  23. ctx := context.Background()
  24. conn := testConn()
  25. m := testModels()
  26. loader := newTestLoader()
  27. rds := testRedis()
  28. ts := now()
  29. pcode := "mn2_" + uniqueId()
  30. // 插入两个用户 + 一个真实产品,确保 Load 走到 5 分钟正缓存分支并注册索引
  31. uid1 := uniqueId()
  32. uid2 := uniqueId()
  33. userId1 := insertUser(ctx, t, m, &userModel.SysUser{
  34. Username: uid1, Password: hashPwd("pass123"), Nickname: "nick_" + uid1,
  35. Email: uid1 + "@t.com", Phone: "13800000008", DeptId: 0,
  36. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  37. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  38. })
  39. userId2 := insertUser(ctx, t, m, &userModel.SysUser{
  40. Username: uid2, Password: hashPwd("pass123"), Nickname: "nick_" + uid2,
  41. Email: uid2 + "@t.com", Phone: "13800000009", DeptId: 0,
  42. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  43. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  44. })
  45. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  46. Code: pcode, Name: "p_" + pcode, AppKey: "ak_" + pcode, AppSecret: "as_" + pcode,
  47. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  48. })
  49. t.Cleanup(func() {
  50. loader.Del(ctx, userId1, pcode)
  51. loader.Del(ctx, userId2, pcode)
  52. cleanTable(ctx, conn, "`sys_product`", pid)
  53. cleanTable(ctx, conn, "`sys_user`", userId1, userId2)
  54. })
  55. // 把缓存一次预热,让 userIndex/productIndex 被 registerCacheKey 真实写入
  56. _, err := loader.Load(ctx, userId1, pcode)
  57. require.NoError(t, err)
  58. _, err = loader.Load(ctx, userId2, pcode)
  59. require.NoError(t, err)
  60. k1 := loader.cacheKey(userId1, pcode)
  61. k2 := loader.cacheKey(userId2, pcode)
  62. pIdx := loader.productIndexKey(pcode)
  63. u1Idx := loader.userIndexKey(userId1)
  64. u2Idx := loader.userIndexKey(userId2)
  65. // 预检:主 key 写入、productIndex / userIndex 存在对应元素
  66. for _, k := range []string{k1, k2} {
  67. val, gerr := rds.GetCtx(ctx, k)
  68. require.NoError(t, gerr)
  69. require.NotEmpty(t, val, "M-N2 预检:主 cacheKey 必须被写入才有意义")
  70. }
  71. has, _ := rds.SismemberCtx(ctx, pIdx, k1)
  72. require.True(t, has, "M-N2 预检:productIndex 必须含 k1")
  73. has, _ = rds.SismemberCtx(ctx, pIdx, k2)
  74. require.True(t, has, "M-N2 预检:productIndex 必须含 k2")
  75. has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
  76. require.True(t, has, "M-N2 预检:userIndex(u1) 必须含 k1")
  77. has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
  78. require.True(t, has, "M-N2 预检:userIndex(u2) 必须含 k2")
  79. // 触发被测路径:BatchDel(pipelined SREM)
  80. loader.BatchDel(ctx, []int64{userId1, userId2}, pcode)
  81. // 主 key 被清空(原 TC-0513 已保障)
  82. for _, k := range []string{k1, k2} {
  83. val, _ := rds.GetCtx(ctx, k)
  84. assert.Empty(t, val, "M-N2:BatchDel 必须删除主 cacheKey")
  85. }
  86. // userIndex / productIndex 中的对应 cacheKey 必须被 SREM 清除(本 TC 核心断言)
  87. has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
  88. assert.False(t, has, "M-N2:BatchDel 必须把 k1 从 userIndex(u1) SREM 出去")
  89. has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
  90. assert.False(t, has, "M-N2:BatchDel 必须把 k2 从 userIndex(u2) SREM 出去")
  91. has, _ = rds.SismemberCtx(ctx, pIdx, k1)
  92. assert.False(t, has, "M-N2:BatchDel 必须把 k1 从 productIndex SREM 出去")
  93. has, _ = rds.SismemberCtx(ctx, pIdx, k2)
  94. assert.False(t, has, "M-N2:BatchDel 必须把 k2 从 productIndex SREM 出去")
  95. }
  96. // TC-1014: M-N2 —— productCode 为空时 BatchDel 仅 SREM userIndex,不得 panic 或误访问 productIndex。
  97. // 目前业务侧 BatchDel 的所有调用都传了 productCode;但 pipeline 分支必须对空串 fail-safe,
  98. // 防止未来调用方误传时 pipeline 里塞空 key 把 Redis 侧写脏。
  99. func TestUserDetailsLoader_MN2_BatchDelEmptyProductCodeDoesNotPanic(t *testing.T) {
  100. ctx := context.Background()
  101. loader := newTestLoader()
  102. // 即便 uid 不存在,pipelined SREM 对不存在的集合是 no-op,不应报错/panic
  103. require.NotPanics(t, func() {
  104. loader.BatchDel(ctx, []int64{9999999991, 9999999992}, "")
  105. })
  106. }