userDetailsLoaderCleanByUserIds_audit_test.go 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. package loaders
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // ---------------------------------------------------------------------------
  9. // 覆盖目标:审计第 6 轮 M-1 修复回归 —— UserDetailsLoader.CleanByUserIds
  10. // * 对多个 userId 的所有产品下缓存 + 用户索引 key 必须整体删除;
  11. // * 对空输入必须立即返回,不打 Redis;
  12. // * 基于 SUNION + 批 DEL,单次 RTT 常数,调用方(UpdateDept)调一次即可清完。
  13. // ---------------------------------------------------------------------------
  14. // TC-0846: 预埋 3 用户 × 2 产品的缓存,CleanByUserIds 一次性清光所有 ud: key 与 idx: key。
  15. func TestCleanByUserIds_WipesAllUserProductKeysAndIndexes(t *testing.T) {
  16. rds := testRedis()
  17. loader := newTestLoader()
  18. ctx := context.Background()
  19. type cell struct {
  20. uid int64
  21. pc string
  22. }
  23. cells := []cell{
  24. {1000001, "pcX"}, {1000001, "pcY"},
  25. {1000002, "pcX"}, {1000002, "pcY"},
  26. {1000003, "pcX"}, {1000003, "pcY"},
  27. }
  28. // 预埋缓存:每个 cell 写一条 value 到 cacheKey,并 SADD 到 user / product 索引。
  29. cacheKeys := make([]string, 0, len(cells))
  30. for _, c := range cells {
  31. ck := loader.cacheKey(c.uid, c.pc)
  32. require.NoError(t, rds.SetCtx(ctx, ck, "dummy"))
  33. _, _ = rds.SaddCtx(ctx, loader.userIndexKey(c.uid), ck)
  34. _, _ = rds.SaddCtx(ctx, loader.productIndexKey(c.pc), ck)
  35. cacheKeys = append(cacheKeys, ck)
  36. }
  37. // 调用 CleanByUserIds 触发 SUNION + 批 DEL。
  38. loader.CleanByUserIds(ctx, []int64{1000001, 1000002, 1000003})
  39. // 6 条 ud: key 必须全消失。
  40. for _, ck := range cacheKeys {
  41. exist, err := rds.ExistsCtx(ctx, ck)
  42. require.NoError(t, err)
  43. assert.False(t, exist, "M-1:cacheKey %s 必须被清理", ck)
  44. }
  45. // 3 条 user 索引 key 必须也被清掉(否则会漏缓存)。
  46. for _, uid := range []int64{1000001, 1000002, 1000003} {
  47. exist, err := rds.ExistsCtx(ctx, loader.userIndexKey(uid))
  48. require.NoError(t, err)
  49. assert.False(t, exist,
  50. "M-1:user 索引集合必须被 DEL,否则下次 Clean 会复活假指针")
  51. }
  52. // 清理 product 索引残留(修复 SLA 不负责 product 索引,其残留 key 已在 user 索引里一并清掉
  53. // 的那一组;但为了测试幂等性,手动 cleanup)。
  54. t.Cleanup(func() {
  55. _, _ = rds.DelCtx(ctx, loader.productIndexKey("pcX"), loader.productIndexKey("pcY"))
  56. })
  57. }
  58. // TC-0847: 空 ids 切片必须直接返回,不打 Redis。
  59. // 如果源码退化成把空 SUNION 交给 Redis,会收到 "SUNION wrong number of arguments" 错误;
  60. // 我们通过断言 Redis 未产生任何错误以及函数未 panic 来验证。
  61. func TestCleanByUserIds_EmptyIds_NoOp(t *testing.T) {
  62. loader := newTestLoader()
  63. // 只要不 panic、返回即可;如果源码 foundation 有 wrong-args 会 logx.Errorf 输出,
  64. // 这里做最小断言:调用返回控制权。
  65. loader.CleanByUserIds(context.Background(), nil)
  66. loader.CleanByUserIds(context.Background(), []int64{})
  67. // 若走到了 SUNION 分支,Redis 会在 wrong-args 下被 logx 记 Errorf,
  68. // 业务回调仍然返回,此时不应 panic;通过到达本行说明 OK。
  69. }