material_manage_system/internal/service/material_import_excel_test.go

225 lines
7.1 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 (
"archive/zip"
"bytes"
"errors"
"io"
"testing"
"github.com/xuri/excelize/v2"
)
func TestParseMaterialImportFileStopsBeforeRowsBeyondLimit(t *testing.T) {
dataRows := make([][]string, MaterialImportMaxRows+1)
for i := range dataRows {
dataRows[i] = []string{"物料", "", "", "机械结构类", "标准件"}
}
data := buildTestWorkbook(t, dataRows)
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Fatal(err)
}
var changed bytes.Buffer
writer := zip.NewWriter(&changed)
for _, entry := range reader.File {
source, err := entry.Open()
if err != nil {
t.Fatal(err)
}
content, err := io.ReadAll(source)
source.Close()
if err != nil {
t.Fatal(err)
}
if entry.Name == "xl/worksheets/sheet1.xml" {
// 超限行之后故意放非法 XML,证明解析器不会再读取后续行。
content = bytes.Replace(content, []byte("</sheetData>"),
[]byte(`<row r="20003"><c></row></sheetData>`), 1)
}
target, err := writer.Create(entry.Name)
if err != nil {
t.Fatal(err)
}
if _, err := target.Write(content); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
rows, err := ParseMaterialImportFile(bytes.NewReader(changed.Bytes()))
if !errors.Is(err, ErrImportTooManyRows) || rows != nil {
t.Fatalf("rows=%d, err=%v, want immediate ErrImportTooManyRows", len(rows), err)
}
}
func TestParseMaterialImportFileAcceptsRowLimit(t *testing.T) {
dataRows := make([][]string, MaterialImportMaxRows)
for i := range dataRows {
dataRows[i] = []string{"物料", "", "", "机械结构类", "标准件"}
}
rows, err := ParseMaterialImportFile(bytes.NewReader(buildTestWorkbook(t, dataRows)))
if err != nil || len(rows) != MaterialImportMaxRows {
t.Fatalf("rows=%d, err=%v", len(rows), err)
}
}
func TestParseMaterialImportFileReadsRowsWithExcelRowNumbers(t *testing.T) {
data := buildTestWorkbook(t, [][]string{
{"内六角螺栓", "M8x30", "个", "机械结构类", "标准件"},
{"", "", "", "", ""}, // 整行空,应被跳过
{"急停按钮", "XB2", "个", "电气元器件类", "成品件"},
})
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(rows) != 2 {
t.Fatalf("rows = %d, want 2", len(rows))
}
if rows[0].Row != 2 || rows[0].Name != "内六角螺栓" || rows[0].MinorName != "标准件" {
t.Fatalf("first row = %+v", rows[0])
}
// 第 3 行是空行被跳过,所以第二条数据是 Excel 第 4 行
if rows[1].Row != 4 || rows[1].Name != "急停按钮" {
t.Fatalf("second row = %+v", rows[1])
}
}
func TestParseMaterialImportFileKeepsRowsWithAnyContent(t *testing.T) {
// 名称填了、其余为空:不是空行,必须保留交给校验去报错
data := buildTestWorkbook(t, [][]string{
{"只有名称", "", "", "", ""},
})
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(rows) != 1 || rows[0].Name != "只有名称" {
t.Fatalf("rows = %+v", rows)
}
}
func TestParseMaterialImportFileRejectsMissingRequiredColumn(t *testing.T) {
// 只有「物料名称」和「规格」,缺必填的大类、小类
data := buildTestWorkbookWithHeaders(t, []string{"物料名称", "规格"}, [][]string{
{"内六角螺栓", "M8x30"},
})
_, err := ParseMaterialImportFile(bytes.NewReader(data))
if !errors.Is(err, ErrImportTemplateInvalid) {
t.Fatalf("err = %v, want ErrImportTemplateInvalid", err)
}
}
func TestParseMaterialImportFileAcceptsMissingOptionalColumnsAndReorderedColumns(t *testing.T) {
// 删掉「规格」,并把「单位」挪到「物料名称」前面
headers := []string{"单位", "物料名称", "大类", "小类"}
data := buildTestWorkbookWithHeaders(t, headers, [][]string{
{"个", "内六角螺栓", "机械结构类", "标准件"},
})
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(rows) != 1 {
t.Fatalf("rows = %d, want 1", len(rows))
}
if rows[0].Name != "内六角螺栓" || rows[0].Unit != "个" || rows[0].MinorName != "标准件" {
t.Fatalf("row = %+v", rows[0])
}
if rows[0].Spec != "" {
t.Fatalf("缺失的规格列应解析为空,得到 %q", rows[0].Spec)
}
}
func TestParseMaterialImportFileIgnoresUnknownColumns(t *testing.T) {
headers := []string{"物料名称", "备注", "大类", "小类"}
data := buildTestWorkbookWithHeaders(t, headers, [][]string{
{"内六角螺栓", "与导入无关", "机械结构类", "标准件"},
})
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(rows) != 1 || rows[0].Name != "内六角螺栓" {
t.Fatalf("rows = %+v", rows)
}
}
func TestParseMaterialImportFileRejectsTooManyRows(t *testing.T) {
rows := make([][]string, 0, MaterialImportMaxRows+1)
for i := 0; i < MaterialImportMaxRows+1; i++ {
rows = append(rows, []string{"物料", "", "", "机械结构类", "标准件"})
}
data := buildTestWorkbook(t, rows)
_, err := ParseMaterialImportFile(bytes.NewReader(data))
if !errors.Is(err, ErrImportTooManyRows) {
t.Fatalf("err = %v, want ErrImportTooManyRows", err)
}
}
func TestBuildMaterialImportTemplateRoundTrips(t *testing.T) {
data, err := BuildMaterialImportTemplate()
if err != nil {
t.Fatalf("build template: %v", err)
}
f, err := excelize.OpenReader(bytes.NewReader(data))
if err != nil {
t.Fatalf("open template: %v", err)
}
defer f.Close()
if idx, _ := f.GetSheetIndex(MaterialImportSheetData); idx < 0 {
t.Fatal("模板缺少「物料数据」工作表")
}
if idx, _ := f.GetSheetIndex(MaterialImportSheetGuide); idx < 0 {
t.Fatal("模板缺少「填写说明」工作表")
}
headers, err := f.GetRows(MaterialImportSheetData)
if err != nil {
t.Fatalf("read rows: %v", err)
}
if len(headers) == 0 || headers[0][0] != "物料名称" || headers[0][4] != "小类" {
t.Fatalf("表头 = %v", headers)
}
// 模板必须能被自己的解析器接受(表头一致)
if _, err := ParseMaterialImportFile(bytes.NewReader(data)); err != nil {
t.Fatalf("模板无法被解析器接受: %v", err)
}
}
// buildTestWorkbook 用标准表头 + 给定数据行构造一个 xlsx 字节流。
func buildTestWorkbook(t *testing.T, dataRows [][]string) []byte {
t.Helper()
return buildTestWorkbookWithHeaders(t, materialImportHeaders, dataRows)
}
// buildTestWorkbookWithHeaders 允许指定表头,用来覆盖缺列、乱序、多余列的解析分支。
func buildTestWorkbookWithHeaders(t *testing.T, headers []string, dataRows [][]string) []byte {
t.Helper()
f := excelize.NewFile()
defer f.Close()
f.SetSheetName("Sheet1", MaterialImportSheetData)
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(MaterialImportSheetData, cell, header)
}
for r, row := range dataRows {
for c, value := range row {
cell, _ := excelize.CoordinatesToCellName(c+1, r+2)
f.SetCellValue(MaterialImportSheetData, cell, value)
}
}
buf, err := f.WriteToBuffer()
if err != nil {
t.Fatalf("write workbook: %v", err)
}
return buf.Bytes()
}