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:` 键则不会被 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:(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") }