Files
iotlcm/internal/service/batchresult/generator_test.go
T
2026-10-29 14:52:43 +08:00

248 lines
8.4 KiB
Go

package batchresult
import (
"context"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"sync"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/xuri/excelize/v2"
)
func TestWriteDataServiceWorkbookPreservesReviewStatusAndReason(t *testing.T) {
for _, batchType := range []int{consts.BatchTypeDataCut, consts.BatchTypeDataStart, consts.BatchTypeDataStop} {
t.Run(TaskName(batchType), func(t *testing.T) {
path := filepath.Join(t.TempDir(), "result.xlsx")
rows := []Row{
{RowNo: 1, ICCID: "test-success", DetailStatus: consts.BatchDetailStatusSuccess, Success: true},
{RowNo: 2, ICCID: "test-failed", DetailStatus: consts.BatchDetailStatusFailed, Reason: "上游明确拒绝"},
{RowNo: 3, ICCID: "test-unknown", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "查询超时,尚未确认上游执行结果", UpstreamCode: 202, OrderNum: "test-job-id"},
{RowNo: 4, ICCID: "test-review-priority", DetailStatus: consts.BatchDetailStatusUnknown, Success: true, Reason: "明细仍待核查"},
{RowNo: 5, ICCID: "test-legacy-success", Success: true},
{RowNo: 6, ICCID: "test-legacy-failed", Reason: "未受理"},
}
if err := writeWorkbook(path, Request{BatchType: batchType, Rows: rows}); err != nil {
t.Fatalf("写入数据服务结果失败:%v", err)
}
file, err := excelize.OpenFile(path)
if err != nil {
t.Fatalf("打开数据服务结果失败:%v", err)
}
defer func() { _ = file.Close() }()
for cell, want := range map[string]string{
"G1": "执行结果", "H1": "失败/待核查原因",
"G2": "成功", "G3": "失败", "H3": "上游明确拒绝",
"G4": "待核查", "H4": rows[2].Reason, "I4": "202", "J4": "test-job-id",
"G5": "待核查", "H5": rows[3].Reason,
"G6": "成功", "G7": "失败", "H7": "未受理",
} {
got, err := file.GetCellValue("处理结果", cell)
if err != nil || got != want {
t.Fatalf("单元格 %s = %q,预期 %q,错误:%v", cell, got, want, err)
}
}
})
}
}
func TestCreateSameBatchUsesIndependentResultFiles(t *testing.T) {
// Keep the generated workbook path below Excel's 207-character limit on Windows.
root, rootErr := os.MkdirTemp("", "batch-result-")
if rootErr != nil {
t.Fatal(rootErr)
}
t.Cleanup(func() {
if err := os.RemoveAll(root); err != nil {
t.Errorf("remove test result directory: %v", err)
}
})
t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root)
oldAdapter := g.Cfg().GetAdapter()
adapter, err := gcfg.NewAdapterContent(`
system:
upload:
local:
path: "resource/upload"
`)
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) })
request := Request{BatchNo: "DATA-REVIEW", BatchType: consts.BatchTypeDataStart, TenantID: "tenant", Rows: []Row{{RowNo: 1, ICCID: "first-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}}}
requests := []Request{request, request}
requests[1].Rows = []Row{{RowNo: 1, ICCID: "second-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}}
artifacts := make([]Artifact, 2)
errors := make([]error, 2)
var workers sync.WaitGroup
for index := range requests {
workers.Add(1)
go func(index int) {
defer workers.Done()
artifacts[index], errors[index] = New().Create(context.Background(), requests[index])
}(index)
}
workers.Wait()
for _, err := range errors {
if err != nil {
t.Fatal(err)
}
}
first, second := artifacts[0], artifacts[1]
defer RemoveLocalFile(first)
defer RemoveLocalFile(second)
if first.Path == second.Path || first.LocalPath == second.LocalPath || first.FileName == second.FileName {
t.Fatalf("同批结果物理文件冲突: first=%+v second=%+v", first, second)
}
if first.OriginalName != "DATA-REVIEW-result.xlsx" || second.OriginalName != first.OriginalName {
t.Fatalf("用户下载原始文件名被更改: %q / %q", first.OriginalName, second.OriginalName)
}
RemoveLocalFile(first)
if _, err := os.Stat(first.LocalPath); !os.IsNotExist(err) {
t.Fatalf("首次生成文件未删除: %v", err)
}
file, err := excelize.OpenFile(second.LocalPath)
if err != nil {
t.Fatalf("删除第一份后第二份不可读: %v", err)
}
defer file.Close()
got, err := file.GetCellValue("处理结果", "B2")
if err != nil || got != "second-card" {
t.Fatalf("第二份结果内容损坏: B2=%q err=%v", got, err)
}
}
func TestCreatePreservesWorkbookAndPermissionsForAllBatchTypes(t *testing.T) {
root, err := os.MkdirTemp("", "batchresult-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(root) })
t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root)
oldAdapter := g.Cfg().GetAdapter()
adapter, err := gcfg.NewAdapterContent(`
system:
upload:
local:
path: "resource/upload"
`)
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) })
for _, batchType := range []int{1, 3, 4, 11, 12, 40, 41, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110} {
t.Run(strconv.Itoa(batchType), func(t *testing.T) {
request := Request{BatchNo: "COMPAT-" + strconv.Itoa(batchType), BatchType: batchType, TenantID: "tenant", Rows: []Row{
{RowNo: 1, ICCID: "test-success", ObjectCode: "test-success", Success: true},
{RowNo: 2, ICCID: "test-failed", ObjectCode: "test-failed", Reason: "原有失败原因"},
}}
baseline := filepath.Join(t.TempDir(), "baseline.xlsx")
if err := writeWorkbook(baseline, request); err != nil {
t.Fatal(err)
}
artifact, err := New().Create(context.Background(), request)
if err != nil {
t.Fatal(err)
}
defer RemoveLocalFile(artifact)
if artifact.OriginalName != request.BatchNo+"-result.xlsx" || filepath.Ext(artifact.LocalPath) != ".xlsx" {
t.Fatalf("结果文件名或后缀发生变化: %+v", artifact)
}
baselineInfo, err := os.Stat(baseline)
if err != nil {
t.Fatal(err)
}
actualInfo, err := os.Stat(artifact.LocalPath)
if err != nil || actualInfo.Mode().Perm() != baselineInfo.Mode().Perm() {
t.Fatalf("结果文件权限未保留原SaveAs行为: %v", err)
}
before, err := excelize.OpenFile(baseline)
if err != nil {
t.Fatal(err)
}
defer before.Close()
after, err := excelize.OpenFile(artifact.LocalPath)
if err != nil {
t.Fatal(err)
}
defer after.Close()
want, err := before.GetRows("处理结果")
if err != nil {
t.Fatal(err)
}
got, err := after.GetRows("处理结果")
if err != nil || !reflect.DeepEqual(got, want) {
t.Fatalf("随机物理名改变了批次%d工作簿内容: err=%v", batchType, err)
}
})
}
}
func TestCreateWritesToMountedUploadAndKeepsLogicalPath(t *testing.T) {
wd := t.TempDir()
oldWd, err := os.Getwd()
if err != nil {
t.Fatalf("get working directory: %v", err)
}
if err = os.Chdir(wd); err != nil {
t.Fatalf("change working directory: %v", err)
}
defer func() {
_ = os.Chdir(oldWd)
}()
if err = os.MkdirAll(filepath.Join(wd, "upload"), 0o755); err != nil {
t.Fatalf("create mounted upload root: %v", err)
}
oldAdapter := g.Cfg().GetAdapter()
adapter, err := gcfg.NewAdapterContent(`
system:
upload:
local:
path: "resource/upload"
`)
if err != nil {
t.Fatalf("create config adapter: %v", err)
}
g.Cfg().SetAdapter(adapter)
defer g.Cfg().SetAdapter(oldAdapter)
artifact, err := New().Create(context.Background(), Request{
BatchNo: "INB202608140001001",
BatchType: 1,
TenantID: "000000",
Rows: []Row{{RowNo: 1, ICCID: "89860401786527567308", Success: true}},
})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
defer RemoveLocalFile(artifact)
logicalPath := filepath.ToSlash(artifact.Path)
if !strings.HasPrefix(logicalPath, "resource/upload/000000/user/file/") {
t.Fatalf("logical path = %q, want resource/upload path", artifact.Path)
}
localPath := filepath.ToSlash(artifact.LocalPath)
if !strings.HasPrefix(localPath, "upload/000000/user/file/") {
t.Fatalf("local path = %q, want mounted upload path", artifact.LocalPath)
}
if _, err = os.Stat(artifact.LocalPath); err != nil {
t.Fatalf("result file was not written at local path %q: %v", artifact.LocalPath, err)
}
if _, err = os.Stat(artifact.Path); !os.IsNotExist(err) {
t.Fatalf("result file should not be written to logical-only path %q, stat error=%v", artifact.Path, err)
}
if !strings.HasPrefix(artifact.URL, "/upload/000000/user/file/") {
t.Fatalf("url = %q, want /upload URL", artifact.URL)
}
}