310 lines
7.9 KiB
Go
310 lines
7.9 KiB
Go
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, `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>员工:{{na</w:t></w:r><w:r><w:t>me}}</w:t></w:r><w:r><w:t>,完成:{{done}}</w:t></w:r></w:p></w:body></w:document>`)
|
||
|
||
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", `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>`)
|
||
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)
|
||
}
|
||
}
|