updateDeptLogic_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package dept
  2. import (
  3. "context"
  4. "errors"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "go.uber.org/mock/gomock"
  8. "perms-system-server/internal/consts"
  9. "perms-system-server/internal/loaders"
  10. "perms-system-server/internal/middleware"
  11. deptModel "perms-system-server/internal/model/dept"
  12. "perms-system-server/internal/response"
  13. "perms-system-server/internal/svc"
  14. "perms-system-server/internal/testutil"
  15. "perms-system-server/internal/testutil/ctxhelper"
  16. "perms-system-server/internal/testutil/mocks"
  17. "perms-system-server/internal/types"
  18. "testing"
  19. )
  20. func TestUpdateDept_Normal(t *testing.T) {
  21. ctx := ctxhelper.SuperAdminCtx()
  22. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  23. conn := testutil.GetTestSqlConn()
  24. deptId, err := insertDeptRaw(ctx, svcCtx, 0, "upd_"+testutil.UniqueId(), "/")
  25. require.NoError(t, err)
  26. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  27. newName := "updated_" + testutil.UniqueId()
  28. l := NewUpdateDeptLogic(ctx, svcCtx)
  29. err = l.UpdateDept(&types.UpdateDeptReq{
  30. Id: deptId,
  31. Name: newName,
  32. Sort: 99,
  33. Remark: "updated remark",
  34. Status: 2,
  35. })
  36. require.NoError(t, err)
  37. d, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  38. require.NoError(t, err)
  39. assert.Equal(t, newName, d.Name)
  40. assert.Equal(t, int64(99), d.Sort)
  41. assert.Equal(t, "updated remark", d.Remark)
  42. assert.Equal(t, int64(2), d.Status)
  43. }
  44. // TC-0102: 不存在
  45. func TestUpdateDept_NotFound(t *testing.T) {
  46. ctx := ctxhelper.SuperAdminCtx()
  47. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  48. l := NewUpdateDeptLogic(ctx, svcCtx)
  49. err := l.UpdateDept(&types.UpdateDeptReq{
  50. Id: 999999999,
  51. Name: "ghost_" + testutil.UniqueId(),
  52. })
  53. require.Error(t, err)
  54. var ce *response.CodeError
  55. require.True(t, errors.As(err, &ce))
  56. assert.Equal(t, 404, ce.Code())
  57. assert.Equal(t, "部门不存在", ce.Error())
  58. }
  59. // TC-0101: 正常更新
  60. func TestUpdateDept_StatusZeroUnchanged(t *testing.T) {
  61. ctx := ctxhelper.SuperAdminCtx()
  62. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  63. conn := testutil.GetTestSqlConn()
  64. deptId, err := insertDeptRaw(ctx, svcCtx, 0, "s0_"+testutil.UniqueId(), "/")
  65. require.NoError(t, err)
  66. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  67. before, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  68. require.NoError(t, err)
  69. assert.Equal(t, int64(1), before.Status)
  70. l := NewUpdateDeptLogic(ctx, svcCtx)
  71. err = l.UpdateDept(&types.UpdateDeptReq{
  72. Id: deptId,
  73. Name: "changed_" + testutil.UniqueId(),
  74. Status: 0,
  75. })
  76. require.NoError(t, err)
  77. after, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  78. require.NoError(t, err)
  79. assert.Equal(t, int64(1), after.Status)
  80. }
  81. // TC-0103: DeptType NORMAL→DEV
  82. func TestUpdateDept_DeptType_NormalToDev(t *testing.T) {
  83. ctx := ctxhelper.SuperAdminCtx()
  84. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  85. conn := testutil.GetTestSqlConn()
  86. deptId, err := insertDeptRaw(ctx, svcCtx, 0, "dt_"+testutil.UniqueId(), "/")
  87. require.NoError(t, err)
  88. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  89. before, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  90. require.NoError(t, err)
  91. assert.Equal(t, "NORMAL", before.DeptType)
  92. l := NewUpdateDeptLogic(ctx, svcCtx)
  93. err = l.UpdateDept(&types.UpdateDeptReq{
  94. Id: deptId,
  95. Name: "dt_changed_" + testutil.UniqueId(),
  96. DeptType: "DEV",
  97. })
  98. require.NoError(t, err)
  99. after, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  100. require.NoError(t, err)
  101. assert.Equal(t, "DEV", after.DeptType)
  102. }
  103. // TC-0104: DeptType无效值返回错误
  104. func TestUpdateDept_DeptType_InvalidRejected(t *testing.T) {
  105. ctx := ctxhelper.SuperAdminCtx()
  106. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  107. conn := testutil.GetTestSqlConn()
  108. deptId, err := insertDeptRaw(ctx, svcCtx, 0, "dti_"+testutil.UniqueId(), "/")
  109. require.NoError(t, err)
  110. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  111. l := NewUpdateDeptLogic(ctx, svcCtx)
  112. err = l.UpdateDept(&types.UpdateDeptReq{
  113. Id: deptId,
  114. Name: "dti_changed_" + testutil.UniqueId(),
  115. DeptType: "INVALID_TYPE",
  116. })
  117. require.Error(t, err)
  118. var ce *response.CodeError
  119. require.True(t, errors.As(err, &ce))
  120. assert.Equal(t, 400, ce.Code())
  121. assert.Contains(t, ce.Error(), "部门类型无效")
  122. after, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  123. require.NoError(t, err)
  124. assert.Equal(t, "NORMAL", after.DeptType, "无效DeptType不应修改数据库")
  125. }
  126. // TC-0533: updateDept非超管拒绝
  127. func TestUpdateDept_NonSuperAdminRejected(t *testing.T) {
  128. ctx := ctxhelper.AdminCtx("test_product")
  129. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  130. l := NewUpdateDeptLogic(ctx, svcCtx)
  131. err := l.UpdateDept(&types.UpdateDeptReq{Id: 1, Name: "test"})
  132. require.Error(t, err)
  133. var ce *response.CodeError
  134. require.True(t, errors.As(err, &ce))
  135. assert.Equal(t, 403, ce.Code())
  136. }
  137. func superAdminCtx() context.Context {
  138. return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  139. UserId: 1, Username: "su",
  140. IsSuperAdmin: true, MemberType: consts.MemberTypeSuperAdmin,
  141. Status: consts.StatusEnabled,
  142. })
  143. }
  144. // TC-0848: deptType 变更 → FindIdsByDeptId 恰好调用 1 次,返回 [100, 101];handler 返回 nil。
  145. func TestUpdateDept_DeptTypeChanged_InvokesFindIdsOnce(t *testing.T) {
  146. ctrl := gomock.NewController(t)
  147. t.Cleanup(ctrl.Finish)
  148. deptMock := mocks.NewMockSysDeptModel(ctrl)
  149. userMock := mocks.NewMockSysUserModel(ctrl)
  150. // 老部门是 NORMAL,请求改成 DEV → deptTypeChanged=true 才能触发 FindIdsByDeptId。
  151. deptMock.EXPECT().FindOne(gomock.Any(), int64(77)).
  152. Return(&deptModel.SysDept{
  153. Id: 77, Name: "n", DeptType: consts.DeptTypeNormal,
  154. Status: consts.StatusEnabled, UpdateTime: 500,
  155. }, nil)
  156. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(500)).Return(nil)
  157. // 关键断言:恰好调用 1 次;返回的切片会被继续塞进 CleanByUserIds(loader 内部走 Redis)。
  158. userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(77)).
  159. Return([]int64{100, 101}, nil).Times(1)
  160. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  161. Dept: deptMock, User: userMock,
  162. })
  163. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  164. Id: 77, Name: "n", Sort: 0, Remark: "", DeptType: consts.DeptTypeDev,
  165. })
  166. require.NoError(t, err,
  167. "正常场景 UpdateDept 必须返回 nil,且仅调用一次 FindIdsByDeptId")
  168. }
  169. // TC-0849: FindIdsByDeptId 返回 err —— handler 仍返回 nil(degraded 成功),旧缓存由 TTL 过期兜底。
  170. func TestUpdateDept_FindIdsByDeptIdError_DegradedSuccess(t *testing.T) {
  171. ctrl := gomock.NewController(t)
  172. t.Cleanup(ctrl.Finish)
  173. deptMock := mocks.NewMockSysDeptModel(ctrl)
  174. userMock := mocks.NewMockSysUserModel(ctrl)
  175. deptMock.EXPECT().FindOne(gomock.Any(), int64(88)).
  176. Return(&deptModel.SysDept{
  177. Id: 88, Name: "n", DeptType: consts.DeptTypeNormal,
  178. Status: consts.StatusEnabled, UpdateTime: 1000,
  179. }, nil)
  180. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(1000)).Return(nil)
  181. userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(88)).
  182. Return(nil, errors.New("transient DB error"))
  183. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  184. Dept: deptMock, User: userMock,
  185. })
  186. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  187. Id: 88, Name: "n", DeptType: consts.DeptTypeDev,
  188. })
  189. assert.NoError(t, err,
  190. "FindIdsByDeptId 失败不得映射 500;TTL 过期兜底,客户端不应重试整次 UpdateDept")
  191. }
  192. // 补充:deptType / status 都没变时,不应调 FindIdsByDeptId(避免无效缓存失效风暴)。
  193. func TestUpdateDept_NoEffectiveChange_SkipsFindIds(t *testing.T) {
  194. ctrl := gomock.NewController(t)
  195. t.Cleanup(ctrl.Finish)
  196. deptMock := mocks.NewMockSysDeptModel(ctrl)
  197. userMock := mocks.NewMockSysUserModel(ctrl)
  198. // 老部门 DEV,请求也是 DEV;status 未传 → 没有变更。
  199. deptMock.EXPECT().FindOne(gomock.Any(), int64(99)).
  200. Return(&deptModel.SysDept{
  201. Id: 99, Name: "x", DeptType: consts.DeptTypeDev,
  202. Status: consts.StatusEnabled, UpdateTime: 200,
  203. }, nil)
  204. deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(200)).Return(nil)
  205. // 关键:没有任何 FindIdsByDeptId EXPECT 即等价 Times(0)。
  206. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  207. Dept: deptMock, User: userMock,
  208. })
  209. err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{
  210. Id: 99, Name: "x", DeptType: consts.DeptTypeDev, Sort: 1,
  211. })
  212. require.NoError(t, err, "无变更时 UpdateDept 只做元字段更新,不得触发缓存清理风暴")
  213. }