package pub import ( "context" "fmt" "testing" "perms-system-server/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/redis" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 L-R11-4 —— SyncPerms 纯新增(added>0 && updated==0 && disabled==0) // 不触发 UserDetailsLoader.CleanByProduct,避免把该产品下所有在线用户的 UD 缓存集体清空 // 造成雪崩;updated/disabled 任一 > 0 时仍必须清。 // // 用 Redis 层面的 productIndexKey(`:ud:idx:p:`)做间接观测: // - 测试前手工 SAdd 一个合成 cacheKey 到索引集合,建立"被 CleanByProduct 吃掉"的探针; // - 跑 ExecuteSyncPerms; // - 若 CleanByProduct 被调用,索引集合会被 SmembersCtx -> DelCtx 一并清空,Exists 返 0; // - 若未调用,合成 key 仍然在集合里,Exists 返 1。 // --------------------------------------------------------------------------- func primeProductIndex(t *testing.T, rds *redis.Redis, cachePrefix, productCode string) string { t.Helper() idxKey := fmt.Sprintf("%s:ud:idx:p:%s", cachePrefix, productCode) canary := fmt.Sprintf("%s:ud:probe:%s", cachePrefix, testutil.UniqueId()) _, err := rds.Sadd(idxKey, canary) require.NoError(t, err, "SAdd 到 productIndexKey 失败,Redis 不可用,测试前置条件失败") members, err := rds.Smembers(idxKey) require.NoError(t, err) require.Contains(t, members, canary, "primeProductIndex: canary 必须先出现在集合里") return idxKey } // TC-1064: L-R11-4 —— 纯新增不触发 CleanByProduct func TestSyncPerms_PureAddDoesNotTriggerCleanByProduct(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) conn := testutil.GetTestSqlConn() pc := testutil.UniqueId() appKey := testutil.UniqueId() appSecret := testutil.UniqueId() _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1) t.Cleanup(cleanProduct) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) }) idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc) t.Cleanup(func() { _, _ = rds.Del(idxKey) }) result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{ {Code: "r11_4_add_a", Name: "A"}, {Code: "r11_4_add_b", Name: "B"}, {Code: "r11_4_add_c", Name: "C"}, }) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, int64(3), result.Added) assert.Equal(t, int64(0), result.Updated) assert.Equal(t, int64(0), result.Disabled) // 纯新增路径:CleanByProduct 不得被调用,索引集合必须仍保留 canary。 exists, err := rds.Exists(idxKey) require.NoError(t, err) assert.True(t, exists, "L-R11-4:added=3 / updated=0 / disabled=0 属于纯新增,不得触发 CleanByProduct;"+ "productIndexKey 若被删除说明 SyncPerms 仍在走全产品清缓存路径,回归:会把该产品"+ "所有在线用户下一次请求同时打穿回 DB") } // TC-1065: L-R11-4 —— updated > 0 时必须触发 CleanByProduct func TestSyncPerms_UpdateTriggersCleanByProduct(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) conn := testutil.GetTestSqlConn() pc := testutil.UniqueId() appKey := testutil.UniqueId() appSecret := testutil.UniqueId() _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1) t.Cleanup(cleanProduct) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) }) // 第一次同步:纯新增,为随后的 update 打底;这次不触发 Clean,CleanByProduct 仍然可被后续触发。 _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{ {Code: "r11_4_upd", Name: "OldName"}, }) require.NoError(t, err) idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc) t.Cleanup(func() { _, _ = rds.Del(idxKey) }) // 第二次同步:同一 Code 改 Name → updated=1。 result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{ {Code: "r11_4_upd", Name: "NewName"}, }) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, int64(1), result.Updated, "前置:同名 Code 改 Name 必须 updated=1,否则后续断言失去意义") exists, err := rds.Exists(idxKey) require.NoError(t, err) assert.False(t, exists, "L-R11-4:updated>0 必须触发 CleanByProduct;若 canary 仍在,说明 Logic 把"+ "updated 情况也误归入'纯新增'分支,已存在 UD 缓存中的旧 perms 将长期对外返回") } // TC-1066: L-R11-4 —— disabled > 0 时必须触发 CleanByProduct func TestSyncPerms_DisableTriggersCleanByProduct(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) conn := testutil.GetTestSqlConn() pc := testutil.UniqueId() appKey := testutil.UniqueId() appSecret := testutil.UniqueId() _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1) t.Cleanup(cleanProduct) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) }) // 先注入两个 perm。 _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{ {Code: "r11_4_keep", Name: "K"}, {Code: "r11_4_drop", Name: "D"}, }) require.NoError(t, err) idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc) t.Cleanup(func() { _, _ = rds.Del(idxKey) }) // 第二次只同步 r11_4_keep,r11_4_drop 会被 DisableNotInCodesWithTx 置 disabled。 result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{ {Code: "r11_4_keep", Name: "K"}, }) require.NoError(t, err) assert.Equal(t, int64(1), result.Disabled, "前置:第二次只同步 keep,drop 必须被 disabled=1") exists, err := rds.Exists(idxKey) require.NoError(t, err) assert.False(t, exists, "L-R11-4:disabled>0 必须触发 CleanByProduct,否则已缓存的 UD.perms 里仍挂着"+ "已禁用权限,权限网关会把不再有效的权限判为 allow,产生权限残留") }