initial public release
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package cv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func setupProfileDB(t *testing.T, name, email, location string) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE profile (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
name TEXT, email TEXT, location TEXT, timezone TEXT,
|
||||
salary_target_min REAL, salary_target_max REAL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name != "" {
|
||||
if _, err := db.Exec(`INSERT INTO profile (id, name, email, location) VALUES (1, ?, ?, ?)`,
|
||||
name, email, location); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestLaTeXExporter_Export_EndToEnd(t *testing.T) {
|
||||
db := setupProfileDB(t, "Ada Lovelace", "ada@example.com", "London, UK")
|
||||
defer db.Close()
|
||||
tmp := t.TempDir()
|
||||
e := &LaTeXExporter{DB: db, OutputDir: tmp}
|
||||
|
||||
out, err := e.Export(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Export: %v", err)
|
||||
}
|
||||
if out.TeXPath == "" {
|
||||
t.Fatal("TeXPath empty")
|
||||
}
|
||||
if filepath.Dir(out.TeXPath) != tmp {
|
||||
t.Errorf("tex written outside OutputDir: %q", out.TeXPath)
|
||||
}
|
||||
data, err := os.ReadFile(out.TeXPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(data)
|
||||
if !strings.Contains(s, "Ada Lovelace") {
|
||||
t.Error("name not spliced")
|
||||
}
|
||||
if !strings.Contains(s, "ada@example.com") {
|
||||
t.Error("email not spliced")
|
||||
}
|
||||
if strings.Contains(s, "{{NAME}}") || strings.Contains(s, "{{EXPERIENCE}}") {
|
||||
t.Error("placeholders left unfilled in output")
|
||||
}
|
||||
if !strings.Contains(s, `\begin{document}`) || !strings.Contains(s, `\end{document}`) {
|
||||
t.Error("document structure missing")
|
||||
}
|
||||
// pdflatex may or may not be present in CI; either way the tex must succeed.
|
||||
// If present, PDFPath is populated; if absent, Issues notes the skip.
|
||||
if out.PDFPath == "" && len(out.Issues) == 0 {
|
||||
t.Log("pdflatex present but PDFPath unset — check compile path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaTeXExporter_LaTeXEscapingOnContact(t *testing.T) {
|
||||
db := setupProfileDB(t, "R&D Person", "x_y%z@example.com", "C#/OT")
|
||||
defer db.Close()
|
||||
e := &LaTeXExporter{DB: db, OutputDir: t.TempDir()}
|
||||
out, err := e.Export(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Export: %v", err)
|
||||
}
|
||||
data, _ := os.ReadFile(out.TeXPath)
|
||||
s := string(data)
|
||||
// & should have been escaped to \&, % to \%, # to \#, _ to \_
|
||||
for _, bad := range []string{"R&D Person", "C#/OT"} {
|
||||
if strings.Contains(s, bad) {
|
||||
t.Errorf("unescaped LaTeX special in output for %q", bad)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(s, `R\&D Person`) {
|
||||
t.Error("missing escaped ampersand in name")
|
||||
}
|
||||
if !strings.Contains(s, `C\#/OT`) {
|
||||
t.Error("missing escaped hash in location")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_CatchesStructuralGaps(t *testing.T) {
|
||||
badTex := `\documentclass{article}
|
||||
\begin{document}
|
||||
Hello.
|
||||
\end{document}
|
||||
`
|
||||
issues := validate(badTex)
|
||||
if len(issues) == 0 {
|
||||
t.Fatal("expected structural issues")
|
||||
}
|
||||
var sawEducation, sawSubheading bool
|
||||
for _, i := range issues {
|
||||
if strings.Contains(i, "Education") {
|
||||
sawEducation = true
|
||||
}
|
||||
if strings.Contains(i, "resumeSubheading") {
|
||||
sawSubheading = true
|
||||
}
|
||||
}
|
||||
if !sawEducation || !sawSubheading {
|
||||
t.Errorf("missing expected issues: got %v", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_FlagsUnresolvedPlaceholders(t *testing.T) {
|
||||
// Minimal file with required markers but an unresolved placeholder.
|
||||
content := `\begin{document}
|
||||
\section{Education}
|
||||
\section{Work Experience}
|
||||
\section{Personal Projects}
|
||||
\section{Technical Skills}
|
||||
\resumeSubheading\resumeItem\resumeProjectHeading
|
||||
Name: {{NAME}}
|
||||
\end{document}
|
||||
`
|
||||
issues := validate(content)
|
||||
joined := strings.Join(issues, " | ")
|
||||
if !strings.Contains(joined, "unresolved placeholders") {
|
||||
t.Errorf("expected unresolved-placeholder flag, got: %s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"Ada Lovelace": "ada-lovelace",
|
||||
" Dr. María ": "dr-mara", // diacritics dropped (ASCII-only)
|
||||
"../../evil": "evil",
|
||||
"": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := sanitizeFilename(in); got != want {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user