updateDeptCleanBatch_audit_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package dept
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. "perms-system-server/internal/middleware"
  9. deptModel "perms-system-server/internal/model/dept"
  10. "perms-system-server/internal/testutil/mocks"
  11. "perms-system-server/internal/types"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. "go.uber.org/mock/gomock"
  15. )
  16. // ---------------------------------------------------------------------------
  17. // 覆盖目标:审计第 6 轮 M-1 修复回归 —— UpdateDept 在 deptType / status 真正变更时:
  18. // * 必须调用 SysUserModel.FindIdsByDeptId 获取受影响 userIds;
  19. // * FindIdsByDeptId 返回 err 时仅 Errorf 记录,handler 返回 nil(degraded 成功);
  20. // * FindIdsByDeptId 成功时只调用 "1 次",避免在 handler 里串行按用户 Clean 造成秒级延迟。
  21. // ---------------------------------------------------------------------------
  22. func superAdminCtx() context.Context {
  23. return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  24. UserId: 1, Username: "su",
  25. IsSuperAdmin: true, MemberType: consts.MemberTypeSuperAdmin,
  26. Status: consts.StatusEnabled,
  27. })
  28. }
  29. // TC-0848: deptType 变更 → FindIdsByDeptId 恰好调用 1 次,返回 [100, 101];handler 返回 nil。
  30. func TestUpdateDept_DeptTypeChanged_InvokesFindIdsOnce(t *testing.T) {
  31. ctrl := gomock.NewController(t)
  32. t.Cleanup(ctrl.Finish)
  33. deptMock := mocks.NewMockSysDeptModel(ctrl)
  34. userMock := mocks.NewMockSysUserModel(ctrl)
  35. // 老部门是 NORMAL,请求改成 DEV → deptTypeChanged=true 才能触发 FindIdsByDeptId。
  36. deptMock.EXPECT().FindOne(gomock.Any(), int64(77)).
  37. Return(&deptModel.SysDept{
  38. Id: 77, Name: "n", DeptType: consts.DeptTypeNormal,
  39. Status: consts.StatusEnabled, UpdateTime: 500,
  40. }, nil)
  41. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(500)).Return(nil)
  42. // 关键断言:恰好调用 1 次;返回的切片会被继续塞进 CleanByUserIds(loader 内部走 Redis)。
  43. userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(77)).
  44. Return([]int64{100, 101}, nil).Times(1)
  45. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  46. Dept: deptMock, User: userMock,
  47. })
  48. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  49. Id: 77, Name: "n", Sort: 0, Remark: "", DeptType: consts.DeptTypeDev,
  50. })
  51. require.NoError(t, err,
  52. "M-1:正常场景 UpdateDept 必须返回 nil,且仅调用一次 FindIdsByDeptId")
  53. }
  54. // TC-0849: FindIdsByDeptId 返回 err —— handler 仍返回 nil(degraded 成功),旧缓存由 TTL 过期兜底。
  55. func TestUpdateDept_FindIdsByDeptIdError_DegradedSuccess(t *testing.T) {
  56. ctrl := gomock.NewController(t)
  57. t.Cleanup(ctrl.Finish)
  58. deptMock := mocks.NewMockSysDeptModel(ctrl)
  59. userMock := mocks.NewMockSysUserModel(ctrl)
  60. deptMock.EXPECT().FindOne(gomock.Any(), int64(88)).
  61. Return(&deptModel.SysDept{
  62. Id: 88, Name: "n", DeptType: consts.DeptTypeNormal,
  63. Status: consts.StatusEnabled, UpdateTime: 1000,
  64. }, nil)
  65. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(1000)).Return(nil)
  66. userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(88)).
  67. Return(nil, errors.New("transient DB error"))
  68. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  69. Dept: deptMock, User: userMock,
  70. })
  71. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  72. Id: 88, Name: "n", DeptType: consts.DeptTypeDev,
  73. })
  74. assert.NoError(t, err,
  75. "M-1:FindIdsByDeptId 失败不得映射 500;TTL 过期兜底,客户端不应重试整次 UpdateDept")
  76. }
  77. // 补充:deptType / status 都没变时,不应调 FindIdsByDeptId(避免无效缓存失效风暴)。
  78. func TestUpdateDept_NoEffectiveChange_SkipsFindIds(t *testing.T) {
  79. ctrl := gomock.NewController(t)
  80. t.Cleanup(ctrl.Finish)
  81. deptMock := mocks.NewMockSysDeptModel(ctrl)
  82. userMock := mocks.NewMockSysUserModel(ctrl)
  83. // 老部门 DEV,请求也是 DEV;status 未传 → 没有变更。
  84. deptMock.EXPECT().FindOne(gomock.Any(), int64(99)).
  85. Return(&deptModel.SysDept{
  86. Id: 99, Name: "x", DeptType: consts.DeptTypeDev,
  87. Status: consts.StatusEnabled, UpdateTime: 200,
  88. }, nil)
  89. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(200)).Return(nil)
  90. // 关键:没有任何 FindIdsByDeptId EXPECT 即等价 Times(0)。
  91. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  92. Dept: deptMock, User: userMock,
  93. })
  94. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  95. Id: 99, Name: "x", DeptType: consts.DeptTypeDev, Sort: 1,
  96. })
  97. require.NoError(t, err, "M-1:无变更时 UpdateDept 只做元字段更新,不得触发缓存清理风暴")
  98. }