incrementTokenVersion_audit_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. package user_test
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "sync"
  7. "testing"
  8. "time"
  9. "perms-system-server/internal/model/user"
  10. "perms-system-server/internal/testutil"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. // TC-0736: H-B 修复回归(model 层)—— IncrementTokenVersion 返回的版本号必须等于
  15. // 事务结束后 DB 中实际落盘的 tokenVersion。旧实现基于"缓存读 +1",并发下会返回 stale 值,
  16. // 新实现用 `LAST_INSERT_ID(tokenVersion+1)` 原子递增并回读,返回值必须与 DB 记录一致。
  17. func TestSysUserModel_IncrementTokenVersion_ReturnedEqualsPersisted(t *testing.T) {
  18. m, conn := newModel(t)
  19. ctx := context.Background()
  20. now := time.Now().Unix()
  21. username := "itv_eq_" + testutil.UniqueId()
  22. res, err := m.Insert(ctx, &user.SysUser{
  23. Username: username, Password: "x", Nickname: "n",
  24. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  25. Status: 1, TokenVersion: 7, CreateTime: now, UpdateTime: now,
  26. })
  27. require.NoError(t, err)
  28. id, _ := res.LastInsertId()
  29. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) })
  30. for expected := int64(8); expected <= 12; expected++ {
  31. got, err := m.IncrementTokenVersion(ctx, id, username)
  32. require.NoError(t, err)
  33. assert.Equal(t, expected, got,
  34. "IncrementTokenVersion 必须返回 DB 真实递增后的值(H-B:不可再受 stale cache 影响)")
  35. fresh, err := m.FindOne(ctx, id)
  36. require.NoError(t, err)
  37. assert.Equal(t, got, fresh.TokenVersion,
  38. "返回值必须等于 DB 中真实持久化的 tokenVersion")
  39. }
  40. }
  41. // TC-0737: H-B 修复回归 —— 自增后缓存必须被主动清理,Load → tokenVersion 能读到新值。
  42. // 旧实现只更新 DB,返回值基于缓存,并且未强制 DelCache,导致 JWT 中间件仍从缓存读到旧值。
  43. func TestSysUserModel_IncrementTokenVersion_InvalidatesCache(t *testing.T) {
  44. m, conn := newModel(t)
  45. ctx := context.Background()
  46. now := time.Now().Unix()
  47. username := "itv_cache_" + testutil.UniqueId()
  48. res, err := m.Insert(ctx, &user.SysUser{
  49. Username: username, Password: "x", Nickname: "n",
  50. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  51. Status: 1, TokenVersion: 0, CreateTime: now, UpdateTime: now,
  52. })
  53. require.NoError(t, err)
  54. id, _ := res.LastInsertId()
  55. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) })
  56. // 先 FindOne 让 id-key、username-key 双路缓存写入
  57. u0, err := m.FindOne(ctx, id)
  58. require.NoError(t, err)
  59. require.Equal(t, int64(0), u0.TokenVersion)
  60. u0b, err := m.FindOneByUsername(ctx, username)
  61. require.NoError(t, err)
  62. require.Equal(t, int64(0), u0b.TokenVersion)
  63. _, err = m.IncrementTokenVersion(ctx, id, username)
  64. require.NoError(t, err)
  65. u1, err := m.FindOne(ctx, id)
  66. require.NoError(t, err)
  67. assert.Equal(t, int64(1), u1.TokenVersion, "按 id 读取缓存路径也必须拿到最新版本")
  68. u1b, err := m.FindOneByUsername(ctx, username)
  69. require.NoError(t, err)
  70. assert.Equal(t, int64(1), u1b.TokenVersion, "按 username 读取缓存路径也必须失效")
  71. }
  72. // TC-0738: H-B 修复并发回归 —— 10 个 goroutine 同时 Increment 同一用户,
  73. // 每次返回值必须互不重复,最终 DB 里 tokenVersion = 起始值 + N。
  74. func TestSysUserModel_IncrementTokenVersion_ConcurrentUnique(t *testing.T) {
  75. m, conn := newModel(t)
  76. ctx := context.Background()
  77. now := time.Now().Unix()
  78. username := "itv_conc_" + testutil.UniqueId()
  79. res, err := m.Insert(ctx, &user.SysUser{
  80. Username: username, Password: "x", Nickname: "n",
  81. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  82. Status: 1, TokenVersion: 0, CreateTime: now, UpdateTime: now,
  83. })
  84. require.NoError(t, err)
  85. id, _ := res.LastInsertId()
  86. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) })
  87. const N = 10
  88. var wg sync.WaitGroup
  89. results := make([]int64, N)
  90. errs := make([]error, N)
  91. for i := 0; i < N; i++ {
  92. wg.Add(1)
  93. go func(idx int) {
  94. defer wg.Done()
  95. v, e := m.IncrementTokenVersion(ctx, id, username)
  96. results[idx] = v
  97. errs[idx] = e
  98. }(i)
  99. }
  100. wg.Wait()
  101. seen := make(map[int64]int, N)
  102. for i := 0; i < N; i++ {
  103. require.NoError(t, errs[i], "并发 IncrementTokenVersion 任一 goroutine 不得失败")
  104. seen[results[i]]++
  105. }
  106. for v, cnt := range seen {
  107. assert.Equal(t, 1, cnt, fmt.Sprintf("返回值 %d 被重复派发 %d 次,与 DB 实际递增序列脱节", v, cnt))
  108. }
  109. fresh, err := m.FindOne(ctx, id)
  110. require.NoError(t, err)
  111. assert.Equal(t, int64(N), fresh.TokenVersion, "DB 最终 tokenVersion 应为并发次数")
  112. }