583 lines
22 KiB
Go
583 lines
22 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
"github.com/wmms/wmms/internal/model"
|
||
"github.com/wmms/wmms/internal/repository"
|
||
"github.com/wmms/wmms/pkg/database"
|
||
)
|
||
|
||
// 前端表单在「新增」时可能残留上次编辑过的 id(PartnerList.vue 的
|
||
// openCreate 只做 Object.assign、不删多余键)。创建必须无视这个 id,
|
||
// 否则会走成 UPDATE 把旧记录覆盖掉——这正是本次要修的缺陷。
|
||
func TestPartnerCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectExec("INSERT INTO business_partners").WillReturnResult(sqlmock.NewResult(2, 1))
|
||
mock.ExpectExec("INSERT INTO organization_partners").WillReturnResult(sqlmock.NewResult(2, 1))
|
||
mock.ExpectCommit()
|
||
|
||
item := &model.BusinessPartner{OrganizationID: 1, ID: 1, PartnerCode: "SUP-002", LegalName: "新伙伴", IsSupplier: true}
|
||
if err := NewPartnerService().Create(item); err != nil {
|
||
t.Fatalf("Create: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||
}
|
||
}
|
||
|
||
// 更新接口漏传 id 时必须报错,否则会静默写 0 号记录(什么都不改)而不自知。
|
||
func TestPartnerUpdateRequiresID(t *testing.T) {
|
||
err := NewPartnerService().Update(&model.BusinessPartner{OrganizationID: 1, PartnerCode: "SUP-001", LegalName: "伙伴", IsSupplier: true})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// 拆方法不能把校验丢了。
|
||
func TestPartnerCreateStillValidatesName(t *testing.T) {
|
||
err := NewPartnerService().Create(&model.BusinessPartner{OrganizationID: 1, PartnerCode: "SUP-001", IsSupplier: true})
|
||
if err == nil {
|
||
t.Fatal("legal_name 为空应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestPartnerCreateStillRequiresARole(t *testing.T) {
|
||
err := NewPartnerService().Create(&model.BusinessPartner{OrganizationID: 1, PartnerCode: "SUP-001", LegalName: "伙伴"})
|
||
if err == nil {
|
||
t.Fatal("既非供应商也非客户时应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// MasterDataSettings.vue 的 openCreate 同样是 Object.assign 复用表单,
|
||
// 分类新增也会带上前一次编辑残留的 id,必须走 INSERT。
|
||
func TestCategoryCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("INSERT INTO material_categories").WillReturnResult(sqlmock.NewResult(9, 1))
|
||
|
||
item := &model.MaterialCategory{OrganizationID: 1, ID: 1, Code: "A99", Name: "新分类"}
|
||
if err := NewMasterDataService().CreateCategory(item); err != nil {
|
||
t.Fatalf("CreateCategory: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestUnitUpdateRequiresID(t *testing.T) {
|
||
err := NewMasterDataService().UpdateUnit(&model.Unit{OrganizationID: 1, Code: "pcs", Name: "件"})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestCodeRuleUpdateRequiresID(t *testing.T) {
|
||
err := NewMasterDataService().UpdateCodeRule(&model.CodeRule{OrganizationID: 1, CategoryID: 1, Prefix: "A", SequenceLength: 4})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// 单位的小数位校验不能因为拆方法而丢失。
|
||
func TestUnitCreateStillValidatesPrecisionScale(t *testing.T) {
|
||
err := NewMasterDataService().CreateUnit(&model.Unit{OrganizationID: 1, Code: "kg", Name: "千克", PrecisionScale: 9})
|
||
if err == nil {
|
||
t.Fatal("precision_scale 超过 6 应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestWarehouseCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("INSERT INTO warehouses").WillReturnResult(sqlmock.NewResult(5, 1))
|
||
|
||
item := &model.Warehouse{OrganizationID: 1, ID: 3, Code: "WH2", Name: "二号仓"}
|
||
if err := NewWarehouseService().Create(item); err != nil {
|
||
t.Fatalf("Create: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWarehouseUpdateRequiresID(t *testing.T) {
|
||
err := NewWarehouseService().Update(&model.Warehouse{OrganizationID: 1, Code: "WH1", Name: "一号仓"})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestLocationCreateRequiresWarehouseID(t *testing.T) {
|
||
err := NewWarehouseService().CreateLocation(&model.WarehouseLocation{Code: "L1", Name: "库位一"}, 1)
|
||
if err == nil {
|
||
t.Fatal("缺少 warehouse_id 的库位创建应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// 上面的 UpdateRequiresID 只覆盖了拒绝分支,方法体里真正的 s.repo.Update 从未被执行。
|
||
// 六个 Update* 互为复制粘贴的模板,若某一处尾行被误写成 s.repo.Create,
|
||
// 生产行为会退化成「编辑即新增」——与本次修复的缺陷同级。这里补上走通的路径。
|
||
// Partner / Category / Unit / CodeRule / Warehouse / Location 六个现已全覆盖,
|
||
// 且逐个把尾行改成 Create* 验证过确实会被抓住,不是只跑绿。
|
||
func TestPartnerUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT business_partner_id FROM organization_partners").
|
||
WithArgs(int64(12), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"business_partner_id"}).AddRow(int64(7)))
|
||
mock.ExpectExec("UPDATE business_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectExec("UPDATE organization_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectCommit()
|
||
|
||
item := &model.BusinessPartner{
|
||
ID: 12,
|
||
OrganizationID: 1,
|
||
PartnerCode: "SUP-007",
|
||
LegalName: "伙伴改名",
|
||
IsSupplier: true,
|
||
}
|
||
if err := NewPartnerService().Update(item); err != nil {
|
||
t.Fatalf("Update: %v", err)
|
||
}
|
||
// 只登记了 SELECT ... FOR UPDATE 与两条 UPDATE。若尾行被误写成 Create,
|
||
// 事务里发出的 INSERT 与这里的期望不匹配:要么 Create 直接报错、要么
|
||
// 这两条 UPDATE 永不被消费,ExpectationsWereMet 必然失败。
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCategoryUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("UPDATE material_categories").
|
||
WithArgs("改名后的分类", "说明", nil, "active", int64(9), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.MaterialCategory{
|
||
ID: 9,
|
||
OrganizationID: 1,
|
||
Code: "A01",
|
||
Name: "改名后的分类",
|
||
Description: "说明",
|
||
}
|
||
if err := NewMasterDataService().UpdateCategory(item); err != nil {
|
||
t.Fatalf("UpdateCategory: %v", err)
|
||
}
|
||
// 这里只登记了 UPDATE;误写成 Create 时发出的 INSERT 不会被消费,
|
||
// Expectation 落空,ExpectationsWereMet 报错。
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是插入: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestUnitUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("UPDATE units").
|
||
WithArgs("件(改名)", int64(2), "active", int64(4), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.Unit{
|
||
ID: 4,
|
||
OrganizationID: 1,
|
||
Code: "pcs",
|
||
Name: "件(改名)",
|
||
PrecisionScale: 2,
|
||
}
|
||
if err := NewMasterDataService().UpdateUnit(item); err != nil {
|
||
t.Fatalf("UpdateUnit: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCodeRuleUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
// category_id 必须出现在参数里:SET 漏掉它正是本次修的缺陷,
|
||
// 这里用 WithArgs 钉住参数个数和顺序,漏了或写错位置都会失败。
|
||
// 组织隔离新增校验:编码规则引用的分类必须属于当前公司,先查一次分类。
|
||
mock.ExpectQuery("SELECT id, organization_id, code, name, description, parent_id, status FROM material_categories").
|
||
WithArgs(int64(2), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "description", "parent_id", "status"}).
|
||
AddRow(int64(2), int64(1), "A01", "小类", "", int64(1), "active"))
|
||
mock.ExpectExec("UPDATE code_rules").
|
||
WithArgs(int64(2), "B", int64(5), int64(7), "active", int64(3), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.CodeRule{
|
||
ID: 3,
|
||
OrganizationID: 1,
|
||
CategoryID: 2,
|
||
Prefix: "B",
|
||
SequenceLength: 5,
|
||
NextSequence: 7,
|
||
}
|
||
if err := NewMasterDataService().UpdateCodeRule(item); err != nil {
|
||
t.Fatalf("UpdateCodeRule: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWarehouseUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("UPDATE warehouses").
|
||
WithArgs("一号仓(改名)", "active", int64(5), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.Warehouse{
|
||
ID: 5,
|
||
OrganizationID: 1,
|
||
Code: "WH1",
|
||
Name: "一号仓(改名)",
|
||
}
|
||
if err := NewWarehouseService().Update(item); err != nil {
|
||
t.Fatalf("Update: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||
}
|
||
}
|
||
|
||
// 库位这条和上面三条不同:repository 的 UPDATE 是 JOIN warehouses 的写法,
|
||
// orgID 由方法参数传入、不在 item 上,SQL 模式写窄一点才能对上。
|
||
func TestLocationUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
|
||
mock.ExpectExec("UPDATE warehouse_locations").
|
||
WithArgs("库位一(改名)", "active", int64(8), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.WarehouseLocation{
|
||
ID: 8,
|
||
WarehouseID: 3,
|
||
Code: "L1",
|
||
Name: "库位一(改名)",
|
||
}
|
||
if err := NewWarehouseService().UpdateLocation(item, 1); err != nil {
|
||
t.Fatalf("UpdateLocation: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||
}
|
||
}
|
||
|
||
// 以下几组测试钉住「编码不可变」这条契约的两端:
|
||
// 更新侧——请求里不带 code 也必须能走通。旧实现复用 Create 的校验,要求 code
|
||
// 非空,而 repository 的 UPDATE 又不写这一列,形成「必须传、传了却被丢弃」。
|
||
// 新建侧——拆出 normalize* 时不能把 code 的必填校验一起丢了。
|
||
func newMasterDataMock(t *testing.T) sqlmock.Sqlmock {
|
||
t.Helper()
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("sqlmock: %v", err)
|
||
}
|
||
database.DB = db
|
||
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||
return mock
|
||
}
|
||
|
||
func TestCategoryUpdateAcceptsRequestWithoutCode(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectExec("UPDATE material_categories").
|
||
WithArgs("只改名称", "", nil, "active", int64(9), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.MaterialCategory{ID: 9, OrganizationID: 1, Name: "只改名称"}
|
||
if err := NewMasterDataService().UpdateCategory(item); err != nil {
|
||
t.Fatalf("UpdateCategory: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("不带 code 的更新应当照常执行 UPDATE: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestUnitUpdateAcceptsRequestWithoutCode(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectExec("UPDATE units").
|
||
WithArgs("件(改名)", int64(2), "active", int64(4), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.Unit{ID: 4, OrganizationID: 1, Name: "件(改名)", PrecisionScale: 2}
|
||
if err := NewMasterDataService().UpdateUnit(item); err != nil {
|
||
t.Fatalf("UpdateUnit: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("不带 code 的更新应当照常执行 UPDATE: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWarehouseUpdateAcceptsRequestWithoutCode(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectExec("UPDATE warehouses").
|
||
WithArgs("一号仓(改名)", "active", int64(5), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.Warehouse{ID: 5, OrganizationID: 1, Name: "一号仓(改名)"}
|
||
if err := NewWarehouseService().Update(item); err != nil {
|
||
t.Fatalf("Update: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("不带 code 的更新应当照常执行 UPDATE: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestLocationUpdateAcceptsRequestWithoutCode(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectExec("UPDATE warehouse_locations").
|
||
WithArgs("库位一(改名)", "active", int64(8), int64(1)).
|
||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||
|
||
item := &model.WarehouseLocation{ID: 8, WarehouseID: 3, Name: "库位一(改名)"}
|
||
if err := NewWarehouseService().UpdateLocation(item, 1); err != nil {
|
||
t.Fatalf("UpdateLocation: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("不带 code 的更新应当照常执行 UPDATE: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestPartnerUpdateAcceptsRequestWithoutPartnerCode(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT business_partner_id FROM organization_partners").
|
||
WithArgs(int64(12), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"business_partner_id"}).AddRow(int64(7)))
|
||
mock.ExpectExec("UPDATE business_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectExec("UPDATE organization_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectCommit()
|
||
|
||
item := &model.BusinessPartner{ID: 12, OrganizationID: 1, LegalName: "伙伴改名", IsSupplier: true}
|
||
if err := NewPartnerService().Update(item); err != nil {
|
||
t.Fatalf("Update: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("不带 partner_code 的更新应当照常执行: %v", err)
|
||
}
|
||
}
|
||
|
||
// 更新侧放开 code 之后,名称这类可改字段的校验不能跟着一起没了。
|
||
func TestUpdatesStillRequireName(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
run func() error
|
||
}{
|
||
{"category", func() error {
|
||
return NewMasterDataService().UpdateCategory(&model.MaterialCategory{OrganizationID: 1, ID: 9, Code: "A01"})
|
||
}},
|
||
{"unit", func() error {
|
||
return NewMasterDataService().UpdateUnit(&model.Unit{OrganizationID: 1, ID: 4, Code: "pcs"})
|
||
}},
|
||
{"warehouse", func() error {
|
||
return NewWarehouseService().Update(&model.Warehouse{OrganizationID: 1, ID: 5, Code: "WH1"})
|
||
}},
|
||
{"location", func() error {
|
||
return NewWarehouseService().UpdateLocation(&model.WarehouseLocation{ID: 8, WarehouseID: 3, Code: "L1"}, 1)
|
||
}},
|
||
{"partner", func() error {
|
||
return NewPartnerService().Update(&model.BusinessPartner{OrganizationID: 1, ID: 12, PartnerCode: "SUP-007", IsSupplier: true})
|
||
}},
|
||
}
|
||
for _, c := range cases {
|
||
if err := c.run(); err == nil {
|
||
t.Errorf("%s: 名称为空的更新应当被拒绝", c.name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 新建侧仍然要求 code:拆方法不能把必填校验弄丢。
|
||
func TestCreatesStillRequireCode(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
run func() error
|
||
}{
|
||
{"category", func() error {
|
||
return NewMasterDataService().CreateCategory(&model.MaterialCategory{OrganizationID: 1, Name: "新分类"})
|
||
}},
|
||
{"unit", func() error {
|
||
return NewMasterDataService().CreateUnit(&model.Unit{OrganizationID: 1, Name: "件"})
|
||
}},
|
||
{"warehouse", func() error {
|
||
return NewWarehouseService().Create(&model.Warehouse{OrganizationID: 1, Name: "二号仓"})
|
||
}},
|
||
{"location", func() error {
|
||
return NewWarehouseService().CreateLocation(&model.WarehouseLocation{WarehouseID: 3, Name: "库位一"}, 1)
|
||
}},
|
||
{"partner", func() error {
|
||
return NewPartnerService().Create(&model.BusinessPartner{OrganizationID: 1, LegalName: "新伙伴", IsSupplier: true})
|
||
}},
|
||
}
|
||
for _, c := range cases {
|
||
if err := c.run(); err == nil {
|
||
t.Errorf("%s: 缺少 code 的新建应当被拒绝", c.name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 修好 category_id 之后,把规则改到已有规则的分类上会撞
|
||
// uk_code_rules_org_category。此前 SET 不写这一列,永远撞不上;
|
||
// 现在必须把这冲突翻成中文提示,否则前端只会显示「请求失败」。
|
||
func TestCodeRuleUpdateReportsDuplicateCategory(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
// 组织隔离新增校验:先确认引用的分类属于当前公司。
|
||
mock.ExpectQuery("SELECT id, organization_id, code, name, description, parent_id, status FROM material_categories").
|
||
WithArgs(int64(2), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "description", "parent_id", "status"}).
|
||
AddRow(int64(2), int64(1), "A01", "小类", "", int64(1), "active"))
|
||
mock.ExpectExec("UPDATE code_rules").
|
||
WillReturnError(errors.New("Error 1062: Duplicate entry '1-2' for key 'uk_code_rules_org_category'"))
|
||
|
||
item := &model.CodeRule{ID: 3, OrganizationID: 1, CategoryID: 2, Prefix: "B", SequenceLength: 4, NextSequence: 1}
|
||
err := NewMasterDataService().UpdateCodeRule(item)
|
||
if !errors.Is(err, repository.ErrDuplicateCodeRule) {
|
||
t.Fatalf("撞唯一键应当翻成 ErrDuplicateCodeRule,实际 %v", err)
|
||
}
|
||
// 前端 index.js 只把中文消息照原样弹出,英文一律显示"请求失败"。
|
||
if !strings.Contains(err.Error(), "已有编码规则") {
|
||
t.Fatalf("错误消息应当是中文,实际 %q", err.Error())
|
||
}
|
||
}
|
||
|
||
func TestCodeRuleCreateReportsDuplicateCategory(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
// 组织隔离新增校验:先确认引用的分类属于当前公司。
|
||
mock.ExpectQuery("SELECT id, organization_id, code, name, description, parent_id, status FROM material_categories").
|
||
WithArgs(int64(2), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "description", "parent_id", "status"}).
|
||
AddRow(int64(2), int64(1), "A01", "小类", "", int64(1), "active"))
|
||
mock.ExpectExec("INSERT INTO code_rules").
|
||
WillReturnError(errors.New("Error 1062: Duplicate entry '1-2' for key 'uk_code_rules_org_category'"))
|
||
|
||
item := &model.CodeRule{OrganizationID: 1, CategoryID: 2, Prefix: "B", SequenceLength: 4, NextSequence: 1}
|
||
if err := NewMasterDataService().CreateCodeRule(item); !errors.Is(err, repository.ErrDuplicateCodeRule) {
|
||
t.Fatalf("新建撞唯一键也应当翻成 ErrDuplicateCodeRule,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
// 记录不存在、或者存在但不属于当前组织时,UPDATE 影响 0 行。此前一律返回 nil,
|
||
// 接口提示"保存成功"而实际什么都没改。
|
||
//
|
||
// 这条检查依赖 DSN 的 clientFoundRows=true:不开的话「编辑弹窗里一个字段都没动」
|
||
// 同样是 0 行,正常的保存会被误判成记录不存在。开关的实测数据见
|
||
// pkg/database/database.go 里 normalizeDSN 的注释。
|
||
func TestUpdatesReportMissingRecord(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
sql string
|
||
prep func(mock sqlmock.Sqlmock)
|
||
run func() error
|
||
}{
|
||
{"category", "UPDATE material_categories", nil, func() error {
|
||
return NewMasterDataService().UpdateCategory(&model.MaterialCategory{ID: 404, OrganizationID: 1, Name: "改名"})
|
||
}},
|
||
{"unit", "UPDATE units", nil, func() error {
|
||
return NewMasterDataService().UpdateUnit(&model.Unit{ID: 404, OrganizationID: 1, Name: "件"})
|
||
}},
|
||
{"code rule", "UPDATE code_rules", func(mock sqlmock.Sqlmock) {
|
||
// 编码规则先校验引用的分类属于当前公司,再执行 UPDATE。
|
||
mock.ExpectQuery("SELECT id, organization_id, code, name, description, parent_id, status FROM material_categories").
|
||
WithArgs(int64(2), int64(1)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "description", "parent_id", "status"}).
|
||
AddRow(int64(2), int64(1), "A01", "小类", "", int64(1), "active"))
|
||
}, func() error {
|
||
return NewMasterDataService().UpdateCodeRule(&model.CodeRule{
|
||
ID: 404, OrganizationID: 1, CategoryID: 2, Prefix: "B", SequenceLength: 4, NextSequence: 1,
|
||
})
|
||
}},
|
||
{"warehouse", "UPDATE warehouses", nil, func() error {
|
||
return NewWarehouseService().Update(&model.Warehouse{ID: 404, OrganizationID: 1, Name: "一号仓"})
|
||
}},
|
||
{"location", "UPDATE warehouse_locations", nil, func() error {
|
||
return NewWarehouseService().UpdateLocation(&model.WarehouseLocation{ID: 404, WarehouseID: 3, Name: "库位一"}, 1)
|
||
}},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
if c.prep != nil {
|
||
c.prep(mock)
|
||
}
|
||
// 第二个参数是受影响行数:0 行代表 id 不存在或组织对不上。
|
||
mock.ExpectExec(c.sql).WillReturnResult(sqlmock.NewResult(0, 0))
|
||
if err := c.run(); !errors.Is(err, repository.ErrRecordNotFound) {
|
||
t.Fatalf("影响 0 行应当返回 ErrRecordNotFound,实际 %v", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 伙伴走的是另一条路:事务里先 SELECT ... FOR UPDATE,查不到就已经能判定不存在。
|
||
// 不翻译的话会以 sql.ErrNoRows 冒到接口,变成 500 加一句英文。
|
||
func TestPartnerUpdateReportsMissingRecord(t *testing.T) {
|
||
mock := newMasterDataMock(t)
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery("SELECT business_partner_id FROM organization_partners").
|
||
WillReturnRows(sqlmock.NewRows([]string{"business_partner_id"}))
|
||
mock.ExpectRollback()
|
||
|
||
item := &model.BusinessPartner{ID: 404, OrganizationID: 1, LegalName: "不存在的伙伴", IsSupplier: true}
|
||
if err := NewPartnerService().Update(item); !errors.Is(err, repository.ErrRecordNotFound) {
|
||
t.Fatalf("查不到伙伴应当返回 ErrRecordNotFound,实际 %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("查不到时应当回滚事务: %v", err)
|
||
}
|
||
}
|