package docextract import ( "archive/zip" "bytes" "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" ) // TestMain regenerates the test fixtures from code before the suite runs, so the fixtures are fully // reproducible (no opaque binary blobs that only a human could recreate). The generated files are also // committed under testdata so the suite still runs if regeneration were ever skipped. Each generator // builds a real file of its format: a valid single page PDF, a valid DOCX zip, a valid HTML document, // and a plain text file. func TestMain(m *testing.M) { if err := writeFixtures(); err != nil { fmt.Fprintf(os.Stderr, "docextract: write fixtures: %v\n", err) os.Exit(1) } os.Exit(m.Run()) } func writeFixtures() error { if err := os.MkdirAll("testdata", 0o755); err != nil { return err } if err := os.WriteFile(filepath.Join("testdata", "sample.pdf"), buildMinimalPDF("SIGINT Analyst"), 0o644); err != nil { return err } docx, err := buildMinimalDOCX("ISSO") if err != nil { return err } if err := os.WriteFile(filepath.Join("testdata", "sample.docx"), docx, 0o644); err != nil { return err } html := "
COMSEC Custodian
" if err := os.WriteFile(filepath.Join("testdata", "sample.html"), []byte(html), 0o644); err != nil { return err } if err := os.WriteFile(filepath.Join("testdata", "sample.txt"), []byte("Cyber Engineer needed for the program."), 0o644); err != nil { return err } return nil } // buildMinimalPDF builds a single page PDF whose content stream renders the given text. The cross // reference table offsets are computed from the running byte length so the file is structurally valid // and a pure Go reader can parse it. func buildMinimalPDF(text string) []byte { var buf bytes.Buffer var offsets []int mark := func() { offsets = append(offsets, buf.Len()) } buf.WriteString("%PDF-1.4\n") mark() buf.WriteString("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") mark() buf.WriteString("2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n") mark() buf.WriteString("3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>\nendobj\n") content := fmt.Sprintf("BT /F1 24 Tf 72 700 Td (%s) Tj ET", text) mark() buf.WriteString(fmt.Sprintf("4 0 obj\n<< /Length %d >>\nstream\n%s\nendstream\nendobj\n", len(content), content)) mark() buf.WriteString("5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n") xrefStart := buf.Len() buf.WriteString("xref\n0 6\n0000000000 65535 f \n") for _, off := range offsets { buf.WriteString(fmt.Sprintf("%010d 00000 n \n", off)) } buf.WriteString("trailer\n<< /Size 6 /Root 1 0 R >>\n") buf.WriteString(fmt.Sprintf("startxref\n%d\n", xrefStart)) buf.WriteString("%%EOF\n") return buf.Bytes() } // buildMinimalDOCX builds a real minimal Office Open XML document (a zip with the three parts a reader // requires: the content types map, the package relationships, and the main document part) whose single // paragraph carries the given text. func buildMinimalDOCX(text string) ([]byte, error) { var buf bytes.Buffer zw := zip.NewWriter(&buf) parts := []struct { name, body string }{ { "[Content_Types].xml", `` + `Visible Role
`) got, err := ExtractText(context.Background(), "x.html", html) if err != nil { t.Fatalf("ExtractText html: %v", err) } if !strings.Contains(got, "Visible Role") { t.Fatalf("visible text missing: %q", got) } if strings.Contains(got, "secret()") || strings.Contains(got, "color:red") { t.Fatalf("script or style content leaked into output: %q", got) } } // TestExtractTextUnknownIsRaw proves an unknown extension returns the raw bytes as a string. func TestExtractTextUnknownIsRaw(t *testing.T) { got, err := ExtractText(context.Background(), "notes.dat", []byte("raw payload")) if err != nil { t.Fatalf("ExtractText unknown: %v", err) } if got != "raw payload" { t.Fatalf("unknown extension should return raw bytes, got %q", got) } } // TestExtractTextCorruptPDF proves a corrupt PDF returns a wrapped parse error, not a panic. func TestExtractTextCorruptPDF(t *testing.T) { _, err := ExtractText(context.Background(), "broken.pdf", []byte("%PDF-1.4 this is not a real pdf body")) if err == nil { t.Fatal("expected an error on a corrupt pdf") } if !errors.Is(err, ErrParse) { t.Fatalf("corrupt pdf error should wrap ErrParse, got %v", err) } } // TestExtractTextCorruptDOCX proves a non zip DOCX returns a wrapped parse error. func TestExtractTextCorruptDOCX(t *testing.T) { _, err := ExtractText(context.Background(), "broken.docx", []byte("not a zip at all")) if err == nil { t.Fatal("expected an error on a corrupt docx") } if !errors.Is(err, ErrParse) { t.Fatalf("corrupt docx error should wrap ErrParse, got %v", err) } } // TestExtractTextCap proves the output is capped at maxTextBytes. func TestExtractTextCap(t *testing.T) { big := bytes.Repeat([]byte("A"), maxTextBytes+5000) got, err := ExtractText(context.Background(), "big.txt", big) if err != nil { t.Fatalf("ExtractText big txt: %v", err) } if len(got) > maxTextBytes { t.Fatalf("output not capped: got %d bytes, cap %d", len(got), maxTextBytes) } }