package product import ( "context" "errors" "testing" deptModel "perms-system-server/internal/model/dept" productModel "perms-system-server/internal/model/product" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/testutil/ctxhelper" "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" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-5 修复 —— 旧实现用 strings.Contains(err, "uk_code") 来分辨 // "产品码冲突" vs 其它唯一键冲突,文案随 MySQL 版本、驱动甚至索引重命名漂移, // 极易把真实冲突静默降级为通用 500;修复后统一返回 ErrConflict("数据冲突,请稍后重试"), // 由 pre-check 负责业务语义。本文件锚定"非特定文案也能兜到 409"。 // --------------------------------------------------------------------------- // TC-0827: M-5 —— 事务内冒出 1062 错误(错误消息里不含 "uk_code" 字样)时, // 仍必须返回 409 通用冲突,而不是被旧的 strings.Contains 分支漏掉降级成 500。 func TestCreateProduct_DuplicateEntry_UnknownIndexName_MapsTo409(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) // 关键:索引名选一个完全不含 "uk_code" 的,让旧 strings.Contains 分支必然 miss。 dupErr := &mysql.MySQLError{ Number: 1062, Message: "Duplicate entry 'abc' for key 'sys_product_PRIMARY'", } mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByCode(gomock.Any(), "m5_code"). Return(nil, productModel.ErrNotFound) mockProduct.EXPECT().TransactCtx(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { return fn(ctx, nil) }) // 直接让 InsertWithTx 冒出 1062 mockProduct.EXPECT().InsertWithTx(gomock.Any(), nil, gomock.Any()). Return(nil, dupErr) mockUser := mocks.NewMockSysUserModel(ctrl) mockUser.EXPECT().FindOneByUsername(gomock.Any(), "admin_m5_code"). Return(nil, userModel.ErrNotFound) // 审计 L-R10-1:CreateProduct 现在必填 AdminDeptId,且在入库前 FindOne + 校验启用状态 mockDept := mocks.NewMockSysDeptModel(ctrl) mockDept.EXPECT().FindOne(gomock.Any(), int64(77)). Return(&deptModel.SysDept{Id: 77, Path: "/77/", Status: 1}, nil) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{ Product: mockProduct, User: mockUser, Dept: mockDept, }) resp, err := NewCreateProductLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateProduct(&types.CreateProductReq{ Code: "m5_code", Name: "M5 Product", AdminDeptId: 77, }) assert.Nil(t, resp) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce), "必须是结构化 CodeError") assert.Equal(t, 409, ce.Code(), "M-5:任何 1062 都应统一返回 409;修复前不含 uk_code 的索引名会被吞成 500") assert.Contains(t, ce.Error(), "数据冲突", "错误消息应当是通用的'数据冲突,请稍后重试',不再尝试解析索引名文案") }