syncPermsConflict_audit_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. permModel "perms-system-server/internal/model/perm"
  7. productModel "perms-system-server/internal/model/product"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/testutil/mocks"
  10. "perms-system-server/internal/types"
  11. "github.com/go-sql-driver/mysql"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. "github.com/zeromicro/go-zero/core/stores/sqlx"
  15. "go.uber.org/mock/gomock"
  16. "golang.org/x/crypto/bcrypt"
  17. )
  18. // ---------------------------------------------------------------------------
  19. // 覆盖目标:审计 M-6 修复 —— 并发同步同一 product 的权限列表时,事务内因 UNIQUE(productCode, code)
  20. // 撞出 MySQL errno 1062,service 必须返回 SyncPermsError{Code:409} 并最终让 logic 层映射成
  21. // HTTP 409(ErrConflict),而不是吞成 500 让接入方看不到"重试即可"的信号。
  22. // ---------------------------------------------------------------------------
  23. // TC-0824: M-6 —— BatchInsert 在事务内冒出 DuplicateEntry → SyncPermsError.Code == 409。
  24. func TestExecuteSyncPerms_DuplicateEntry_Maps409(t *testing.T) {
  25. ctrl := gomock.NewController(t)
  26. t.Cleanup(ctrl.Finish)
  27. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  28. require.NoError(t, err)
  29. mockProduct := mocks.NewMockSysProductModel(ctrl)
  30. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  31. Return(&productModel.SysProduct{
  32. Id: 1, Code: "pc_m6", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
  33. }, nil)
  34. mockPerm := mocks.NewMockSysPermModel(ctrl)
  35. mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6").
  36. Return(map[string]*permModel.SysPerm{}, nil)
  37. dupErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'pc_m6-x' for key 'uk_product_code'"}
  38. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  39. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  40. return fn(ctx, nil)
  41. })
  42. mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(dupErr)
  43. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  44. Product: mockProduct, Perm: mockPerm,
  45. })
  46. result, err := ExecuteSyncPerms(
  47. context.Background(), svcCtx, "ak", "s",
  48. []SyncPermItem{{Code: "x", Name: "X"}},
  49. )
  50. assert.Nil(t, result)
  51. require.Error(t, err)
  52. var se *SyncPermsError
  53. require.True(t, errors.As(err, &se), "必须是 *SyncPermsError 以便 logic 层映射")
  54. assert.Equal(t, 409, se.Code,
  55. "M-6:tx 内 1062 必须映射成 409,让接入方据此重试;修复前这里是 500")
  56. assert.Contains(t, se.Message, "并发冲突")
  57. }
  58. // TC-0825: M-6 logic 映射 —— SyncPermsError{Code:409} 必须通过 SyncPermsLogic.SyncPerms 映射成 HTTP 409。
  59. func TestSyncPermsLogic_ConflictMapsTo409HTTP(t *testing.T) {
  60. ctrl := gomock.NewController(t)
  61. t.Cleanup(ctrl.Finish)
  62. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  63. require.NoError(t, err)
  64. mockProduct := mocks.NewMockSysProductModel(ctrl)
  65. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  66. Return(&productModel.SysProduct{
  67. Id: 1, Code: "pc_m6_h", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
  68. }, nil)
  69. mockPerm := mocks.NewMockSysPermModel(ctrl)
  70. mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6_h").
  71. Return(map[string]*permModel.SysPerm{}, nil)
  72. dupErr := &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'y' for key 'uk'"}
  73. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  74. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  75. return fn(ctx, nil)
  76. })
  77. mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(dupErr)
  78. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  79. Product: mockProduct, Perm: mockPerm,
  80. })
  81. resp, err := NewSyncPermsLogic(context.Background(), svcCtx).SyncPerms(&types.SyncPermsReq{
  82. AppKey: "ak", AppSecret: "s",
  83. Perms: []types.SyncPermItem{{Code: "y", Name: "Y"}},
  84. })
  85. assert.Nil(t, resp)
  86. require.Error(t, err)
  87. var ce *response.CodeError
  88. require.True(t, errors.As(err, &ce), "必须是 response.CodeError")
  89. assert.Equal(t, 409, ce.Code(),
  90. "修复前这是 500,修复后 logic switch 里新增 case 409 把 ErrConflict 映射到 HTTP 409")
  91. }
  92. // TC-0826: M-6 去重 —— 请求里同一 code 出现多次时,service 内部要先去重,
  93. // 避免 tx 内批量 INSERT 自己和自己撞 1062。
  94. func TestExecuteSyncPerms_DeduplicatesRequest(t *testing.T) {
  95. ctrl := gomock.NewController(t)
  96. t.Cleanup(ctrl.Finish)
  97. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  98. require.NoError(t, err)
  99. mockProduct := mocks.NewMockSysProductModel(ctrl)
  100. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  101. Return(&productModel.SysProduct{
  102. Id: 1, Code: "pc_m6_dedup", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
  103. }, nil)
  104. mockPerm := mocks.NewMockSysPermModel(ctrl)
  105. mockPerm.EXPECT().FindMapByProductCode(gomock.Any(), "pc_m6_dedup").
  106. Return(map[string]*permModel.SysPerm{}, nil)
  107. // 关键:BatchInsertWithTx 拿到的切片必须只含 1 条"dup_code"(而不是重复的 3 条)。
  108. var captured []*permModel.SysPerm
  109. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  110. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  111. return fn(ctx, nil)
  112. })
  113. mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).
  114. DoAndReturn(func(ctx context.Context, s sqlx.Session, items []*permModel.SysPerm) error {
  115. captured = items
  116. return nil
  117. })
  118. // 去重后 codes 应当是 ["dup_code"],DisableNotInCodesWithTx 用 codes 做 NOT IN。
  119. mockPerm.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_m6_dedup", []string{"dup_code"}, gomock.Any()).
  120. Return(int64(0), nil)
  121. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  122. Product: mockProduct, Perm: mockPerm,
  123. })
  124. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s", []SyncPermItem{
  125. {Code: "dup_code", Name: "A"},
  126. {Code: "dup_code", Name: "A-again"},
  127. {Code: "dup_code", Name: "A-yet-again"},
  128. })
  129. require.NoError(t, err)
  130. require.NotNil(t, result)
  131. require.Len(t, captured, 1, "M-6:请求内 code 去重后只能 INSERT 一条,避免自撞 1062")
  132. assert.Equal(t, "dup_code", captured[0].Code)
  133. // 第一次出现时的 Name 被保留(去重策略应当稳定到首次出现)。
  134. assert.Equal(t, "A", captured[0].Name, "去重应保留首次出现的属性,使行为可预测")
  135. }