createProductConflict_audit_test.go 3.3 KB

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