Files
xls_doc/internal/docfill/p0_test.go
2026-06-24 09:45:35 +08:00

575 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package docfill
import (
"database/sql"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRunResolvesConfigRelativePathsAndSanitizesOutputFileNames(t *testing.T) {
dir := t.TempDir()
taskDir := filepath.Join(dir, "task")
if err := os.MkdirAll(taskDir, 0o755); err != nil {
t.Fatalf("make task dir: %v", err)
}
writeWorkbook(t, filepath.Join(taskDir, "data.xlsx"), "Daily", [][]string{
{"name", "done"},
{"张/三:测试", "已完成"},
})
writeWorkbook(t, filepath.Join(taskDir, "template.xlsx"), "日报", [][]string{
{"{{name}}", "{{done}}"},
})
configPath := filepath.Join(taskDir, "task.json")
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": "./data.xlsx",
"sheet": "Daily",
},
"template": map[string]any{
"path": "./template.xlsx",
},
"output": map[string]any{
"path": "./out/日报_{{name}}.xlsx",
},
})
result, err := RunWithOptions(configPath, RunOptions{})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
wantPath := filepath.Join(taskDir, "out", "日报_张_三_测试.xlsx")
if len(result.Files) != 1 || result.Files[0] != wantPath {
t.Fatalf("result files = %#v, want %q", result.Files, wantPath)
}
assertWorkbookCell(t, wantPath, "日报", "A1", "张/三:测试")
}
func TestRunUsesVarsForSQLiteParamsAndTemplateDefaults(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "daily.db")
templatePath := filepath.Join(dir, "template.docx")
configPath := filepath.Join(dir, "task.json")
seedP0SQLite(t, dbPath, `
create table daily (name text, date text, done text);
insert into daily values
('张三', '2026-06-18', '完成旧日报'),
('李四', '2026-06-19', '完成新日报');
`)
writeMinimalDocx(t, templatePath, "批次:{{batch}}\n日期{{date}}\n员工{{name}}\n内容{{done}}")
writeJSON(t, configPath, map[string]any{
"vars": map[string]any{
"date": "2026-06-18",
"batch": "默认批次",
},
"data": map[string]any{
"type": "sqlite",
"path": dbPath,
"query": "select name, date, done from daily where date = ?",
"params": []any{"{{date}}"},
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{batch}}_{{date}}_{{name}}.docx"),
},
})
result, err := RunWithOptions(configPath, RunOptions{
Vars: map[string]string{
"date": "2026-06-19",
"batch": "命令行批次",
},
})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
if len(result.Files) != 1 {
t.Fatalf("expected 1 generated file, got %#v", result.Files)
}
got := readZipEntry(t, result.Files[0], "word/document.xml")
if !strings.Contains(got, "批次:命令行批次") || !strings.Contains(got, "员工:李四") {
t.Fatalf("unexpected rendered docx body: %q", got)
}
if strings.Contains(got, "张三") || strings.Contains(result.Files[0], "默认批次") {
t.Fatalf("CLI vars did not override config vars: file=%q body=%q", result.Files[0], got)
}
}
func TestRunSupportsExcelHeaderRow(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"日报导出数据"},
{"name", "done"},
{"张三", "完成日报"},
})
writeWorkbook(t, templatePath, "日报", [][]string{
{"员工", "{{name}}"},
{"完成", "{{done}}"},
})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
"header_row": 2,
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
if err := Run(configPath); err != nil {
t.Fatalf("Run returned error: %v", err)
}
assertWorkbookCell(t, filepath.Join(dir, "out", "日报_张三.xlsx"), "日报", "B1", "张三")
}
func TestRunRefusesToOverwriteUnlessConfigured(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
outputPath := filepath.Join(dir, "out", "日报_张三.xlsx")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name"},
{"张三"},
})
writeWorkbook(t, templatePath, "日报", [][]string{{"{{name}}"}})
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
t.Fatalf("make output dir: %v", err)
}
if err := os.WriteFile(outputPath, []byte("existing"), 0o644); err != nil {
t.Fatalf("write existing output: %v", err)
}
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": outputPath,
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Fatalf("expected already exists error, got %v", err)
}
var appErr *AppError
if !errors.As(err, &appErr) {
t.Fatalf("expected AppError, got %T", err)
}
if appErr.Code != CodeOutputExists {
t.Fatalf("error code = %s, want %s", appErr.Code, CodeOutputExists)
}
if appErr.Hint == "" || appErr.Context["output"] != outputPath {
t.Fatalf("expected hint and output context, got %#v", appErr)
}
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": outputPath,
"overwrite": true,
},
})
if err := Run(configPath); err != nil {
t.Fatalf("Run with overwrite returned error: %v", err)
}
assertWorkbookCell(t, outputPath, "日报", "A1", "张三")
}
func TestRunFailsWhenTemplatePlaceholderIsMissing(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name"},
{"张三"},
})
writeWorkbook(t, templatePath, "日报", [][]string{{"{{name}}", "{{missing}}"}})
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": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "missing template field") {
t.Fatalf("expected missing template field error, got %v", err)
}
var appErr *AppError
if !errors.As(err, &appErr) {
t.Fatalf("expected AppError, got %T", err)
}
if appErr.Code != CodeTemplateFieldMissing {
t.Fatalf("error code = %s, want %s", appErr.Code, CodeTemplateFieldMissing)
}
if appErr.Context["field"] != "missing" || appErr.Context["template"] != templatePath {
t.Fatalf("unexpected context: %#v", appErr.Context)
}
if !strings.Contains(appErr.Hint, "missing") {
t.Fatalf("unexpected hint: %q", appErr.Hint)
}
}
func TestRunFailsWhenRenderedTemplateStillHasPlaceholder(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name", "done"},
{"张三", "{{still_pending}}"},
})
writeWorkbook(t, templatePath, "日报", [][]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": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "unresolved placeholder") {
t.Fatalf("expected unresolved placeholder error, got %v", err)
}
}
func TestRunFiltersExcelRowsByMultipleEqualsAndVars(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "tickets.xlsx")
templatePath := filepath.Join(dir, "ticket-template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "工单", [][]string{
{"日期", "工单编号", "客户名称", "处理结果"},
{"2026-06-18", "GD-001", "A 公司", "已派单"},
{"2026-06-18", "GD-002", "B 公司", "已完成"},
{"2026-06-19", "GD-001", "C 公司", "待复核"},
})
writeWorkbook(t, templatePath, "工单", [][]string{
{"编号", "{{工单编号}}"},
{"客户", "{{客户名称}}"},
{"结果", "{{处理结果}}"},
})
writeJSON(t, configPath, map[string]any{
"vars": map[string]any{
"date": "2026-06-17",
},
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "工单",
},
"filter": map[string]any{
"equals": map[string]any{
"日期": "{{date}}",
"工单编号": "{{ticket_no}}",
},
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单_{{日期}}_{{工单编号}}.xlsx"),
},
})
result, err := RunWithOptions(configPath, RunOptions{
Vars: map[string]string{
"date": "2026-06-18",
"ticket_no": "GD-002",
},
})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
wantPath := filepath.Join(dir, "out", "工单_2026-06-18_GD-002.xlsx")
if len(result.Files) != 1 || result.Files[0] != wantPath {
t.Fatalf("result files = %#v, want only %q", result.Files, wantPath)
}
assertWorkbookCell(t, wantPath, "工单", "B2", "B 公司")
assertWorkbookCell(t, wantPath, "工单", "B3", "已完成")
}
func TestRunFilterAllowsManyRowsWhenConfigured(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "tickets.db")
templatePath := filepath.Join(dir, "ticket-template.xlsx")
configPath := filepath.Join(dir, "task.json")
seedP0SQLite(t, dbPath, `
create table tickets (
report_date text,
department text,
ticket_no text,
result text
);
insert into tickets values
('2026-06-18', '营销部', 'GD-001', '已派单'),
('2026-06-18', '客服部', 'GD-002', '已完成'),
('2026-06-19', '营销部', 'GD-003', '待复核');
`)
writeWorkbook(t, templatePath, "工单", [][]string{
{"编号", "{{ticket_no}}"},
{"部门", "{{department}}"},
{"结果", "{{result}}"},
})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "sqlite",
"path": dbPath,
"query": "select report_date, department, ticket_no, result from tickets order by ticket_no",
},
"filter": map[string]any{
"equals": map[string]any{
"report_date": "{{date}}",
},
"expect": "many",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单_{{ticket_no}}_{{_row}}.xlsx"),
},
})
result, err := RunWithOptions(configPath, RunOptions{Vars: map[string]string{"date": "2026-06-18"}})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
if len(result.Files) != 2 {
t.Fatalf("expected 2 filtered files, got %#v", result.Files)
}
assertWorkbookCell(t, filepath.Join(dir, "out", "工单_GD-001_1.xlsx"), "工单", "B3", "已派单")
assertWorkbookCell(t, filepath.Join(dir, "out", "工单_GD-002_2.xlsx"), "工单", "B2", "客服部")
}
func TestRunFilterFailsWhenNoRowsMatch(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "tickets.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "工单", [][]string{
{"日期", "工单编号", "处理结果"},
{"2026-06-18", "GD-001", "已派单"},
})
writeWorkbook(t, templatePath, "工单", [][]string{{"{{工单编号}}", "{{处理结果}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "工单",
},
"filter": map[string]any{
"equals": map[string]any{
"日期": "{{date}}",
"工单编号": "{{ticket_no}}",
},
"expect": "one",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单_{{工单编号}}.xlsx"),
},
})
err := RunWithOptionsError(configPath, RunOptions{Vars: map[string]string{
"date": "2026-06-18",
"ticket_no": "GD-999",
}})
assertAppErrorCode(t, err, ErrorCode("DATA_FILTER_NO_MATCH"))
}
func TestRunFilterFailsWhenOneRowExpectedButManyMatch(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "tickets.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "工单", [][]string{
{"日期", "工单编号", "处理结果"},
{"2026-06-18", "GD-001", "已派单"},
{"2026-06-18", "GD-002", "已完成"},
})
writeWorkbook(t, templatePath, "工单", [][]string{{"{{工单编号}}", "{{处理结果}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "工单",
},
"filter": map[string]any{
"equals": map[string]any{
"日期": "{{date}}",
},
"expect": "one",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单_{{工单编号}}.xlsx"),
},
})
err := RunWithOptionsError(configPath, RunOptions{Vars: map[string]string{"date": "2026-06-18"}})
assertAppErrorCode(t, err, ErrorCode("DATA_FILTER_MULTI_MATCH"))
}
func TestRunFilterFailsWhenFieldIsMissing(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "tickets.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "工单", [][]string{
{"日期", "处理结果"},
{"2026-06-18", "已派单"},
})
writeWorkbook(t, templatePath, "工单", [][]string{{"{{处理结果}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "工单",
},
"filter": map[string]any{
"equals": map[string]any{
"工单编号": "{{ticket_no}}",
},
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单.xlsx"),
},
})
err := RunWithOptionsError(configPath, RunOptions{Vars: map[string]string{"ticket_no": "GD-001"}})
assertAppErrorCode(t, err, ErrorCode("DATA_FILTER_FIELD"))
}
func TestRunFilterFailsWhenVariableIsMissing(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "tickets.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "工单", [][]string{
{"日期", "工单编号", "处理结果"},
{"2026-06-18", "GD-001", "已派单"},
})
writeWorkbook(t, templatePath, "工单", [][]string{{"{{工单编号}}", "{{处理结果}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "工单",
},
"filter": map[string]any{
"equals": map[string]any{
"工单编号": "{{ticket_no}}",
},
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "工单_{{工单编号}}.xlsx"),
},
})
err := RunWithOptionsError(configPath, RunOptions{})
assertAppErrorCode(t, err, ErrorCode("DATA_FILTER_VALUE"))
}
func RunWithOptionsError(configPath string, opts RunOptions) error {
_, err := RunWithOptions(configPath, opts)
return err
}
func assertAppErrorCode(t *testing.T, err error, want ErrorCode) {
t.Helper()
if err == nil {
t.Fatalf("expected %s error, got nil", want)
}
var appErr *AppError
if !errors.As(err, &appErr) {
t.Fatalf("expected AppError, got %T: %v", err, err)
}
if appErr.Code != want {
t.Fatalf("error code = %s, want %s; error=%v", appErr.Code, want, err)
}
}
func seedP0SQLite(t *testing.T, path, statements string) {
t.Helper()
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
defer db.Close()
if _, err := db.Exec(statements); err != nil {
t.Fatalf("seed sqlite: %v", err)
}
}