package docfill import ( "archive/zip" "database/sql" "encoding/json" "io" "os" "path/filepath" "strings" "testing" _ "modernc.org/sqlite" "github.com/xuri/excelize/v2" ) func TestReadExcelRowsUsesFirstRowAsHeaders(t *testing.T) { path := filepath.Join(t.TempDir(), "data.xlsx") writeWorkbook(t, path, "Daily", [][]string{ {"name", "date", "done"}, {"Alice", "2026-06-18", "接口数据已整理"}, {"Bob", "2026-06-18", "报表已提交"}, }) rows, err := readRows(dataConfig{Type: "excel", Path: path, Sheet: "Daily"}) if err != nil { t.Fatalf("readRows returned error: %v", err) } if len(rows) != 2 { t.Fatalf("expected 2 rows, got %d", len(rows)) } if rows[0]["name"] != "Alice" || rows[0]["done"] != "接口数据已整理" { t.Fatalf("unexpected first row: %#v", rows[0]) } if rows[1]["name"] != "Bob" || rows[1]["date"] != "2026-06-18" { t.Fatalf("unexpected second row: %#v", rows[1]) } } func TestReadSQLiteRowsUsesQueryColumnNames(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "data.db") db, err := sql.Open("sqlite", dbPath) if err != nil { t.Fatalf("open sqlite: %v", err) } defer db.Close() _, err = db.Exec(` create table daily (name text, done text); insert into daily (name, done) values ('Alice', '完成日报'), ('Bob', '完成周报'); `) if err != nil { t.Fatalf("seed sqlite: %v", err) } rows, err := readRows(dataConfig{ Type: "sqlite", Path: dbPath, Query: "select name, done from daily order by name", }) if err != nil { t.Fatalf("readRows returned error: %v", err) } if len(rows) != 2 { t.Fatalf("expected 2 rows, got %d", len(rows)) } if rows[0]["name"] != "Alice" || rows[0]["done"] != "完成日报" { t.Fatalf("unexpected first row: %#v", rows[0]) } } func TestRenderXLSXReplacesPlaceholders(t *testing.T) { dir := t.TempDir() templatePath := filepath.Join(dir, "template.xlsx") outputPath := filepath.Join(dir, "out.xlsx") writeWorkbook(t, templatePath, "Sheet1", [][]string{ {"姓名", "内容"}, {"{{name}}", "今天完成:{{done}}"}, }) err := renderTemplate(templatePath, outputPath, map[string]string{ "name": "Alice", "done": "接口数据整理", }) if err != nil { t.Fatalf("renderTemplate returned error: %v", err) } f, err := excelize.OpenFile(outputPath) if err != nil { t.Fatalf("open output: %v", err) } defer f.Close() name, err := f.GetCellValue("Sheet1", "A2") if err != nil { t.Fatalf("read A2: %v", err) } done, err := f.GetCellValue("Sheet1", "B2") if err != nil { t.Fatalf("read B2: %v", err) } if name != "Alice" || done != "今天完成:接口数据整理" { t.Fatalf("unexpected rendered values: A2=%q B2=%q", name, done) } } func TestRenderDOCXReplacesPlaceholdersInXMLParts(t *testing.T) { dir := t.TempDir() templatePath := filepath.Join(dir, "template.docx") outputPath := filepath.Join(dir, "out.docx") writeMinimalDocx(t, templatePath, "日报:{{name}} 完成 {{done}}") err := renderTemplate(templatePath, outputPath, map[string]string{ "name": "Alice", "done": "接口数据整理", }) if err != nil { t.Fatalf("renderTemplate returned error: %v", err) } got := readZipEntry(t, outputPath, "word/document.xml") want := "日报:Alice 完成 接口数据整理" if got != want { t.Fatalf("document.xml = %q, want %q", got, want) } } func TestRenderDOCXReplacesPlaceholderSplitAcrossTextNodes(t *testing.T) { dir := t.TempDir() templatePath := filepath.Join(dir, "template.docx") outputPath := filepath.Join(dir, "out.docx") writeMinimalDocx(t, templatePath, `员工:{{name}},完成:{{done}}`) err := renderTemplate(templatePath, outputPath, map[string]string{ "name": "Alice", "done": "接口数据整理", }) if err != nil { t.Fatalf("renderTemplate returned error: %v", err) } got := readZipEntry(t, outputPath, "word/document.xml") if !strings.Contains(got, "员工:Alice,完成:接口数据整理") { t.Fatalf("document.xml = %q, want rendered split placeholder", got) } if strings.Contains(got, "{{") || strings.Contains(got, "}}") { t.Fatalf("document.xml still has placeholder markers: %q", got) } } func TestRunGeneratesOneOutputPerInputRow(t *testing.T) { dir := t.TempDir() dataPath := filepath.Join(dir, "data.xlsx") templatePath := filepath.Join(dir, "template.xlsx") outputPattern := filepath.Join(dir, "out", "report_{{name}}_{{_row}}.xlsx") configPath := filepath.Join(dir, "task.json") writeWorkbook(t, dataPath, "Daily", [][]string{ {"name", "done"}, {"Alice", "日报"}, {"Bob", "周报"}, }) writeWorkbook(t, templatePath, "Sheet1", [][]string{ {"{{name}}", "{{done}}"}, }) writeJSON(t, configPath, map[string]any{ "data": map[string]any{ "type": "excel", "path": dataPath, "sheet": "Daily", }, "template": map[string]any{ "path": templatePath, }, "output": map[string]any{ "path": outputPattern, }, }) if err := Run(configPath); err != nil { t.Fatalf("Run returned error: %v", err) } assertWorkbookCell(t, filepath.Join(dir, "out", "report_Alice_1.xlsx"), "Sheet1", "A1", "Alice") assertWorkbookCell(t, filepath.Join(dir, "out", "report_Bob_2.xlsx"), "Sheet1", "B1", "周报") } func writeWorkbook(t *testing.T, path, sheet string, rows [][]string) { t.Helper() f := excelize.NewFile() defer f.Close() defaultSheet := f.GetSheetName(0) if defaultSheet != sheet { if err := f.SetSheetName(defaultSheet, sheet); err != nil { t.Fatalf("set sheet name: %v", err) } } for r, row := range rows { for c, value := range row { cell, err := excelize.CoordinatesToCellName(c+1, r+1) if err != nil { t.Fatalf("cell name: %v", err) } if err := f.SetCellValue(sheet, cell, value); err != nil { t.Fatalf("set cell value: %v", err) } } } if err := f.SaveAs(path); err != nil { t.Fatalf("save workbook: %v", err) } } func writeMinimalDocx(t *testing.T, path, body string) { t.Helper() file, err := os.Create(path) if err != nil { t.Fatalf("create docx: %v", err) } defer file.Close() zw := zip.NewWriter(file) writeZipEntry(t, zw, "[Content_Types].xml", ``) writeZipEntry(t, zw, "word/document.xml", body) if err := zw.Close(); err != nil { t.Fatalf("close zip: %v", err) } } func writeZipEntry(t *testing.T, zw *zip.Writer, name, body string) { t.Helper() w, err := zw.Create(name) if err != nil { t.Fatalf("create zip entry %s: %v", name, err) } if _, err := w.Write([]byte(body)); err != nil { t.Fatalf("write zip entry %s: %v", name, err) } } func readZipEntry(t *testing.T, path, name string) string { t.Helper() zr, err := zip.OpenReader(path) if err != nil { t.Fatalf("open zip: %v", err) } defer zr.Close() for _, file := range zr.File { if file.Name != name { continue } rc, err := file.Open() if err != nil { t.Fatalf("open zip entry %s: %v", name, err) } defer rc.Close() data, err := io.ReadAll(rc) if err != nil { t.Fatalf("read zip entry %s: %v", name, err) } return string(data) } t.Fatalf("zip entry %s not found", name) return "" } func writeJSON(t *testing.T, path string, value any) { t.Helper() data, err := json.MarshalIndent(value, "", " ") if err != nil { t.Fatalf("marshal json: %v", err) } if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatalf("write json: %v", err) } } func assertWorkbookCell(t *testing.T, path, sheet, cell, want string) { t.Helper() f, err := excelize.OpenFile(path) if err != nil { t.Fatalf("open workbook %s: %v", path, err) } defer f.Close() got, err := f.GetCellValue(sheet, cell) if err != nil { t.Fatalf("read cell %s: %v", cell, err) } if got != want { t.Fatalf("%s %s = %q, want %q", filepath.Base(path), cell, got, want) } }