createProductConflict_audit_test.go 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. package product
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. productModel "perms-system-server/internal/model/product"
  7. userModel "perms-system-server/internal/model/user"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/testutil/ctxhelper"
  10. "perms-system-server/internal/testutil/mocks"
  11. "perms-system-server/internal/types"
  12. "github.com/go-sql-driver/mysql"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. "github.com/zeromicro/go-zero/core/stores/sqlx"
  16. "go.uber.org/mock/gomock"
  17. )
  18. // ---------------------------------------------------------------------------
  19. // 覆盖目标:审计 M-5 修复 —— 旧实现用 strings.Contains(err, "uk_code") 来分辨
  20. // "产品码冲突" vs 其它唯一键冲突,文案随 MySQL 版本、驱动甚至索引重命名漂移,
  21. // 极易把真实冲突静默降级为通用 500;修复后统一返回 ErrConflict("数据冲突,请稍后重试"),
  22. // 由 pre-check 负责业务语义。本文件锚定"非特定文案也能兜到 409"。
  23. // ---------------------------------------------------------------------------
  24. // TC-0827: M-5 —— 事务内冒出 1062 错误(错误消息里不含 "uk_code" 字样)时,
  25. // 仍必须返回 409 通用冲突,而不是被旧的 strings.Contains 分支漏掉降级成 500。
  26. func TestCreateProduct_DuplicateEntry_UnknownIndexName_MapsTo409(t *testing.T) {
  27. ctrl := gomock.NewController(t)
  28. t.Cleanup(ctrl.Finish)
  29. // 关键:索引名选一个完全不含 "uk_code" 的,让旧 strings.Contains 分支必然 miss。
  30. dupErr := &mysql.MySQLError{
  31. Number: 1062,
  32. Message: "Duplicate entry 'abc' for key 'sys_product_PRIMARY'",
  33. }
  34. mockProduct := mocks.NewMockSysProductModel(ctrl)
  35. mockProduct.EXPECT().FindOneByCode(gomock.Any(), "m5_code").
  36. Return(nil, productModel.ErrNotFound)
  37. mockProduct.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  38. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  39. return fn(ctx, nil)
  40. })
  41. // 直接让 InsertWithTx 冒出 1062
  42. mockProduct.EXPECT().InsertWithTx(gomock.Any(), nil, gomock.Any()).
  43. Return(nil, dupErr)
  44. mockUser := mocks.NewMockSysUserModel(ctrl)
  45. mockUser.EXPECT().FindOneByUsername(gomock.Any(), "admin_m5_code").
  46. Return(nil, userModel.ErrNotFound)
  47. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  48. Product: mockProduct,
  49. User: mockUser,
  50. })
  51. resp, err := NewCreateProductLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateProduct(&types.CreateProductReq{
  52. Code: "m5_code",
  53. Name: "M5 Product",
  54. })
  55. assert.Nil(t, resp)
  56. require.Error(t, err)
  57. var ce *response.CodeError
  58. require.True(t, errors.As(err, &ce), "必须是结构化 CodeError")
  59. assert.Equal(t, 409, ce.Code(),
  60. "M-5:任何 1062 都应统一返回 409;修复前不含 uk_code 的索引名会被吞成 500")
  61. assert.Contains(t, ce.Error(), "数据冲突",
  62. "错误消息应当是通用的'数据冲突,请稍后重试',不再尝试解析索引名文案")
  63. }