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 TestRenderDOCXReplacesMultipleSplitPlaceholdersInSameParagraph(t *testing.T) {
dir := t.TempDir()
templatePath := filepath.Join(dir, "template.docx")
outputPath := filepath.Join(dir, "out.docx")
writeMinimalDocx(t, templatePath, `员工:{{name}},完成:{{done}},备注:{{note}}`)
err := renderTemplate(templatePath, outputPath, map[string]string{
"name": "Alice",
"done": "接口数据整理",
"note": "保持同段落",
})
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 placeholders", 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)
}
}