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(), "数据已被其他操作修改") }