| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- package loaders
- import (
- "context"
- "testing"
- "perms-system-server/internal/consts"
- productModel "perms-system-server/internal/model/product"
- userModel "perms-system-server/internal/model/user"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 M-N2 修复 —— BatchDel 必须把 userIndex / productIndex 的 SREM
- // 合进一次 Pipelined RTT,历史 per-user 串行路径下"角色绑千人"时尾延迟会抬到秒级。
- // 本测试以"主缓存 + user/product 索引集合"的最终一致性为切入点,在 N 条数据下验证:
- // (1) 主 cacheKey 全量清空
- // (2) 每个 userIndex 集合中对应的 cacheKey 已经被 SREM
- // (3) productIndex 集合中所有 cacheKey 也已经被 SREM
- // 相比只断言 (1) 的 TC-0513,本 TC 把 index 一致性钉死,防止 pipelined 分支被回退到
- // "只 DEL 主 key、遗漏 SREM"的静默回归。
- // ---------------------------------------------------------------------------
- // TC-1013: M-N2 —— BatchDel 必须同步清理 userIndex / productIndex 中的 cacheKey 集合(Pipelined)。
- func TestUserDetailsLoader_MN2_BatchDelClearsUserAndProductIndexes(t *testing.T) {
- ctx := context.Background()
- conn := testConn()
- m := testModels()
- loader := newTestLoader()
- rds := testRedis()
- ts := now()
- pcode := "mn2_" + uniqueId()
- // 插入两个用户 + 一个真实产品,确保 Load 走到 5 分钟正缓存分支并注册索引
- uid1 := uniqueId()
- uid2 := uniqueId()
- userId1 := insertUser(ctx, t, m, &userModel.SysUser{
- Username: uid1, Password: hashPwd("pass123"), Nickname: "nick_" + uid1,
- Email: uid1 + "@t.com", Phone: "13800000008", DeptId: 0,
- IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
- Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
- })
- userId2 := insertUser(ctx, t, m, &userModel.SysUser{
- Username: uid2, Password: hashPwd("pass123"), Nickname: "nick_" + uid2,
- Email: uid2 + "@t.com", Phone: "13800000009", DeptId: 0,
- IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
- Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
- })
- pid := insertProduct(ctx, t, m, &productModel.SysProduct{
- Code: pcode, Name: "p_" + pcode, AppKey: "ak_" + pcode, AppSecret: "as_" + pcode,
- Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
- })
- t.Cleanup(func() {
- loader.Del(ctx, userId1, pcode)
- loader.Del(ctx, userId2, pcode)
- cleanTable(ctx, conn, "`sys_product`", pid)
- cleanTable(ctx, conn, "`sys_user`", userId1, userId2)
- })
- // 把缓存一次预热,让 userIndex/productIndex 被 registerCacheKey 真实写入
- _, err := loader.Load(ctx, userId1, pcode)
- require.NoError(t, err)
- _, err = loader.Load(ctx, userId2, pcode)
- require.NoError(t, err)
- k1 := loader.cacheKey(userId1, pcode)
- k2 := loader.cacheKey(userId2, pcode)
- pIdx := loader.productIndexKey(pcode)
- u1Idx := loader.userIndexKey(userId1)
- u2Idx := loader.userIndexKey(userId2)
- // 预检:主 key 写入、productIndex / userIndex 存在对应元素
- for _, k := range []string{k1, k2} {
- val, gerr := rds.GetCtx(ctx, k)
- require.NoError(t, gerr)
- require.NotEmpty(t, val, "M-N2 预检:主 cacheKey 必须被写入才有意义")
- }
- has, _ := rds.SismemberCtx(ctx, pIdx, k1)
- require.True(t, has, "M-N2 预检:productIndex 必须含 k1")
- has, _ = rds.SismemberCtx(ctx, pIdx, k2)
- require.True(t, has, "M-N2 预检:productIndex 必须含 k2")
- has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
- require.True(t, has, "M-N2 预检:userIndex(u1) 必须含 k1")
- has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
- require.True(t, has, "M-N2 预检:userIndex(u2) 必须含 k2")
- // 触发被测路径:BatchDel(pipelined SREM)
- loader.BatchDel(ctx, []int64{userId1, userId2}, pcode)
- // 主 key 被清空(原 TC-0513 已保障)
- for _, k := range []string{k1, k2} {
- val, _ := rds.GetCtx(ctx, k)
- assert.Empty(t, val, "M-N2:BatchDel 必须删除主 cacheKey")
- }
- // userIndex / productIndex 中的对应 cacheKey 必须被 SREM 清除(本 TC 核心断言)
- has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
- assert.False(t, has, "M-N2:BatchDel 必须把 k1 从 userIndex(u1) SREM 出去")
- has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
- assert.False(t, has, "M-N2:BatchDel 必须把 k2 从 userIndex(u2) SREM 出去")
- has, _ = rds.SismemberCtx(ctx, pIdx, k1)
- assert.False(t, has, "M-N2:BatchDel 必须把 k1 从 productIndex SREM 出去")
- has, _ = rds.SismemberCtx(ctx, pIdx, k2)
- assert.False(t, has, "M-N2:BatchDel 必须把 k2 从 productIndex SREM 出去")
- }
- // TC-1014: M-N2 —— productCode 为空时 BatchDel 仅 SREM userIndex,不得 panic 或误访问 productIndex。
- // 目前业务侧 BatchDel 的所有调用都传了 productCode;但 pipeline 分支必须对空串 fail-safe,
- // 防止未来调用方误传时 pipeline 里塞空 key 把 Redis 侧写脏。
- func TestUserDetailsLoader_MN2_BatchDelEmptyProductCodeDoesNotPanic(t *testing.T) {
- ctx := context.Background()
- loader := newTestLoader()
- // 即便 uid 不存在,pipelined SREM 对不存在的集合是 no-op,不应报错/panic
- require.NotPanics(t, func() {
- loader.BatchDel(ctx, []int64{9999999991, 9999999992}, "")
- })
- }
|