| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- 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(`<prefix>:ud:idx:p:<productCode>`)做间接观测:
- // - 测试前手工 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,产生权限残留")
- }
|