diff --git a/src/__tests__/sanitize.test.ts b/src/__tests__/sanitize.test.ts
new file mode 100644
index 0000000..550ecd0
--- /dev/null
+++ b/src/__tests__/sanitize.test.ts
@@ -0,0 +1,24 @@
+import { describe, it, expect } from "vitest";
+import { sanitizeFilename } from "../utils/sanitize.js";
+
+describe("sanitizeFilename", () => {
+ it("passes normal filename through", () => {
+ expect(sanitizeFilename("report.pdf")).toBe("report.pdf");
+ });
+ it("replaces control characters", () => {
+ expect(sanitizeFilename("file\x00name.pdf")).toBe("file_name.pdf");
+ });
+ it("replaces quotes", () => {
+ expect(sanitizeFilename('file"name.pdf')).toBe("file_name.pdf");
+ });
+ it("returns default for empty string", () => {
+ expect(sanitizeFilename("")).toBe("document.pdf");
+ });
+ it("truncates to 200 characters", () => {
+ const long = "a".repeat(250) + ".pdf";
+ expect(sanitizeFilename(long).length).toBeLessThanOrEqual(200);
+ });
+ it("supports custom default name", () => {
+ expect(sanitizeFilename("", "invoice.pdf")).toBe("invoice.pdf");
+ });
+});
diff --git a/src/__tests__/templates.test.ts b/src/__tests__/templates.test.ts
new file mode 100644
index 0000000..52ca00b
--- /dev/null
+++ b/src/__tests__/templates.test.ts
@@ -0,0 +1,57 @@
+import { describe, it, expect } from "vitest";
+import { renderTemplate, templates } from "../services/templates.js";
+
+// Access esc via rendering — test that HTML entities are escaped in output
+describe("Template rendering", () => {
+ it("throws for unknown template", () => {
+ expect(() => renderTemplate("nonexistent", {})).toThrow("not found");
+ });
+
+ it("invoice renders with correct totals", () => {
+ const html = renderTemplate("invoice", {
+ invoiceNumber: "INV-001",
+ date: "2026-01-01",
+ from: { name: "Seller" },
+ to: { name: "Buyer" },
+ items: [{ description: "Widget", quantity: 2, unitPrice: 10, taxRate: 20 }],
+ });
+ expect(html).toContain("INV-001");
+ expect(html).toContain("€20.00"); // subtotal: 2*10
+ expect(html).toContain("€4.00"); // tax: 20*0.2
+ expect(html).toContain("€24.00"); // total
+ });
+
+ it("receipt renders with correct total", () => {
+ const html = renderTemplate("receipt", {
+ receiptNumber: "R-001",
+ date: "2026-01-01",
+ from: { name: "Shop" },
+ items: [{ description: "Item A", amount: 15 }, { description: "Item B", amount: 25 }],
+ });
+ expect(html).toContain("R-001");
+ expect(html).toContain("€40.00");
+ });
+
+ it("defaults currency to €", () => {
+ const html = renderTemplate("invoice", {
+ invoiceNumber: "X", date: "2026-01-01",
+ from: { name: "A" }, to: { name: "B" },
+ items: [{ description: "Test", quantity: 1, unitPrice: 5 }],
+ });
+ expect(html).toContain("€5.00");
+ });
+
+ it("escapes HTML entities including single quotes", () => {
+ const html = renderTemplate("invoice", {
+ invoiceNumber: '',
+ date: "2026-01-01",
+ from: { name: "O'Brien & Co" },
+ to: { name: "Bob" },
+ items: [{ description: "Test", quantity: 1, unitPrice: 1 }],
+ });
+ expect(html).not.toContain("