package pub import ( "context" "errors" "testing" permModel "perms-system-server/internal/model/perm" productModel "perms-system-server/internal/model/product" "perms-system-server/internal/response" "perms-system-server/internal/testutil/mocks" "perms-system-server/internal/types" "github.com/go-sql-driver/mysql" "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" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-6 修复 —— 并发同步同一 product 的权限列表时,事务内因 UNIQUE(productCode, code) // 撞出 MySQL errno 1062,service 必须返回 SyncPermsError{Code:409} 并最终让 logic 层映射成 // HTTP 409(ErrConflict),而不是吞成 500 让接入方看不到"重试即可"的信号。 // --------------------------------------------------------------------------- // TC-0824: M-6 —— BatchInsert 在事务内冒出 DuplicateEntry → SyncPermsError.Code == 409。 func TestExecuteSyncPerms_DuplicateEntry_Maps409(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak"). Return(&productModel.SysProduct{ Id: 1, Code: "pc_m6", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1, }, nil) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6"). Return(map[string]*permModel.SysPerm{}, nil) dupErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'pc_m6-x' for key 'uk_product_code'"} 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(dupErr) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Product: mockProduct, Perm: mockPerm, }) result, err := ExecuteSyncPerms( context.Background(), svcCtx, "ak", "s", []SyncPermItem{{Code: "x", Name: "X"}}, ) assert.Nil(t, result) require.Error(t, err) var se *SyncPermsError require.True(t, errors.As(err, &se), "必须是 *SyncPermsError 以便 logic 层映射") assert.Equal(t, 409, se.Code, "M-6:tx 内 1062 必须映射成 409,让接入方据此重试;修复前这里是 500") assert.Contains(t, se.Message, "并发冲突") } // TC-0825: M-6 logic 映射 —— SyncPermsError{Code:409} 必须通过 SyncPermsLogic.SyncPerms 映射成 HTTP 409。 func TestSyncPermsLogic_ConflictMapsTo409HTTP(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak"). Return(&productModel.SysProduct{ Id: 1, Code: "pc_m6_h", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1, }, nil) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6_h"). Return(map[string]*permModel.SysPerm{}, nil) dupErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'y' for key 'uk'"} 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(dupErr) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Product: mockProduct, Perm: mockPerm, }) resp, err := NewSyncPermsLogic(context.Background(), svcCtx).SyncPerms(&types.SyncPermsReq{ AppKey: "ak", AppSecret: "s", Perms: []types.SyncPermItem{{Code: "y", Name: "Y"}}, }) assert.Nil(t, resp) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce), "必须是 response.CodeError") assert.Equal(t, 409, ce.Code(), "修复前这是 500,修复后 logic switch 里新增 case 409 把 ErrConflict 映射到 HTTP 409") } // TC-0826: M-6 去重 —— 请求里同一 code 出现多次时,service 内部要先去重, // 避免 tx 内批量 INSERT 自己和自己撞 1062。 func TestExecuteSyncPerms_DeduplicatesRequest(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak"). Return(&productModel.SysProduct{ Id: 1, Code: "pc_m6_dedup", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1, }, nil) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6_dedup"). Return(map[string]*permModel.SysPerm{}, nil) // 关键:BatchInsertWithTx 拿到的切片必须只含 1 条"dup_code"(而不是重复的 3 条)。 var captured []*permModel.SysPerm 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()). DoAndReturn(func(ctx context.Context, s sqlx.Session, items []*permModel.SysPerm) error { captured = items return nil }) // 去重后 codes 应当是 ["dup_code"],DisableNotInCodesWithTx 用 codes 做 NOT IN。 mockPerm.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_m6_dedup", []string{"dup_code"}, gomock.Any()). Return(int64(0), nil) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Product: mockProduct, Perm: mockPerm, }) result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s", []SyncPermItem{ {Code: "dup_code", Name: "A"}, {Code: "dup_code", Name: "A-again"}, {Code: "dup_code", Name: "A-yet-again"}, }) require.NoError(t, err) require.NotNil(t, result) require.Len(t, captured, 1, "M-6:请求内 code 去重后只能 INSERT 一条,避免自撞 1062") assert.Equal(t, "dup_code", captured[0].Code) // 第一次出现时的 Name 被保留(去重策略应当稳定到首次出现)。 assert.Equal(t, "A", captured[0].Name, "去重应保留首次出现的属性,使行为可预测") }