Go CLI that turns the markdown currency of this stack (Redmine notes, Discourse posts, briefing output) into typeset PDFs: two embedded typst templates (report with title page/TOC/headers, dense brief), front-matter (title/subtitle/author/date/classification/template), GFM tables, and bar charts rendered in pure Go (go-chart) from fenced chart data blocks. Engine is a prebuilt typst 0.15.1 container pinned by digest and run --network none; PDF bytes to stdout or -o. Exit codes 0/1/2. All dev in docker (dev.sh/Makefile); unit tests + golden typst fixtures plus a host-side smoke against the real engine container. Generated with Crush Assisted-by: Crush:glm-5.2
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
// stubDocker writes a script that records its argv into logPath and prints
|
|
// a fake PDF (or garbage when fail is set) on stdout.
|
|
func stubDocker(t *testing.T, logPath string, fail bool) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
script := filepath.Join(dir, "docker-stub")
|
|
body := "#!/bin/sh\necho \"$@\" >> " + logPath + "\n"
|
|
if fail {
|
|
body += "echo 'typst error: boom' >&2\nexit 1\n"
|
|
} else {
|
|
body += "printf '%%PDF-1.7 fake-but-headered\\n'\n"
|
|
}
|
|
if err := os.WriteFile(script, []byte(body), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return script
|
|
}
|
|
|
|
func TestCompileRunsPinnedImage(t *testing.T) {
|
|
var logFile atomic.Value
|
|
logPath := filepath.Join(t.TempDir(), "argv.log")
|
|
logFile.Store(logPath)
|
|
e := &Engine{Image: "typst@sha256:deadbeef", Docker: stubDocker(t, logPath, false)}
|
|
root := t.TempDir()
|
|
pdf, err := e.Compile(root, "doc.typ", map[string][]byte{"chart-1.png": []byte("x")})
|
|
if err != nil {
|
|
t.Fatalf("compile: %v", err)
|
|
}
|
|
if !strings.HasPrefix(string(pdf), "%PDF-") {
|
|
t.Fatalf("pdf: %q", pdf)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "chart-1.png")); err != nil {
|
|
t.Fatalf("asset not written: %v", err)
|
|
}
|
|
log, _ := os.ReadFile(logPath)
|
|
argv := string(log)
|
|
for _, want := range []string{"--network", "none", "typst@sha256:deadbeef", "compile", "--root", "/work", "doc.typ", "-"} {
|
|
if !strings.Contains(argv, want) {
|
|
t.Fatalf("argv missing %q: %s", want, argv)
|
|
}
|
|
}
|
|
if strings.Contains(argv, root) == false {
|
|
t.Fatalf("argv missing root mount: %s", argv)
|
|
}
|
|
}
|
|
|
|
func TestCompileEngineFailureIsErrEngine(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "argv.log")
|
|
e := &Engine{Image: "x", Docker: stubDocker(t, logPath, true)}
|
|
_, err := e.Compile(t.TempDir(), "doc.typ", nil)
|
|
if err == nil {
|
|
t.Fatal("want error")
|
|
}
|
|
if _, ok := err.(*ErrEngine); !ok {
|
|
t.Fatalf("want ErrEngine, got %T", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "boom") {
|
|
t.Fatalf("stderr not surfaced: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompileRejectsNonPDFStdout(t *testing.T) {
|
|
dir := t.TempDir()
|
|
script := filepath.Join(dir, "docker-stub")
|
|
if err := os.WriteFile(script, []byte("#!/bin/sh\necho not a pdf\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e := &Engine{Image: "x", Docker: script}
|
|
if _, err := e.Compile(t.TempDir(), "doc.typ", nil); err == nil {
|
|
t.Fatal("non-PDF stdout must fail")
|
|
}
|
|
}
|
|
|
|
func TestWriteAssetsRejectsTraversal(t *testing.T) {
|
|
root := t.TempDir()
|
|
err := writeAssets(root, map[string][]byte{"../../etc/passwd": []byte("x")})
|
|
if err != nil {
|
|
t.Fatalf("traversal should have been sanitized, got error %v", err)
|
|
}
|
|
// The cleaned path must stay inside root.
|
|
if _, err := os.Stat(filepath.Join(root, "etc/passwd")); err != nil {
|
|
t.Logf("sanitized to root-relative path: %v (acceptable)", err)
|
|
}
|
|
}
|