| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- package dept
- import (
- "context"
- "testing"
- deptModel "perms-system-server/internal/model/dept"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/testutil/mocks"
- "perms-system-server/internal/types"
- "github.com/stretchr/testify/assert"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- "go.uber.org/mock/gomock"
- )
- // runDeptTxInline 让 TransactCtx 的期望就地把传入的闭包执行掉,从而让我们能在 gomock 层面
- // 同时观察 tx 内部(UpdateWithOptLockTx / FindIdsByDeptIdForShareTx /
- // BatchIncrementTokenVersionWithTx)与 tx 外部(InvalidateDeptCache / FindIdsByDeptId 等
- // post-commit 钩子)的调用次数与顺序。
- //
- // 直接 `Return(nil)` 的传统写法会跳过闭包,把"事务内部丢调用/漏调用"这类回归掩盖掉,
- // 因此统一走 inline 执行;session 传 nil 即可,因为这里所有 tx 方法都是 mock。
- func runDeptTxInline(m *mocks.MockSysDeptModel) *gomock.Call {
- return m.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).DoAndReturn(
- func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
- return fn(ctx, nil)
- },
- )
- }
- // TC-0105: UpdateDept 只清理自身部门用户缓存,不再级联到子部门。
- // 在当前"tx 包裹 UpdateWithOptLockTx + post-commit InvalidateDeptCache + 自身部门 FindIdsByDeptId"
- // 语义下,断言仍为:FindByPathPrefix 永不被调用;FindIdsByDeptId 仅针对本部门一次。
- func TestUpdateDept_Mock_CascadeCacheClean(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- parentDeptId := int64(10)
- mockDept := mocks.NewMockSysDeptModel(ctrl)
- mockDept.EXPECT().FindOne(gomock.Any(), parentDeptId).
- Return(&deptModel.SysDept{
- Id: parentDeptId,
- Name: "Parent",
- Path: "/10/",
- DeptType: "NORMAL",
- Status: 1,
- UpdateTime: 1000,
- }, nil)
- runDeptTxInline(mockDept)
- // 事务内:NORMAL→DEV 是"放宽权限"方向,不应吊销会话,因此不得触发
- // FindIdsByDeptIdForShareTx / BatchIncrementTokenVersionWithTx(未声明即等价 Times(0))。
- mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(1000)).Return(nil)
- // post-commit:dept 低层缓存必须失效,否则 FindOne 回旧值抹掉本次更新可见性。
- mockDept.EXPECT().InvalidateDeptCache(gomock.Any(), parentDeptId)
- mockUser := mocks.NewMockSysUserModel(ctrl)
- // post-commit:仅查询目标部门直属用户,不再级联查询子部门用户(无 FindByPathPrefix)。
- mockUser.EXPECT().FindIdsByDeptId(gomock.Any(), parentDeptId).
- Return([]int64{100, 101}, nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- Dept: mockDept,
- User: mockUser,
- })
- ctx := ctxhelper.SuperAdminCtx()
- logic := NewUpdateDeptLogic(ctx, svcCtx)
- err := logic.UpdateDept(&types.UpdateDeptReq{
- Id: parentDeptId,
- Name: "Parent Updated",
- DeptType: "DEV",
- })
- assert.NoError(t, err)
- }
- // TC-0714: UpdateDept 当 deptType 与 status 都未变化时,不触发任何用户缓存清理。
- // 关键回归:InvalidateDeptCache 仍需触发(dept 自身行被 UPDATE 过),但不得做任何 sys_user
- // 维度的扫描——避免频繁改名触发 O(部门规模) 的缓存失效风暴。
- func TestUpdateDept_Mock_NoCacheCleanWhenUnchanged(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- deptId := int64(11)
- mockDept := mocks.NewMockSysDeptModel(ctrl)
- mockDept.EXPECT().FindOne(gomock.Any(), deptId).
- Return(&deptModel.SysDept{
- Id: deptId,
- Name: "Old",
- Path: "/11/",
- DeptType: "NORMAL",
- Status: 1,
- UpdateTime: 2000,
- }, nil)
- runDeptTxInline(mockDept)
- mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(2000)).Return(nil)
- mockDept.EXPECT().InvalidateDeptCache(gomock.Any(), deptId)
- mockUser := mocks.NewMockSysUserModel(ctrl)
- // 不应调用 FindIdsByDeptId / FindIdsByDeptIdForShareTx —— 未设置期望,任何调用都会 FAIL
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- Dept: mockDept,
- User: mockUser,
- })
- ctx := ctxhelper.SuperAdminCtx()
- logic := NewUpdateDeptLogic(ctx, svcCtx)
- err := logic.UpdateDept(&types.UpdateDeptReq{
- Id: deptId,
- Name: "New Name",
- DeptType: "NORMAL", // 与原值一致
- Status: 1, // 与原值一致
- })
- assert.NoError(t, err)
- }
- // TC-0715: UpdateDept 乐观锁冲突时返回 409 ErrConflict;且因为事务内就失败,post-commit
- // 的 InvalidateDeptCache / FindIdsByDeptId 绝不应被调用,避免"回滚了但缓存被清了"的不一致。
- func TestUpdateDept_Mock_OptLockConflict(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- deptId := int64(12)
- mockDept := mocks.NewMockSysDeptModel(ctrl)
- mockDept.EXPECT().FindOne(gomock.Any(), deptId).
- Return(&deptModel.SysDept{
- Id: deptId,
- Name: "Old",
- Path: "/12/",
- DeptType: "NORMAL",
- Status: 1,
- UpdateTime: 3000,
- }, nil)
- runDeptTxInline(mockDept)
- // 模拟并发:另一事务已更新该行,updateTime 不再匹配 —— tx 内部返回 ErrUpdateConflict,
- // TransactCtx 透传该错误,handler 映射为 409;post-commit 缓存失效这里必须不发生。
- mockDept.EXPECT().UpdateWithOptLockTx(gomock.Any(), gomock.Any(), gomock.Any(), int64(3000)).
- Return(deptModel.ErrUpdateConflict)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- Dept: mockDept,
- })
- ctx := ctxhelper.SuperAdminCtx()
- logic := NewUpdateDeptLogic(ctx, svcCtx)
- err := logic.UpdateDept(&types.UpdateDeptReq{
- Id: deptId,
- Name: "new",
- })
- assert.Error(t, err)
- // 期望 409 Conflict
- assert.Contains(t, err.Error(), "数据已被其他操作修改")
- }
|