| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- package user_test
- import (
- "context"
- "testing"
- "time"
- "perms-system-server/internal/model/user"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/redis"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 M-R11-2 —— UpdateStatus / IncrementTokenVersion 必须用**调用方透传**的
- // `username` 构造缓存失效键,不得再在 Model 内部隐式 FindOne 取真实 username。
- //
- // 直接可观测的契约:
- // 1) 调用方传 "wrongUser"(故意传一个与 DB 实际 username 不一致的值),函数 Del 的
- // Redis key 必须是 `cache:sysUser:username:wrongUser`(执行后该键消失);
- // 2) DB 实际 username 对应的 `cache:sysUser:username:<real>` 键则不会被 Del
- // (若仍有内部 FindOne,真实 username 的缓存会被动摇,下一次 FindOneByUsername 会回源 DB,
- // 这里通过预热 + 对比校验来锁死)。
- //
- // 如果 DEV 未来把 Model 层回退成内部 FindOne 取 username,这个用例会立刻红:
- // 要么 wrongUser key 不再被删,要么 realUser key 反而被删。两条契约各自独立失败、
- // 定位极快。
- // ---------------------------------------------------------------------------
- func sysUserUsernameCacheKey(username string) string {
- return testutil.GetTestCachePrefix() + ":cache:sysUser:username:" + username
- }
- // TC-1044: M-R11-2 —— UpdateStatus 失效 wrongUser cache,real username cache 不受影响
- func TestSysUserModel_UpdateStatus_UsesSuppliedUsername_NoInternalFindOne(t *testing.T) {
- ctx := context.Background()
- m, conn := newModel(t)
- realUsername := "mr112s_real_" + testutil.UniqueId()
- wrongUsername := "mr112s_wrong_" + testutil.UniqueId()
- data := newTestSysUser(realUsername, 1)
- res, err := m.Insert(ctx, data)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), id) })
- // 预热 cache:sysUser:username:<realUsername>(via FindOneByUsername 走 go-zero 的 WithCache)。
- _, err = m.FindOneByUsername(ctx, realUsername)
- require.NoError(t, err)
- rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
- // 直接往 Redis 里插一条 wrongUser 的桩缓存,供我们观察它是否被 UpdateStatus 失效。
- // 注意:我们并不关心桩的内容,只关心 key 是否被 Del。
- wrongKey := sysUserUsernameCacheKey(wrongUsername)
- realKey := sysUserUsernameCacheKey(realUsername)
- require.NoError(t, rds.Set(wrongKey, "stub"))
- // 预热后确认 realKey 存在(如果环境脏,用下面的断言兜底;缓存可能是 */null/任意值)。
- gotReal, err := rds.Get(realKey)
- require.NoError(t, err)
- require.NotEmpty(t, gotReal, "FindOneByUsername 未能把 realKey 写入缓存,前置条件失败")
- // 推进 updateTime 以触发 CAS 可成功。sys_user.updateTime 精度到秒。
- time.Sleep(1100 * time.Millisecond)
- cur, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- // 关键:传入故意错位的 username。若 Model 还在内部 FindOne,就会用 realUsername 作失效键,
- // wrongKey 不会被删;若 Model 已按 M-R11-2 的契约"透传即用",wrongKey 必被删。
- require.NoError(t,
- m.UpdateStatus(ctx, id, wrongUsername, 2, cur.UpdateTime),
- "UpdateStatus 语义上只依赖 id+expectedUpdateTime 做 CAS,username 只用于构造缓存键,不应因错位而失败")
- // 契约 1:wrongKey 必被删
- gotWrong, _ := rds.Get(wrongKey)
- assert.Empty(t, gotWrong,
- "M-R11-2:UpdateStatus 必须用调用方透传的 username 做 Del,wrongKey 必须消失")
- // 契约 2:realKey 依然留存(Model 不知道真 username,不应当去动它)
- gotRealAfter, err := rds.Get(realKey)
- require.NoError(t, err)
- assert.NotEmpty(t, gotRealAfter,
- "M-R11-2:Model 没有内部 FindOne 获取真 username,因此不应删除 realKey")
- }
- // TC-1045: M-R11-2 —— IncrementTokenVersion 同样只删调用方透传的 username key
- func TestSysUserModel_IncrementTokenVersion_UsesSuppliedUsername_NoInternalFindOne(t *testing.T) {
- ctx := context.Background()
- m, conn := newModel(t)
- realUsername := "mr112i_real_" + testutil.UniqueId()
- wrongUsername := "mr112i_wrong_" + testutil.UniqueId()
- data := newTestSysUser(realUsername, 1)
- res, err := m.Insert(ctx, data)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), id) })
- _, err = m.FindOneByUsername(ctx, realUsername)
- require.NoError(t, err)
- rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
- wrongKey := sysUserUsernameCacheKey(wrongUsername)
- realKey := sysUserUsernameCacheKey(realUsername)
- require.NoError(t, rds.Set(wrongKey, "stub"))
- // IncrementTokenVersion 不依赖 expectedUpdateTime,直接按 id 更新即可。
- newV, err := m.IncrementTokenVersion(ctx, id, wrongUsername)
- require.NoError(t, err)
- assert.Equal(t, int64(1), newV, "从 0 起递增到 1")
- gotWrong, _ := rds.Get(wrongKey)
- assert.Empty(t, gotWrong,
- "M-R11-2:IncrementTokenVersion 必须用透传的 username 做 Del,wrongKey 必须消失")
- gotRealAfter, err := rds.Get(realKey)
- require.NoError(t, err)
- assert.NotEmpty(t, gotRealAfter,
- "M-R11-2:Model 没有内部 FindOne 取真 username,realKey 不应受影响")
- }
- // TC-1046: M-R11-2 —— IncrementTokenVersion 用户已被并发删除,返回 ErrUpdateConflict
- // 此契约由 L-R10-3 引入,M-R11-2 下的签名改动不得削弱它:affected=0 仍要 ErrUpdateConflict。
- func TestSysUserModel_IncrementTokenVersion_DeletedRow_StillConflicts(t *testing.T) {
- ctx := context.Background()
- m, conn := newModel(t)
- username := "mr112i_del_" + testutil.UniqueId()
- data := newTestSysUser(username, 1)
- res, err := m.Insert(ctx, data)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- testutil.CleanTable(ctx, conn, m.TableName(), id)
- _, err = m.IncrementTokenVersion(ctx, id, username)
- require.ErrorIs(t, err, user.ErrUpdateConflict,
- "M-R11-2:目标行已被并发删除,IncrementTokenVersion 不得静默返回 tokenVersion=0")
- }
|