package convert import ( "context" "errors" "strings" "sync" "testing" "time" ) // fakeRunner records the args it was invoked with and replays canned // responses. Lets us assert command lines + binary refs + scratch // dirs without needing actual pandoc. type fakeRunner struct { mu sync.Mutex calls [][]string binaries []string stdin [][]byte scratchDir []string resp []byte err error } func (f *fakeRunner) Run(_ context.Context, binary string, stdin []byte, scratchDir string, cmd []string) ([]byte, error) { f.mu.Lock() defer f.mu.Unlock() f.calls = append(f.calls, append([]string(nil), cmd...)) f.binaries = append(f.binaries, binary) f.stdin = append(f.stdin, append([]byte(nil), stdin...)) f.scratchDir = append(f.scratchDir, scratchDir) return f.resp, f.err } func (f *fakeRunner) lastCall() (string, []string) { f.mu.Lock() defer f.mu.Unlock() if len(f.calls) == 0 { return "", nil } return f.binaries[len(f.binaries)-1], f.calls[len(f.calls)-1] } func TestToDocx_UsesPandocBinary(t *testing.T) { f := &fakeRunner{resp: []byte("FAKE-DOCX")} InstallRunner(f) t.Cleanup(func() { InstallRunner(nil) }) SetBinaries("pandoc", "chromium-browser") out, err := ToDocx(context.Background(), []byte("# Hello\n"), Metadata{ Title: "Hello", Client: "Acme", }) if err != nil { t.Fatalf("ToDocx: %v", err) } if string(out) != "FAKE-DOCX" { t.Errorf("unexpected output: %q", out) } binary, call := f.lastCall() if binary != "pandoc" { t.Errorf("expected pandoc binary, got %q", binary) } if !contains(call, "--to=docx") { t.Errorf("missing --to=docx: %v", call) } if !contains(call, "title=Hello") { t.Errorf("missing title metadata: %v", call) } if !contains(call, "client=Acme") { t.Errorf("missing client metadata: %v", call) } // Last arg must be "-" so pandoc reads from stdin. if call[len(call)-1] != "-" { t.Errorf("expected stdin marker as last arg, got %q", call[len(call)-1]) } // ToDocx is stdin → stdout — no scratch dir needed. if f.scratchDir[len(f.scratchDir)-1] != "" { t.Errorf("ToDocx should not need a scratch dir, got %q", f.scratchDir[len(f.scratchDir)-1]) } } func TestToHTML_UsesTemplateFromScratchDir(t *testing.T) { f := &fakeRunner{resp: []byte("fake")} InstallRunner(f) t.Cleanup(func() { InstallRunner(nil) }) SetBinaries("pandoc", "chromium-browser") _, err := ToHTML(context.Background(), []byte("# Hi\n"), Metadata{Title: "Hi"}) if err != nil { t.Fatalf("ToHTML: %v", err) } binary, call := f.lastCall() if binary != "pandoc" { t.Errorf("expected pandoc binary, got %q", binary) } // Template flag must reference an absolute path under the scratch // dir (no /tpl indirection anymore — the wrapper bind-mounts the // scratch dir at its own path, so absolute host paths just work). scratch := f.scratchDir[len(f.scratchDir)-1] if scratch == "" { t.Fatalf("ToHTML must pass a scratch dir to the runner") } wantTpl := "--template=" + scratch + "/viewer-template.html" if !contains(call, wantTpl) { t.Errorf("template flag missing/wrong; want %q in %v", wantTpl, call) } if !contains(call, "--toc") { t.Errorf("TOC flag missing (default NoTOC=false): %v", call) } } func TestToHTML_NoTOCSuppressesTOC(t *testing.T) { f := &fakeRunner{resp: []byte("
")} InstallRunner(f) t.Cleanup(func() { InstallRunner(nil) }) _, _ = ToHTML(context.Background(), []byte("# Hi\n"), Metadata{NoTOC: true}) _, call := f.lastCall() if contains(call, "--toc") { t.Errorf("TOC should be suppressed when NoTOC=true: %v", call) } if !contains(call, "no-toc=true") { t.Errorf("no-toc metadata variable missing: %v", call) } } // recordingRunner records every call and returns canned responses in // sequence. Lets ToPDF tests assert the two-stage pipeline (pandoc // then chromium). type recordingRunner struct { mu sync.Mutex calls []recordedCall resp [][]byte err []error cursor int } type recordedCall struct { binary string cmd []string scratch string } func (r *recordingRunner) Run(_ context.Context, binary string, _ []byte, scratch string, cmd []string) ([]byte, error) { r.mu.Lock() defer r.mu.Unlock() r.calls = append(r.calls, recordedCall{ binary: binary, cmd: append([]string(nil), cmd...), scratch: scratch, }) if r.cursor >= len(r.resp) { return nil, nil } out := r.resp[r.cursor] var e error if r.cursor < len(r.err) { e = r.err[r.cursor] } r.cursor++ return out, e } func TestScratchDir_UsedByToHTML(t *testing.T) { f := &fakeRunner{resp: []byte("fake"), // stage 1 stdout nil, // stage 2 stdout (chromium writes PDF to scratch) }, } InstallRunner(r) t.Cleanup(func() { InstallRunner(nil) }) SetBinaries("pandoc", "chromium-browser") _, err := ToPDF(context.Background(), []byte("# Hi\n"), Metadata{}) // PDF read-back will fail (fake runner didn't write the file) — // that's expected for this test which only inspects the call shape. if err == nil { t.Fatalf("expected error from PDF read-back; got nil") } if len(r.calls) != 2 { t.Fatalf("expected 2 calls (pandoc + chromium); got %d", len(r.calls)) } if r.calls[0].binary != "pandoc" { t.Errorf("stage 1 binary: got %q want pandoc", r.calls[0].binary) } if r.calls[1].binary != "chromium-browser" { t.Errorf("stage 2 binary: got %q want chromium-browser", r.calls[1].binary) } // Stage 2 must include --print-to-pdf pointing at an absolute // path under the scratch dir. stage2 := r.calls[1] if stage2.scratch == "" { t.Fatalf("chromium call must have a scratch dir") } wantPDF := "--print-to-pdf=" + stage2.scratch + "/out.pdf" if !contains(stage2.cmd, wantPDF) { t.Errorf("chromium call missing --print-to-pdf=%s/out.pdf: %v", stage2.scratch, stage2.cmd) } if !contains(stage2.cmd, "--no-sandbox") { t.Errorf("chromium call missing --no-sandbox: %v", stage2.cmd) } // Stage 2 chromium reads file://