updateDeptLogic_mock_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package dept
  2. import (
  3. "context"
  4. "testing"
  5. deptModel "perms-system-server/internal/model/dept"
  6. "perms-system-server/internal/testutil/ctxhelper"
  7. "perms-system-server/internal/testutil/mocks"
  8. "perms-system-server/internal/types"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/zeromicro/go-zero/core/stores/sqlx"
  11. "go.uber.org/mock/gomock"
  12. )
  13. // runDeptTxInline 让 TransactCtx 的期望就地把传入的闭包执行掉,从而让我们能在 gomock 层面
  14. // 同时观察 tx 内部(UpdateWithOptLockTx / FindIdsByDeptIdForShareTx /
  15. // BatchIncrementTokenVersionWithTx)与 tx 外部(InvalidateDeptCache / FindIdsByDeptId 等
  16. // post-commit 钩子)的调用次数与顺序。
  17. //
  18. // 直接 `Return(nil)` 的传统写法会跳过闭包,把"事务内部丢调用/漏调用"这类回归掩盖掉,
  19. // 因此统一走 inline 执行;session 传 nil 即可,因为这里所有 tx 方法都是 mock。
  20. func runDeptTxInline(m *mocks.MockSysDeptModel) *gomock.Call {
  21. return m.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).DoAndReturn(
  22. func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  23. return fn(ctx, nil)
  24. },
  25. )
  26. }
  27. // TC-0105: UpdateDept 只清理自身部门用户缓存,不再级联到子部门。
  28. // 在当前"tx 包裹 UpdateWithOptLockTx + post-commit InvalidateDeptCache + 自身部门 FindIdsByDeptId"
  29. // 语义下,断言仍为:FindByPathPrefix 永不被调用;FindIdsByDeptId 仅针对本部门一次。
  30. func TestUpdateDept_Mock_CascadeCacheClean(t *testing.T) {
  31. ctrl := gomock.NewController(t)
  32. defer ctrl.Finish()
  33. parentDeptId := int64(10)
  34. mockDept := mocks.NewMockSysDeptModel(ctrl)
  35. mockDept.EXPECT().FindOne(gomock.Any(), parentDeptId).
  36. Return(&deptModel.SysDept{
  37. Id: parentDeptId,
  38. Name: "Parent",
  39. Path: "/10/",
  40. DeptType: "NORMAL",
  41. Status: 1,
  42. UpdateTime: 1000,
  43. }, nil)
  44. runDeptTxInline(mockDept)
  45. // 事务内:NORMAL→DEV 是"放宽权限"方向,不应吊销会话,因此不得触发
  46. // FindIdsByDeptIdForShareTx / BatchIncrementTokenVersionWithTx(未声明即等价 Times(0))。
  47. mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(1000)).Return(nil)
  48. // post-commit:dept 低层缓存必须失效,否则 FindOne 回旧值抹掉本次更新可见性。
  49. mockDept.EXPECT().InvalidateDeptCache(gomock.Any(), parentDeptId)
  50. mockUser := mocks.NewMockSysUserModel(ctrl)
  51. // post-commit:仅查询目标部门直属用户,不再级联查询子部门用户(无 FindByPathPrefix)。
  52. mockUser.EXPECT().FindIdsByDeptId(gomock.Any(), parentDeptId).
  53. Return([]int64{100, 101}, nil)
  54. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  55. Dept: mockDept,
  56. User: mockUser,
  57. })
  58. ctx := ctxhelper.SuperAdminCtx()
  59. logic := NewUpdateDeptLogic(ctx, svcCtx)
  60. err := logic.UpdateDept(&types.UpdateDeptReq{
  61. Id: parentDeptId,
  62. Name: "Parent Updated",
  63. DeptType: "DEV",
  64. })
  65. assert.NoError(t, err)
  66. }
  67. // TC-0714: UpdateDept 当 deptType 与 status 都未变化时,不触发任何用户缓存清理。
  68. // 关键回归:InvalidateDeptCache 仍需触发(dept 自身行被 UPDATE 过),但不得做任何 sys_user
  69. // 维度的扫描——避免频繁改名触发 O(部门规模) 的缓存失效风暴。
  70. func TestUpdateDept_Mock_NoCacheCleanWhenUnchanged(t *testing.T) {
  71. ctrl := gomock.NewController(t)
  72. defer ctrl.Finish()
  73. deptId := int64(11)
  74. mockDept := mocks.NewMockSysDeptModel(ctrl)
  75. mockDept.EXPECT().FindOne(gomock.Any(), deptId).
  76. Return(&deptModel.SysDept{
  77. Id: deptId,
  78. Name: "Old",
  79. Path: "/11/",
  80. DeptType: "NORMAL",
  81. Status: 1,
  82. UpdateTime: 2000,
  83. }, nil)
  84. runDeptTxInline(mockDept)
  85. mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(2000)).Return(nil)
  86. mockDept.EXPECT().InvalidateDeptCache(gomock.Any(), deptId)
  87. mockUser := mocks.NewMockSysUserModel(ctrl)
  88. // 不应调用 FindIdsByDeptId / FindIdsByDeptIdForShareTx —— 未设置期望,任何调用都会 FAIL
  89. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  90. Dept: mockDept,
  91. User: mockUser,
  92. })
  93. ctx := ctxhelper.SuperAdminCtx()
  94. logic := NewUpdateDeptLogic(ctx, svcCtx)
  95. err := logic.UpdateDept(&types.UpdateDeptReq{
  96. Id: deptId,
  97. Name: "New Name",
  98. DeptType: "NORMAL", // 与原值一致
  99. Status: 1, // 与原值一致
  100. })
  101. assert.NoError(t, err)
  102. }
  103. // TC-0715: UpdateDept 乐观锁冲突时返回 409 ErrConflict;且因为事务内就失败,post-commit
  104. // 的 InvalidateDeptCache / FindIdsByDeptId 绝不应被调用,避免"回滚了但缓存被清了"的不一致。
  105. func TestUpdateDept_Mock_OptLockConflict(t *testing.T) {
  106. ctrl := gomock.NewController(t)
  107. defer ctrl.Finish()
  108. deptId := int64(12)
  109. mockDept := mocks.NewMockSysDeptModel(ctrl)
  110. mockDept.EXPECT().FindOne(gomock.Any(), deptId).
  111. Return(&deptModel.SysDept{
  112. Id: deptId,
  113. Name: "Old",
  114. Path: "/12/",
  115. DeptType: "NORMAL",
  116. Status: 1,
  117. UpdateTime: 3000,
  118. }, nil)
  119. runDeptTxInline(mockDept)
  120. // 模拟并发:另一事务已更新该行,updateTime 不再匹配 —— tx 内部返回 ErrUpdateConflict,
  121. // TransactCtx 透传该错误,handler 映射为 409;post-commit 缓存失效这里必须不发生。
  122. mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(3000)).
  123. Return(deptModel.ErrUpdateConflict)
  124. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  125. Dept: mockDept,
  126. })
  127. ctx := ctxhelper.SuperAdminCtx()
  128. logic := NewUpdateDeptLogic(ctx, svcCtx)
  129. err := logic.UpdateDept(&types.UpdateDeptReq{
  130. Id: deptId,
  131. Name: "new",
  132. })
  133. assert.Error(t, err)
  134. // 期望 409 Conflict
  135. assert.Contains(t, err.Error(), "数据已被其他操作修改")
  136. }