225 lines
7.1 KiB
Go
225 lines
7.1 KiB
Go
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()
|
||
}
|