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:新增中文注释紧贴前一个函数的
},补上缺失的空行,纯空白改动。
This commit is contained in:
文睿 张 2026-09-21 15:34:41 +08:00
parent f3124e96cc
commit 72bc6c9af2
2 changed files with 71 additions and 0 deletions

View File

@ -68,6 +68,7 @@ func NewMasterDataService() *MasterDataService {
func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) {
return s.repo.ListCategories(orgID)
}
// CreateCategory 新建分类。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
func (s *MasterDataService) CreateCategory(item *model.MaterialCategory) error {
@ -104,6 +105,7 @@ func prepareCategory(item *model.MaterialCategory) error {
func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) {
return s.repo.ListUnits(orgID)
}
// CreateUnit 新建单位。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
func (s *MasterDataService) CreateUnit(item *model.Unit) error {
if err := prepareUnit(item); err != nil {
@ -154,6 +156,7 @@ func (s *MasterDataService) NextMaterialCode(orgID, categoryID int64) (*model.Ne
}
return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil
}
// CreateCodeRule 新建编码规则。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
func (s *MasterDataService) CreateCodeRule(item *model.CodeRule) error {
if err := prepareCodeRule(item); err != nil {
@ -202,6 +205,7 @@ func (s *PartnerService) List(orgID int64, role string) ([]model.BusinessPartner
}
return s.repo.List(orgID, role)
}
// Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
func (s *PartnerService) Create(item *model.BusinessPartner) error {
@ -251,6 +255,7 @@ func NewWarehouseService() *WarehouseService {
func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) {
return s.repo.List(orgID)
}
// Create 新建仓库。不读取 item.ID,脏 id 不会把新建变成覆盖。
func (s *WarehouseService) Create(item *model.Warehouse) error {
if err := prepareWarehouse(item); err != nil {

View File

@ -131,3 +131,69 @@ func TestLocationCreateRequiresWarehouseID(t *testing.T) {
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)
}
}