package dept import ( "context" "errors" "testing" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" "perms-system-server/internal/middleware" deptModel "perms-system-server/internal/model/dept" "perms-system-server/internal/testutil/mocks" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) // --------------------------------------------------------------------------- // 覆盖目标:审计第 6 轮 M-1 修复回归 —— UpdateDept 在 deptType / status 真正变更时: // * 必须调用 SysUserModel.FindIdsByDeptId 获取受影响 userIds; // * FindIdsByDeptId 返回 err 时仅 Errorf 记录,handler 返回 nil(degraded 成功); // * FindIdsByDeptId 成功时只调用 "1 次",避免在 handler 里串行按用户 Clean 造成秒级延迟。 // --------------------------------------------------------------------------- func superAdminCtx() context.Context { return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 1, Username: "su", IsSuperAdmin: true, MemberType: consts.MemberTypeSuperAdmin, Status: consts.StatusEnabled, }) } // TC-0848: deptType 变更 → FindIdsByDeptId 恰好调用 1 次,返回 [100, 101];handler 返回 nil。 func TestUpdateDept_DeptTypeChanged_InvokesFindIdsOnce(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) deptMock := mocks.NewMockSysDeptModel(ctrl) userMock := mocks.NewMockSysUserModel(ctrl) // 老部门是 NORMAL,请求改成 DEV → deptTypeChanged=true 才能触发 FindIdsByDeptId。 deptMock.EXPECT().FindOne(gomock.Any(), int64(77)). Return(&deptModel.SysDept{ Id: 77, Name: "n", DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, UpdateTime: 500, }, nil) deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(500)).Return(nil) // 关键断言:恰好调用 1 次;返回的切片会被继续塞进 CleanByUserIds(loader 内部走 Redis)。 userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(77)). Return([]int64{100, 101}, nil).Times(1) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Dept: deptMock, User: userMock, }) err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{ Id: 77, Name: "n", Sort: 0, Remark: "", DeptType: consts.DeptTypeDev, }) require.NoError(t, err, "M-1:正常场景 UpdateDept 必须返回 nil,且仅调用一次 FindIdsByDeptId") } // TC-0849: FindIdsByDeptId 返回 err —— handler 仍返回 nil(degraded 成功),旧缓存由 TTL 过期兜底。 func TestUpdateDept_FindIdsByDeptIdError_DegradedSuccess(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) deptMock := mocks.NewMockSysDeptModel(ctrl) userMock := mocks.NewMockSysUserModel(ctrl) deptMock.EXPECT().FindOne(gomock.Any(), int64(88)). Return(&deptModel.SysDept{ Id: 88, Name: "n", DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, UpdateTime: 1000, }, nil) deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(1000)).Return(nil) userMock.EXPECT().FindIdsByDeptId(gomock.Any(), int64(88)). Return(nil, errors.New("transient DB error")) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Dept: deptMock, User: userMock, }) err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{ Id: 88, Name: "n", DeptType: consts.DeptTypeDev, }) assert.NoError(t, err, "M-1:FindIdsByDeptId 失败不得映射 500;TTL 过期兜底,客户端不应重试整次 UpdateDept") } // 补充:deptType / status 都没变时,不应调 FindIdsByDeptId(避免无效缓存失效风暴)。 func TestUpdateDept_NoEffectiveChange_SkipsFindIds(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) deptMock := mocks.NewMockSysDeptModel(ctrl) userMock := mocks.NewMockSysUserModel(ctrl) // 老部门 DEV,请求也是 DEV;status 未传 → 没有变更。 deptMock.EXPECT().FindOne(gomock.Any(), int64(99)). Return(&deptModel.SysDept{ Id: 99, Name: "x", DeptType: consts.DeptTypeDev, Status: consts.StatusEnabled, UpdateTime: 200, }, nil) deptMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(200)).Return(nil) // 关键:没有任何 FindIdsByDeptId EXPECT 即等价 Times(0)。 svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Dept: deptMock, User: userMock, }) err := NewUpdateDeptLogic(superAdminCtx(), svcCtx).UpdateDept(&types.UpdateDeptReq{ Id: 99, Name: "x", DeptType: consts.DeptTypeDev, Sort: 1, }) require.NoError(t, err, "M-1:无变更时 UpdateDept 只做元字段更新,不得触发缓存清理风暴") }