六个 Update* 此前只有 UpdateRequiresID 覆盖到拒绝分支,方法体里的 s.repo.Update*(item) 从未被执行。这 12 个方法互为复制粘贴的模板, 尾行一旦误写成 Create* 全部测试仍然绿灯,生产行为会退化成「编辑即新增」。 新增两条用例:PartnerService.Update(事务:SELECT ... FOR UPDATE + 两条 UPDATE)与 MasterDataService.UpdateCategory(单条 UPDATE),均登记 UPDATE 期望并以 ExpectationsWereMet 兜底。把尾行改成 Create* 实测两条 用例都会失败并指名收到了 INSERT。 同时 gofmt internal/service/admin_master.go:新增中文注释紧贴前一个函数的 },补上缺失的空行,纯空白改动。
200 lines
7.0 KiB
Go
200 lines
7.0 KiB
Go
package service
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
"github.com/wmms/wmms/internal/model"
|
||
"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{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{PartnerCode: "SUP-001", LegalName: "伙伴", IsSupplier: true})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// 拆方法不能把校验丢了。
|
||
func TestPartnerCreateStillValidatesName(t *testing.T) {
|
||
err := NewPartnerService().Create(&model.BusinessPartner{PartnerCode: "SUP-001", IsSupplier: true})
|
||
if err == nil {
|
||
t.Fatal("legal_name 为空应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestPartnerCreateStillRequiresARole(t *testing.T) {
|
||
err := NewPartnerService().Create(&model.BusinessPartner{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{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{Code: "pcs", Name: "件"})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestCodeRuleUpdateRequiresID(t *testing.T) {
|
||
err := NewMasterDataService().UpdateCodeRule(&model.CodeRule{CategoryID: 1, Prefix: "A", SequenceLength: 4})
|
||
if err == nil {
|
||
t.Fatal("缺少 id 的更新应当被拒绝")
|
||
}
|
||
}
|
||
|
||
// 单位的小数位校验不能因为拆方法而丢失。
|
||
func TestUnitCreateStillValidatesPrecisionScale(t *testing.T) {
|
||
err := NewMasterDataService().CreateUnit(&model.Unit{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{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{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,
|
||
// 生产行为会退化成「编辑即新增」——与本次修复的缺陷同级。这里补上走通的路径。
|
||
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)
|
||
}
|
||
}
|