| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- package userrole
- import (
- "context"
- "fmt"
- "testing"
- "time"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/redis"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 L-R11-2 —— Delete 族的 SELECT 从 * 裁剪到 id/userId/roleId。
- // 行为契约不得因此回归:
- // 1) 每一行被删除后,id 维度 + 复合维度(userId+roleId)的缓存 key 必须全部失效;
- // 2) 裁剪后仍然能正确处理"同一 userId 下多条 role 绑定"的批量失效,绝不能只删到
- // 第一条行的缓存 key 就提前返回;
- // 3) 经过 Delete 后再 FindOne 必须回落到 DB 的 ErrNotFound,不能沿用缓存里的旧行。
- //
- // 这里选 sys_user_role 作为三个 Delete-Tx 家族(userrole/roleperm/userperm/perm)的
- // 代表:其 DeleteByRoleIdTx / DeleteByUserIdForProductTx / DeleteByUserIdAndRoleIdsTx
- // 三个方法都走同一条 SELECT-only-key-cols → DELETE 的路径,成一族即覆盖整个收敛面。
- // ---------------------------------------------------------------------------
- func seedUserRoleWithPrimedCache(t *testing.T, ctx context.Context, m SysUserRoleModel, userId, roleId int64) int64 {
- t.Helper()
- ts := time.Now().Unix()
- res, err := m.Insert(ctx, &SysUserRole{
- UserId: userId, RoleId: roleId, CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- _, err = m.FindOne(ctx, id)
- require.NoError(t, err, "FindOne 为 id 维度缓存预热")
- _, err = m.FindOneByUserIdRoleId(ctx, userId, roleId)
- require.NoError(t, err, "FindOneByUserIdRoleId 为复合维度缓存预热")
- return id
- }
- func assertCacheKeysGone(t *testing.T, rds *redis.Redis, idKey, compositeKey string) {
- t.Helper()
- got, err := rds.Get(idKey)
- require.NoError(t, err)
- assert.Empty(t, got, "L-R11-2:id 维度缓存 key %q 应被 DELETE 一并失效", idKey)
- got, err = rds.Get(compositeKey)
- require.NoError(t, err)
- assert.Empty(t, got, "L-R11-2:复合维度缓存 key %q 应被 DELETE 一并失效", compositeKey)
- }
- // TC-1062: L-R11-2 —— DeleteByRoleIdTx 对多行同时失效 id + composite 缓存
- func TestSysUserRoleModel_DeleteByRoleIdTx_InvalidatesAllKeyCols(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
- cachePrefix := testutil.GetTestCachePrefix()
- roleId := randUserRoleId()
- u1, u2 := randUserRoleId(), randUserRoleId()
- if u1 == u2 {
- u2 = u1 + 1
- }
- id1 := seedUserRoleWithPrimedCache(t, ctx, m, u1, roleId)
- id2 := seedUserRoleWithPrimedCache(t, ctx, m, u2, roleId)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "sys_user_role", id1, id2) })
- require.NoError(t,
- m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.DeleteByRoleIdTx(c, session, roleId)
- }))
- // L-R11-2 裁剪 SELECT 为三列后,构造缓存 key 需要的 id/userId/roleId 仍被携带回来。
- // 任何一条遗漏都会让这里的断言炸掉(旧行的缓存还在,FindOne 就能从缓存拿到"幽灵行")。
- for _, c := range []struct {
- id, uid int64
- }{{id1, u1}, {id2, u2}} {
- idKey := fmt.Sprintf("%s:cache:sysUserRole:id:%v", cachePrefix, c.id)
- compKey := fmt.Sprintf("%s:cache:sysUserRole:userId:roleId:%v:%v", cachePrefix, c.uid, roleId)
- assertCacheKeysGone(t, rds, idKey, compKey)
- }
- // 终态真相:两行都不应再存在于 DB。
- for _, id := range []int64{id1, id2} {
- _, err := m.FindOne(ctx, id)
- assert.ErrorIs(t, err, ErrNotFound,
- "L-R11-2:DELETE 已提交,FindOne 必须回落 DB 读到 ErrNotFound;"+
- "若仍查到旧行说明裁剪后的 SELECT 漏掉了 id 列,id 维度缓存仍在")
- }
- }
- // TC-1063: L-R11-2 —— DeleteByUserIdAndRoleIdsTx 的批量 IN 路径
- func TestSysUserRoleModel_DeleteByUserIdAndRoleIdsTx_InvalidatesAllKeyCols(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
- cachePrefix := testutil.GetTestCachePrefix()
- userId := randUserRoleId()
- r1, r2, r3 := randUserRoleId(), randUserRoleId()+1, randUserRoleId()+2
- id1 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r1)
- id2 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r2)
- // r3 作为"不在删除集合内"的对照组:这一行不得被误伤
- id3 := seedUserRoleWithPrimedCache(t, ctx, m, userId, r3)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "sys_user_role", id1, id2, id3) })
- require.NoError(t,
- m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.DeleteByUserIdAndRoleIdsTx(c, session, userId, []int64{r1, r2})
- }))
- // 被删的两条:id+composite 都必须失效。
- for _, c := range []struct {
- id, rid int64
- }{{id1, r1}, {id2, r2}} {
- idKey := fmt.Sprintf("%s:cache:sysUserRole:id:%v", cachePrefix, c.id)
- compKey := fmt.Sprintf("%s:cache:sysUserRole:userId:roleId:%v:%v", cachePrefix, userId, c.rid)
- assertCacheKeysGone(t, rds, idKey, compKey)
- _, err := m.FindOne(ctx, c.id)
- assert.ErrorIs(t, err, ErrNotFound)
- }
- // 未被删的第 3 条:DB 仍在,FindOne 必须成功。
- got, err := m.FindOne(ctx, id3)
- require.NoError(t, err,
- "L-R11-2 防误伤:IN (r1, r2) 不得把 r3 的行带走;"+
- "若失败,说明裁剪后 SELECT 把对照组的 key 也返回并被 ExecCtx 一并失效")
- assert.Equal(t, id3, got.Id)
- assert.Equal(t, r3, got.RoleId)
- }
|