package pub import ( "context" "errors" "testing" permModel "perms-system-server/internal/model/perm" productModel "perms-system-server/internal/model/product" "perms-system-server/internal/testutil/mocks" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/sqlx" "go.uber.org/mock/gomock" "golang.org/x/crypto/bcrypt" ) // TC-0048: 事务保护 —— BatchUpdate 失败时,service 必须回滚整个事务并对外返回 // SyncPermsError{500, "同步权限事务失败"}(不得泄漏内部 DB 驱动错误)。 // // 旧版本使用已废弃的 FindMapByProductCode(非事务版)做 mock;H-3 修复后读/锁都必须 // 落在同一个 tx 里,这里按新契约重写 mock:LockByCodeTx → FindMapByProductCodeWithTx → // BatchInsertWithTx OK → BatchUpdateWithTx 报错 → 统一 500。 func TestSyncPerms_Mock_TransactionRollbackOnBatchUpdateFail(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() dbErr := errors.New("batch update failed") hashedSecret, err := bcrypt.GenerateFromPassword([]byte("test_app_secret"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "test_app_key"). Return(&productModel.SysProduct{ Id: 1, Code: "test_product", AppKey: "test_app_key", AppSecret: string(hashedSecret), Status: 1, }, nil) // H-3:tx 内必须先 LockByCodeTx 锁 product 行,再 FindMapByProductCodeWithTx。 // 审计 M-R10-1:LockByCodeTx 拿到的行必须 Status=1,否则直接 403 不进入 diff mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "test_product"). Return(&productModel.SysProduct{Id: 1, Code: "test_product", Status: 1}, nil) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "test_product"). Return(map[string]*permModel.SysPerm{ "existing_code": { Id: 10, ProductCode: "test_product", Code: "existing_code", Name: "Old Name", Remark: "old remark", Status: 1, }, }, nil) mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { return fn(ctx, nil) }) mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(nil) mockPerm.EXPECT().BatchUpdateWithTx(gomock.Any(), nil, gomock.Any()).Return(dbErr) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Product: mockProduct, Perm: mockPerm, }) logic := NewSyncPermsLogic(context.Background(), svcCtx) resp, err := logic.SyncPerms(&types.SyncPermsReq{ AppKey: "test_app_key", AppSecret: "test_app_secret", Perms: []types.SyncPermItem{ {Code: "new_code", Name: "New Perm"}, {Code: "existing_code", Name: "Updated Name", Remark: "new remark"}, }, }) assert.Nil(t, resp) require.Error(t, err) // H-3 后的统一错误文案;原 DB 驱动错误必须被吞掉,避免泄漏内部实现。 assert.Contains(t, err.Error(), "同步权限事务失败") assert.NotContains(t, err.Error(), "batch update failed", "内部 DB 错误不得透传到客户端") }