package user_test import ( "context" "database/sql" "fmt" "sync" "testing" "time" "perms-system-server/internal/model/user" "perms-system-server/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TC-0736: H-B 修复回归(model 层)—— IncrementTokenVersion 返回的版本号必须等于 // 事务结束后 DB 中实际落盘的 tokenVersion。旧实现基于"缓存读 +1",并发下会返回 stale 值, // 新实现用 `LAST_INSERT_ID(tokenVersion+1)` 原子递增并回读,返回值必须与 DB 记录一致。 func TestSysUserModel_IncrementTokenVersion_ReturnedEqualsPersisted(t *testing.T) { m, conn := newModel(t) ctx := context.Background() now := time.Now().Unix() username := "itv_eq_" + testutil.UniqueId() res, err := m.Insert(ctx, &user.SysUser{ Username: username, Password: "x", Nickname: "n", Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, TokenVersion: 7, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) }) for expected := int64(8); expected <= 12; expected++ { got, err := m.IncrementTokenVersion(ctx, id) require.NoError(t, err) assert.Equal(t, expected, got, "IncrementTokenVersion 必须返回 DB 真实递增后的值(H-B:不可再受 stale cache 影响)") fresh, err := m.FindOne(ctx, id) require.NoError(t, err) assert.Equal(t, got, fresh.TokenVersion, "返回值必须等于 DB 中真实持久化的 tokenVersion") } } // TC-0737: H-B 修复回归 —— 自增后缓存必须被主动清理,Load → tokenVersion 能读到新值。 // 旧实现只更新 DB,返回值基于缓存,并且未强制 DelCache,导致 JWT 中间件仍从缓存读到旧值。 func TestSysUserModel_IncrementTokenVersion_InvalidatesCache(t *testing.T) { m, conn := newModel(t) ctx := context.Background() now := time.Now().Unix() username := "itv_cache_" + testutil.UniqueId() res, err := m.Insert(ctx, &user.SysUser{ Username: username, Password: "x", Nickname: "n", Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, TokenVersion: 0, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) }) // 先 FindOne 让 id-key、username-key 双路缓存写入 u0, err := m.FindOne(ctx, id) require.NoError(t, err) require.Equal(t, int64(0), u0.TokenVersion) u0b, err := m.FindOneByUsername(ctx, username) require.NoError(t, err) require.Equal(t, int64(0), u0b.TokenVersion) _, err = m.IncrementTokenVersion(ctx, id) require.NoError(t, err) u1, err := m.FindOne(ctx, id) require.NoError(t, err) assert.Equal(t, int64(1), u1.TokenVersion, "按 id 读取缓存路径也必须拿到最新版本") u1b, err := m.FindOneByUsername(ctx, username) require.NoError(t, err) assert.Equal(t, int64(1), u1b.TokenVersion, "按 username 读取缓存路径也必须失效") } // TC-0738: H-B 修复并发回归 —— 10 个 goroutine 同时 Increment 同一用户, // 每次返回值必须互不重复,最终 DB 里 tokenVersion = 起始值 + N。 func TestSysUserModel_IncrementTokenVersion_ConcurrentUnique(t *testing.T) { m, conn := newModel(t) ctx := context.Background() now := time.Now().Unix() username := "itv_conc_" + testutil.UniqueId() res, err := m.Insert(ctx, &user.SysUser{ Username: username, Password: "x", Nickname: "n", Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, TokenVersion: 0, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) }) const N = 10 var wg sync.WaitGroup results := make([]int64, N) errs := make([]error, N) for i := 0; i < N; i++ { wg.Add(1) go func(idx int) { defer wg.Done() v, e := m.IncrementTokenVersion(ctx, id) results[idx] = v errs[idx] = e }(i) } wg.Wait() seen := make(map[int64]int, N) for i := 0; i < N; i++ { require.NoError(t, errs[i], "并发 IncrementTokenVersion 任一 goroutine 不得失败") seen[results[i]]++ } for v, cnt := range seen { assert.Equal(t, 1, cnt, fmt.Sprintf("返回值 %d 被重复派发 %d 次,与 DB 实际递增序列脱节", v, cnt)) } fresh, err := m.FindOne(ctx, id) require.NoError(t, err) assert.Equal(t, int64(N), fresh.TokenVersion, "DB 最终 tokenVersion 应为并发次数") }