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}, "") }) }