package user import ( "context" "errors" "testing" memberModel "perms-system-server/internal/model/productmember" roleModel "perms-system-server/internal/model/role" userModel "perms-system-server/internal/model/user" "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" ) // TC-0187: 事务回滚 func TestBindRoles_Mock_BatchInsertFail(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() dbErr := errors.New("db error") mockUser := mocks.NewMockSysUserModel(ctrl) mockUser.EXPECT().FindOne(gomock.Any(), int64(1)). Return(&userModel.SysUser{Id: 1}, nil) mockPM := mocks.NewMockSysProductMemberModel(ctrl) mockPM.EXPECT().FindOneByProductCodeUserId(gomock.Any(), "test_product", int64(1)). Return(&memberModel.SysProductMember{Id: 1, ProductCode: "test_product", UserId: 1, Status: 1}, nil) // 事务首步 FindOneForUpdateTx 锁 sys_product_member 行 mockPM.EXPECT().FindOneForUpdateTx(gomock.Any(), nil, int64(1)). Return(&memberModel.SysProductMember{Id: 1, ProductCode: "test_product", UserId: 1, Status: 1}, nil) mockRole := mocks.NewMockSysRoleModel(ctrl) mockRole.EXPECT().FindByIds(gomock.Any(), []int64{10, 20}). Return([]*roleModel.SysRole{ {Id: 10, ProductCode: "test_product", Status: 1}, {Id: 20, ProductCode: "test_product", Status: 1}, }, nil) // 事务内对 toAdd/入参 roleIds 加 S 锁以闭合 BindRoles × DeleteRole 写偏斜。 // mock 直接放行(真实表现为 `SELECT id FROM sys_role WHERE id IN (10,20) AND status=1 LOCK IN SHARE MODE` // 命中两行无错)。 mockRole.EXPECT().LockRolesForShareTx(gomock.Any(), nil, []int64{10, 20}).Return(nil) mockUR := mocks.NewMockSysUserRoleModel(ctrl) mockUR.EXPECT().FindRoleIdsByUserIdForProductTx(gomock.Any(), nil, int64(1), "test_product").Return([]int64{}, nil) mockUR.EXPECT().TableName().Return("`sys_user_role`").AnyTimes() mockUR.EXPECT().TransactCtx(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { return fn(ctx, nil) }) // audit 修复:循环 DELETE 替换为批量 DeleteByUserIdAndRoleIdsTx;toRemove 为空时也被调用 mockUR.EXPECT().DeleteByUserIdAndRoleIdsTx(gomock.Any(), nil, int64(1), []int64(nil)).Return(nil) mockUR.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(dbErr) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ User: mockUser, Role: mockRole, UserRole: mockUR, ProductMember: mockPM, }) logic := NewBindRolesLogic(ctxhelper.SuperAdminCtx(), svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: 1, RoleIds: []int64{10, 20}, ProductCode: "test_product", }) assert.Error(t, err) assert.ErrorIs(t, err, dbErr) }