فهرست منبع

RHL-015 feat(api): add PDF URL helpers and tests for stream and download functionality

Code_Uwe 4 هفته پیش
والد
کامیت
15a80b473e
2فایلهای تغییر یافته به همراه186 افزوده شده و 0 حذف شده
  1. 98 0
      lib/frontend/explorer/pdfUrl.js
  2. 88 0
      lib/frontend/explorer/pdfUrl.test.js

+ 98 - 0
lib/frontend/explorer/pdfUrl.js

@@ -0,0 +1,98 @@
+/**
+ * PDF URL helpers for the Explorer (RHL-023).
+ *
+ * Why this file exists:
+ * - The PDF endpoint is a binary stream and must NOT be called via apiClient (JSON-centric).
+ * - We want centralized, testable URL construction for:
+ *   GET /api/files/:branch/:year/:month/:day/:filename
+ *
+ * Design goals:
+ * - Pure functions (no React, no window, no Next runtime).
+ * - Defensive encoding for filename (spaces, #, unicode, etc.).
+ * - Minimal, predictable behavior.
+ */
+
+/**
+ * Ensure a required string input is present and not empty/whitespace.
+ *
+ * Note:
+ * - We validate emptiness using trim(), but we may return the original value
+ *   to avoid mutating semantics (important for filenames).
+ *
+ * @param {string} name
+ * @param {unknown} value
+ * @returns {string}
+ */
+function requireNonEmptyString(name, value) {
+	if (typeof value !== "string") {
+		throw new Error(`Route segment "${name}" must be a string`);
+	}
+
+	if (!value.trim()) {
+		throw new Error(`Route segment "${name}" must not be empty`);
+	}
+
+	return value;
+}
+
+/**
+ * Encode a standard route segment (branch/year/month/day).
+ * - We normalize by trimming.
+ *
+ * @param {string} name
+ * @param {unknown} value
+ * @returns {string}
+ */
+function encodeSegment(name, value) {
+	return encodeURIComponent(requireNonEmptyString(name, value).trim());
+}
+
+/**
+ * Encode a filename segment.
+ * - We do NOT trim the filename (filenames are filesystem-exact).
+ * - We still validate that it isn't empty/whitespace.
+ *
+ * @param {unknown} value
+ * @returns {string}
+ */
+function encodeFilename(value) {
+	return encodeURIComponent(requireNonEmptyString("filename", value));
+}
+
+/**
+ * Build the PDF stream URL (inline by default).
+ *
+ * @param {{
+ *   branch: string,
+ *   year: string,
+ *   month: string,
+ *   day: string,
+ *   filename: string
+ * }} input
+ * @returns {string}
+ */
+export function buildPdfUrl({ branch, year, month, day, filename }) {
+	const b = encodeSegment("branch", branch);
+	const y = encodeSegment("year", year);
+	const m = encodeSegment("month", month);
+	const d = encodeSegment("day", day);
+	const f = encodeFilename(filename);
+
+	return `/api/files/${b}/${y}/${m}/${d}/${f}`;
+}
+
+/**
+ * Build the PDF download URL (forces Content-Disposition: attachment).
+ *
+ * @param {{
+ *   branch: string,
+ *   year: string,
+ *   month: string,
+ *   day: string,
+ *   filename: string
+ * }} input
+ * @returns {string}
+ */
+export function buildPdfDownloadUrl(input) {
+	return `${buildPdfUrl(input)}?download=1`;
+}

+ 88 - 0
lib/frontend/explorer/pdfUrl.test.js

@@ -0,0 +1,88 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import { buildPdfUrl, buildPdfDownloadUrl } from "./pdfUrl.js";
+
+describe("lib/frontend/explorer/pdfUrl", () => {
+	it("builds the expected PDF stream URL", () => {
+		const url = buildPdfUrl({
+			branch: "NL01",
+			year: "2024",
+			month: "10",
+			day: "23",
+			filename: "test.pdf",
+		});
+
+		expect(url).toBe("/api/files/NL01/2024/10/23/test.pdf");
+	});
+
+	it("encodes filenames with spaces and special characters (#, &, +, %)", () => {
+		const filename = "Lieferschein #1 & Co +100%.pdf";
+
+		const url = buildPdfUrl({
+			branch: "NL01",
+			year: "2024",
+			month: "10",
+			day: "23",
+			filename,
+		});
+
+		expect(url).toBe(
+			"/api/files/NL01/2024/10/23/Lieferschein%20%231%20%26%20Co%20%2B100%25.pdf"
+		);
+	});
+
+	it("encodes unicode filenames safely", () => {
+		const filename = "Müller Übergabe.pdf";
+
+		const url = buildPdfUrl({
+			branch: "NL01",
+			year: "2024",
+			month: "10",
+			day: "23",
+			filename,
+		});
+
+		// We intentionally compare against encodeURIComponent behavior
+		// to avoid hand-maintaining unicode encodings.
+		const expected = `/api/files/NL01/2024/10/23/${encodeURIComponent(
+			filename
+		)}`;
+
+		expect(url).toBe(expected);
+	});
+
+	it("builds the download URL with ?download=1", () => {
+		const url = buildPdfDownloadUrl({
+			branch: "NL01",
+			year: "2024",
+			month: "10",
+			day: "23",
+			filename: "test.pdf",
+		});
+
+		expect(url).toBe("/api/files/NL01/2024/10/23/test.pdf?download=1");
+	});
+
+	it("throws for missing or empty segments", () => {
+		expect(() =>
+			buildPdfUrl({
+				branch: "NL01",
+				year: "2024",
+				month: "10",
+				day: "23",
+				filename: "",
+			})
+		).toThrow(/must not be empty/i);
+
+		expect(() =>
+			buildPdfUrl({
+				branch: "",
+				year: "2024",
+				month: "10",
+				day: "23",
+				filename: "test.pdf",
+			})
+		).toThrow(/branch/i);
+	});
+});