go test ./... 从 21 个失败回到 0。根因都是测试没跟上代码改动,其中两处
被「自动补位」的补丁 hack 掩盖住了:
- handler 的 materialRow 在「列数 20、值 18」时自动插两个 nil,掩盖了
materials 新增 supplier_id/manufacturer_id 后的列错位。它碰巧补对位置,
而 order 测试的 activeMaterialRows 没走这个 helper,直接 panic。
已删掉补丁、把测试数据补齐到 20 列。
- 导入测试的 buildTestWorkbookWithHeaders 按 7 列给数据补位,而表头这时
已经扩到 9 列(多了供应商、制造商),把「大类/小类」静默塞进了
「来源/备注」。已按 materialImportHeaders 的实际列序修正补位。
其余为非补丁式的期望过时:materials 的 INSERT 参数补到 15 个、物料列表
查询结果集补到 20 列、导入模板表头断言改为按 materialImportHeaders 取末列
而不是写死下标。
顺带清掉已废弃的升级路径残留:upgrades/ 目录在 85525c97 已删除,增量升级
不再使用,因此删除 scripts/sync-company-integrity.ps1(把 checks SQL 嵌入
upgrade SQL 的生成器)与它的测试,application.go 里指向 upgrades 的错误
文案改为只提 init.sql。
202 lines
7.8 KiB
Go
202 lines
7.8 KiB
Go
package repository
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
"github.com/wmms/wmms/internal/model"
|
||
"github.com/wmms/wmms/pkg/database"
|
||
)
|
||
|
||
const importLockRuleQuery = `SELECT id, organization_id, category_id, prefix, sequence_length, next_sequence FROM code_rules WHERE organization_id = ? AND category_id = ? AND status = 'active' FOR UPDATE`
|
||
|
||
const insertMaterialBatchStmt = `INSERT INTO materials (organization_id, material_code, name, material_image, spec, manufacturer_model, source, remark, supplier_id, manufacturer_id, category_id, base_unit_id, unit, status, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||
|
||
func newImportMock(t *testing.T) (sqlmock.Sqlmock, func()) {
|
||
t.Helper()
|
||
db, mock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("open sql mock: %v", err)
|
||
}
|
||
previous := database.DB
|
||
database.DB = db
|
||
return mock, func() {
|
||
database.DB = previous
|
||
db.Close()
|
||
}
|
||
}
|
||
|
||
func materialCategoryPtr(id int64) *int64 { return &id }
|
||
|
||
func TestMaterialRepoImportTxAllocatesCodesAndInsertsOneByOne(t *testing.T) {
|
||
mock, cleanup := newImportMock(t)
|
||
defer cleanup()
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(regexp.QuoteMeta(importLockRuleQuery)).WithArgs(int64(1), int64(12)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "category_id", "prefix", "sequence_length", "next_sequence"}).
|
||
AddRow(5, 1, 12, "A01", 4, 201))
|
||
// 已占用编码探测:A010201 已被手工录入占用,应跳到 A010202
|
||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT material_code FROM materials WHERE organization_id = ? AND material_code LIKE ?`)).
|
||
WithArgs(int64(1), "A01%").
|
||
WillReturnRows(sqlmock.NewRows([]string{"material_code"}).AddRow("A010201"))
|
||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE code_rules SET next_sequence = ? WHERE id = ?`)).
|
||
WithArgs(int64(204), int64(5)).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
// 两行折叠成一条多值 INSERT
|
||
mock.ExpectExec(regexp.QuoteMeta(insertMaterialBatchStmt)).
|
||
WithArgs(
|
||
int64(1), "A010202", "螺栓", nil, "", nil, nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||
int64(1), "A010203", "螺母", nil, "", nil, nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||
).
|
||
WillReturnResult(sqlmock.NewResult(31, 2))
|
||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, material_code FROM materials WHERE organization_id = ? AND material_code IN (?,?)`)).
|
||
WithArgs(int64(1), "A010202", "A010203").
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "material_code"}).
|
||
AddRow(31, "A010202").AddRow(32, "A010203"))
|
||
|
||
tx, err := database.DB.Begin()
|
||
if err != nil {
|
||
t.Fatalf("begin: %v", err)
|
||
}
|
||
categoryID := int64(12)
|
||
materials := []*model.Material{
|
||
{OrganizationID: 1, Name: "螺栓", CategoryID: &categoryID, Unit: "个", Status: "active", Version: 1},
|
||
{OrganizationID: 1, Name: "螺母", CategoryID: &categoryID, Unit: "个", Status: "active", Version: 1},
|
||
}
|
||
if err := NewMaterialRepo().ImportTx(tx, 1, materials); err != nil {
|
||
t.Fatalf("import: %v", err)
|
||
}
|
||
if materials[0].MaterialCode != "A010202" || materials[0].ID != 31 {
|
||
t.Fatalf("first = %+v", materials[0])
|
||
}
|
||
if materials[1].MaterialCode != "A010203" || materials[1].ID != 32 {
|
||
t.Fatalf("second = %+v", materials[1])
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialRepoImportTxReportsMissingCodeRule(t *testing.T) {
|
||
mock, cleanup := newImportMock(t)
|
||
defer cleanup()
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(regexp.QuoteMeta(importLockRuleQuery)).WithArgs(int64(1), int64(12)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "category_id", "prefix", "sequence_length", "next_sequence"}))
|
||
|
||
tx, err := database.DB.Begin()
|
||
if err != nil {
|
||
t.Fatalf("begin: %v", err)
|
||
}
|
||
materials := []*model.Material{
|
||
{OrganizationID: 1, Name: "螺栓", CategoryID: materialCategoryPtr(12), Unit: "个", Status: "active", Version: 1},
|
||
}
|
||
err = NewMaterialRepo().ImportTx(tx, 1, materials)
|
||
if err == nil || !strings.Contains(err.Error(), "编码规则") {
|
||
t.Fatalf("err = %v, want 编码规则错误", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialRepoImportTxBatchesInsertsOverFiveHundredRows(t *testing.T) {
|
||
// 501 行 → 两批插入(500 + 1)。插入语句与回查参数很长,这里用子串松匹配,
|
||
// 只断言"分了两批"以及编码序号按批连续。
|
||
mock, cleanup := newImportMock(t)
|
||
defer cleanup()
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectQuery(regexp.QuoteMeta(importLockRuleQuery)).WithArgs(int64(1), int64(12)).
|
||
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "category_id", "prefix", "sequence_length", "next_sequence"}).
|
||
AddRow(5, 1, 12, "A01", 4, 1))
|
||
mock.ExpectQuery(`SELECT material_code FROM materials`).
|
||
WillReturnRows(sqlmock.NewRows([]string{"material_code"}))
|
||
mock.ExpectExec(`UPDATE code_rules SET next_sequence`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||
mock.ExpectExec(`INSERT INTO materials`).WillReturnResult(sqlmock.NewResult(1000, 500))
|
||
mock.ExpectQuery(`SELECT id, material_code FROM materials`).WillReturnRows(idRowsFor(1, 500))
|
||
mock.ExpectExec(`INSERT INTO materials`).WillReturnResult(sqlmock.NewResult(1500, 1))
|
||
mock.ExpectQuery(`SELECT id, material_code FROM materials`).WillReturnRows(idRowsFor(501, 501))
|
||
|
||
tx, err := database.DB.Begin()
|
||
if err != nil {
|
||
t.Fatalf("begin: %v", err)
|
||
}
|
||
categoryID := int64(12)
|
||
materials := make([]*model.Material, 0, 501)
|
||
for i := 0; i < 501; i++ {
|
||
materials = append(materials, &model.Material{
|
||
OrganizationID: 1, Name: "物料", CategoryID: &categoryID, Unit: "个", Status: "active", Version: 1,
|
||
})
|
||
}
|
||
if err := NewMaterialRepo().ImportTx(tx, 1, materials); err != nil {
|
||
t.Fatalf("import: %v", err)
|
||
}
|
||
if materials[0].MaterialCode != "A010001" || materials[500].MaterialCode != "A010501" {
|
||
t.Fatalf("codes = %s ... %s", materials[0].MaterialCode, materials[500].MaterialCode)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// idRowsFor 造出回查语句的返回行:序号 [from, to] 对应的物料 ID 与编码。
|
||
func idRowsFor(from, to int) *sqlmock.Rows {
|
||
rows := sqlmock.NewRows([]string{"id", "material_code"})
|
||
for i := from; i <= to; i++ {
|
||
rows.AddRow(int64(i), fmt.Sprintf("A01%04d", i))
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func TestMaterialLogRepoCreateBatchTx(t *testing.T) {
|
||
mock, cleanup := newImportMock(t)
|
||
defer cleanup()
|
||
|
||
mock.ExpectBegin()
|
||
mock.ExpectExec(`INSERT INTO material_logs`).WillReturnResult(sqlmock.NewResult(1, 2))
|
||
|
||
tx, err := database.DB.Begin()
|
||
if err != nil {
|
||
t.Fatalf("begin: %v", err)
|
||
}
|
||
logs := []*model.MaterialLog{
|
||
{OperationType: "CREATE", MaterialID: 31, Operator: "tester", Description: "批量导入"},
|
||
{OperationType: "CREATE", MaterialID: 32, Operator: "tester", Description: "批量导入"},
|
||
}
|
||
if err := NewMaterialLogRepo().CreateBatchTx(tx, logs); err != nil {
|
||
t.Fatalf("create batch: %v", err)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialImportLogRepoCreate(t *testing.T) {
|
||
mock, cleanup := newImportMock(t)
|
||
defer cleanup()
|
||
|
||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO material_import_logs (organization_id, operator, file_name, total_rows, success_rows, failed_rows, status, failure_reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)).
|
||
WithArgs(int64(1), "tester", "物料.xlsx", 200, 0, 3, "failed", "共 3 行校验未通过").
|
||
WillReturnResult(sqlmock.NewResult(9, 1))
|
||
|
||
log := &model.MaterialImportLog{
|
||
OrganizationID: 1, Operator: "tester", FileName: "物料.xlsx",
|
||
TotalRows: 200, SuccessRows: 0, FailedRows: 3,
|
||
Status: "failed", FailureReason: "共 3 行校验未通过",
|
||
}
|
||
if err := NewMaterialImportLogRepo().Create(log); err != nil {
|
||
t.Fatalf("create: %v", err)
|
||
}
|
||
if log.ID != 9 {
|
||
t.Fatalf("id = %d, want 9", log.ID)
|
||
}
|
||
if err := mock.ExpectationsWereMet(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|