deleteCacheKey_r11_2_audit_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package userrole
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "perms-system-server/internal/testutil"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "github.com/zeromicro/go-zero/core/stores/redis"
  11. "github.com/zeromicro/go-zero/core/stores/sqlx"
  12. )
  13. // ---------------------------------------------------------------------------
  14. // 覆盖目标:审计 L-R11-2 —— Delete 族的 SELECT 从 * 裁剪到 id/userId/roleId。
  15. // 行为契约不得因此回归:
  16. // 1) 每一行被删除后,id 维度 + 复合维度(userId+roleId)的缓存 key 必须全部失效;
  17. // 2) 裁剪后仍然能正确处理"同一 userId 下多条 role 绑定"的批量失效,绝不能只删到
  18. // 第一条行的缓存 key 就提前返回;
  19. // 3) 经过 Delete 后再 FindOne 必须回落到 DB 的 ErrNotFound,不能沿用缓存里的旧行。
  20. //
  21. // 这里选 sys_user_role 作为三个 Delete-Tx 家族(userrole/roleperm/userperm/perm)的
  22. // 代表:其 DeleteByRoleIdTx / DeleteByUserIdForProductTx / DeleteByUserIdAndRoleIdsTx
  23. // 三个方法都走同一条 SELECT-only-key-cols → DELETE 的路径,成一族即覆盖整个收敛面。
  24. // ---------------------------------------------------------------------------
  25. func seedUserRoleWithPrimedCache(t *testing.T, ctx context.Context, m SysUserRoleModel, userId, roleId int64) int64 {
  26. t.Helper()
  27. ts := time.Now().Unix()
  28. res, err := m.Insert(ctx, &SysUserRole{
  29. UserId: userId, RoleId: roleId, CreateTime: ts, UpdateTime: ts,
  30. })
  31. require.NoError(t, err)
  32. id, err := res.LastInsertId()
  33. require.NoError(t, err)
  34. _, err = m.FindOne(ctx, id)
  35. require.NoError(t, err, "FindOne 为 id 维度缓存预热")
  36. _, err = m.FindOneByUserIdRoleId(ctx, userId, roleId)
  37. require.NoError(t, err, "FindOneByUserIdRoleId 为复合维度缓存预热")
  38. return id
  39. }
  40. func assertCacheKeysGone(t *testing.T, rds *redis.Redis, idKey, compositeKey string) {
  41. t.Helper()
  42. got, err := rds.Get(idKey)
  43. require.NoError(t, err)
  44. assert.Empty(t, got, "L-R11-2:id 维度缓存 key %q 应被 DELETE 一并失效", idKey)
  45. got, err = rds.Get(compositeKey)
  46. require.NoError(t, err)
  47. assert.Empty(t, got, "L-R11-2:复合维度缓存 key %q 应被 DELETE 一并失效", compositeKey)
  48. }
  49. // TC-1062: L-R11-2 —— DeleteByRoleIdTx 对多行同时失效 id + composite 缓存
  50. func TestSysUserRoleModel_DeleteByRoleIdTx_InvalidatesAllKeyCols(t *testing.T) {
  51. ctx := context.Background()
  52. conn := testutil.GetTestSqlConn()
  53. m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  54. rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
  55. cachePrefix := testutil.GetTestCachePrefix()
  56. roleId := randUserRoleId()
  57. u1, u2 := randUserRoleId(), randUserRoleId()
  58. if u1 == u2 {
  59. u2 = u1 + 1
  60. }
  61. id1 := seedUserRoleWithPrimedCache(t, ctx, m, u1, roleId)
  62. id2 := seedUserRoleWithPrimedCache(t, ctx, m, u2, roleId)
  63. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "sys_user_role", id1, id2) })
  64. require.NoError(t,
  65. m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  66. return m.DeleteByRoleIdTx(c, session, roleId)
  67. }))
  68. // L-R11-2 裁剪 SELECT 为三列后,构造缓存 key 需要的 id/userId/roleId 仍被携带回来。
  69. // 任何一条遗漏都会让这里的断言炸掉(旧行的缓存还在,FindOne 就能从缓存拿到"幽灵行")。
  70. for _, c := range []struct {
  71. id, uid int64
  72. }{{id1, u1}, {id2, u2}} {
  73. idKey := fmt.Sprintf("%s:cache:sysUserRole:id:%v", cachePrefix, c.id)
  74. compKey := fmt.Sprintf("%s:cache:sysUserRole:userId:roleId:%v:%v", cachePrefix, c.uid, roleId)
  75. assertCacheKeysGone(t, rds, idKey, compKey)
  76. }
  77. // 终态真相:两行都不应再存在于 DB。
  78. for _, id := range []int64{id1, id2} {
  79. _, err := m.FindOne(ctx, id)
  80. assert.ErrorIs(t, err, ErrNotFound,
  81. "L-R11-2:DELETE 已提交,FindOne 必须回落 DB 读到 ErrNotFound;"+
  82. "若仍查到旧行说明裁剪后的 SELECT 漏掉了 id 列,id 维度缓存仍在")
  83. }
  84. }
  85. // TC-1063: L-R11-2 —— DeleteByUserIdAndRoleIdsTx 的批量 IN 路径
  86. func TestSysUserRoleModel_DeleteByUserIdAndRoleIdsTx_InvalidatesAllKeyCols(t *testing.T) {
  87. ctx := context.Background()
  88. conn := testutil.GetTestSqlConn()
  89. m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  90. rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
  91. cachePrefix := testutil.GetTestCachePrefix()
  92. userId := randUserRoleId()
  93. r1, r2, r3 := randUserRoleId(), randUserRoleId()+1, randUserRoleId()+2
  94. id1 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r1)
  95. id2 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r2)
  96. // r3 作为"不在删除集合内"的对照组:这一行不得被误伤
  97. id3 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r3)
  98. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "sys_user_role", id1, id2, id3) })
  99. require.NoError(t,
  100. m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  101. return m.DeleteByUserIdAndRoleIdsTx(c, session, userId, []int64{r1, r2})
  102. }))
  103. // 被删的两条:id+composite 都必须失效。
  104. for _, c := range []struct {
  105. id, rid int64
  106. }{{id1, r1}, {id2, r2}} {
  107. idKey := fmt.Sprintf("%s:cache:sysUserRole:id:%v", cachePrefix, c.id)
  108. compKey := fmt.Sprintf("%s:cache:sysUserRole:userId:roleId:%v:%v", cachePrefix, userId, c.rid)
  109. assertCacheKeysGone(t, rds, idKey, compKey)
  110. _, err := m.FindOne(ctx, c.id)
  111. assert.ErrorIs(t, err, ErrNotFound)
  112. }
  113. // 未被删的第 3 条:DB 仍在,FindOne 必须成功。
  114. got, err := m.FindOne(ctx, id3)
  115. require.NoError(t, err,
  116. "L-R11-2 防误伤:IN (r1, r2) 不得把 r3 的行带走;"+
  117. "若失败,说明裁剪后 SELECT 把对照组的 key 也返回并被 ExecCtx 一并失效")
  118. assert.Equal(t, id3, got.Id)
  119. assert.Equal(t, r3, got.RoleId)
  120. }