material_manage_system/internal/service/admin_master_test.go
文睿 张 72bc6c9af2 test: 补齐 Update 的 happy-path 用例,锁定「编辑走 UPDATE」而非 INSERT
六个 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:新增中文注释紧贴前一个函数的
},补上缺失的空行,纯空白改动。
2026-09-21 15:34:41 +08:00

200 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}