202 lines
7.7 KiB
Go
202 lines
7.7 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, 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, int64(12), nil, "个", "active", 1,
|
||
int64(1), "A010203", "螺母", 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)
|
||
}
|
||
}
|