mr11_2_noInternalFindOne_audit_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package user_test
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "perms-system-server/internal/model/user"
  7. "perms-system-server/internal/testutil"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "github.com/zeromicro/go-zero/core/stores/redis"
  11. )
  12. // ---------------------------------------------------------------------------
  13. // 覆盖目标:审计 M-R11-2 —— UpdateStatus / IncrementTokenVersion 必须用**调用方透传**的
  14. // `username` 构造缓存失效键,不得再在 Model 内部隐式 FindOne 取真实 username。
  15. //
  16. // 直接可观测的契约:
  17. // 1) 调用方传 "wrongUser"(故意传一个与 DB 实际 username 不一致的值),函数 Del 的
  18. // Redis key 必须是 `cache:sysUser:username:wrongUser`(执行后该键消失);
  19. // 2) DB 实际 username 对应的 `cache:sysUser:username:<real>` 键则不会被 Del
  20. // (若仍有内部 FindOne,真实 username 的缓存会被动摇,下一次 FindOneByUsername 会回源 DB,
  21. // 这里通过预热 + 对比校验来锁死)。
  22. //
  23. // 如果 DEV 未来把 Model 层回退成内部 FindOne 取 username,这个用例会立刻红:
  24. // 要么 wrongUser key 不再被删,要么 realUser key 反而被删。两条契约各自独立失败、
  25. // 定位极快。
  26. // ---------------------------------------------------------------------------
  27. func sysUserUsernameCacheKey(username string) string {
  28. return testutil.GetTestCachePrefix() + ":cache:sysUser:username:" + username
  29. }
  30. // TC-1044: M-R11-2 —— UpdateStatus 失效 wrongUser cache,real username cache 不受影响
  31. func TestSysUserModel_UpdateStatus_UsesSuppliedUsername_NoInternalFindOne(t *testing.T) {
  32. ctx := context.Background()
  33. m, conn := newModel(t)
  34. realUsername := "mr112s_real_" + testutil.UniqueId()
  35. wrongUsername := "mr112s_wrong_" + testutil.UniqueId()
  36. data := newTestSysUser(realUsername, 1)
  37. res, err := m.Insert(ctx, data)
  38. require.NoError(t, err)
  39. id, err := res.LastInsertId()
  40. require.NoError(t, err)
  41. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), id) })
  42. // 预热 cache:sysUser:username:<realUsername>(via FindOneByUsername 走 go-zero 的 WithCache)。
  43. _, err = m.FindOneByUsername(ctx, realUsername)
  44. require.NoError(t, err)
  45. rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
  46. // 直接往 Redis 里插一条 wrongUser 的桩缓存,供我们观察它是否被 UpdateStatus 失效。
  47. // 注意:我们并不关心桩的内容,只关心 key 是否被 Del。
  48. wrongKey := sysUserUsernameCacheKey(wrongUsername)
  49. realKey := sysUserUsernameCacheKey(realUsername)
  50. require.NoError(t, rds.Set(wrongKey, "stub"))
  51. // 预热后确认 realKey 存在(如果环境脏,用下面的断言兜底;缓存可能是 */null/任意值)。
  52. gotReal, err := rds.Get(realKey)
  53. require.NoError(t, err)
  54. require.NotEmpty(t, gotReal, "FindOneByUsername 未能把 realKey 写入缓存,前置条件失败")
  55. // 推进 updateTime 以触发 CAS 可成功。sys_user.updateTime 精度到秒。
  56. time.Sleep(1100 * time.Millisecond)
  57. cur, err := m.FindOne(ctx, id)
  58. require.NoError(t, err)
  59. // 关键:传入故意错位的 username。若 Model 还在内部 FindOne,就会用 realUsername 作失效键,
  60. // wrongKey 不会被删;若 Model 已按 M-R11-2 的契约"透传即用",wrongKey 必被删。
  61. require.NoError(t,
  62. m.UpdateStatus(ctx, id, wrongUsername, 2, cur.UpdateTime),
  63. "UpdateStatus 语义上只依赖 id+expectedUpdateTime 做 CAS,username 只用于构造缓存键,不应因错位而失败")
  64. // 契约 1:wrongKey 必被删
  65. gotWrong, _ := rds.Get(wrongKey)
  66. assert.Empty(t, gotWrong,
  67. "M-R11-2:UpdateStatus 必须用调用方透传的 username 做 Del,wrongKey 必须消失")
  68. // 契约 2:realKey 依然留存(Model 不知道真 username,不应当去动它)
  69. gotRealAfter, err := rds.Get(realKey)
  70. require.NoError(t, err)
  71. assert.NotEmpty(t, gotRealAfter,
  72. "M-R11-2:Model 没有内部 FindOne 获取真 username,因此不应删除 realKey")
  73. }
  74. // TC-1045: M-R11-2 —— IncrementTokenVersion 同样只删调用方透传的 username key
  75. func TestSysUserModel_IncrementTokenVersion_UsesSuppliedUsername_NoInternalFindOne(t *testing.T) {
  76. ctx := context.Background()
  77. m, conn := newModel(t)
  78. realUsername := "mr112i_real_" + testutil.UniqueId()
  79. wrongUsername := "mr112i_wrong_" + testutil.UniqueId()
  80. data := newTestSysUser(realUsername, 1)
  81. res, err := m.Insert(ctx, data)
  82. require.NoError(t, err)
  83. id, err := res.LastInsertId()
  84. require.NoError(t, err)
  85. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), id) })
  86. _, err = m.FindOneByUsername(ctx, realUsername)
  87. require.NoError(t, err)
  88. rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
  89. wrongKey := sysUserUsernameCacheKey(wrongUsername)
  90. realKey := sysUserUsernameCacheKey(realUsername)
  91. require.NoError(t, rds.Set(wrongKey, "stub"))
  92. // IncrementTokenVersion 不依赖 expectedUpdateTime,直接按 id 更新即可。
  93. newV, err := m.IncrementTokenVersion(ctx, id, wrongUsername)
  94. require.NoError(t, err)
  95. assert.Equal(t, int64(1), newV, "从 0 起递增到 1")
  96. gotWrong, _ := rds.Get(wrongKey)
  97. assert.Empty(t, gotWrong,
  98. "M-R11-2:IncrementTokenVersion 必须用透传的 username 做 Del,wrongKey 必须消失")
  99. gotRealAfter, err := rds.Get(realKey)
  100. require.NoError(t, err)
  101. assert.NotEmpty(t, gotRealAfter,
  102. "M-R11-2:Model 没有内部 FindOne 取真 username,realKey 不应受影响")
  103. }
  104. // TC-1046: M-R11-2 —— IncrementTokenVersion 用户已被并发删除,返回 ErrUpdateConflict
  105. // 此契约由 L-R10-3 引入,M-R11-2 下的签名改动不得削弱它:affected=0 仍要 ErrUpdateConflict。
  106. func TestSysUserModel_IncrementTokenVersion_DeletedRow_StillConflicts(t *testing.T) {
  107. ctx := context.Background()
  108. m, conn := newModel(t)
  109. username := "mr112i_del_" + testutil.UniqueId()
  110. data := newTestSysUser(username, 1)
  111. res, err := m.Insert(ctx, data)
  112. require.NoError(t, err)
  113. id, err := res.LastInsertId()
  114. require.NoError(t, err)
  115. testutil.CleanTable(ctx, conn, m.TableName(), id)
  116. _, err = m.IncrementTokenVersion(ctx, id, username)
  117. require.ErrorIs(t, err, user.ErrUpdateConflict,
  118. "M-R11-2:目标行已被并发删除,IncrementTokenVersion 不得静默返回 tokenVersion=0")
  119. }