Parcourir la source

RHL-008-feat(api): add manual API verification script for frontend testing

Code_Uwe il y a 21 heures
Parent
commit
1f8e37d952
1 fichiers modifiés avec 239 ajouts et 0 suppressions
  1. 239 0
      scripts/manual-api-client-flow.mjs

+ 239 - 0
scripts/manual-api-client-flow.mjs

@@ -0,0 +1,239 @@
+// scripts/manual-api-client-flow.mjs
+/**
+ * Manual API verification script for RHL-008.
+ *
+ * What this does:
+ * - Uses the frontend apiClient helpers against a real running app instance.
+ * - Implements a tiny cookie jar so login sessions work in Node (fetch has no cookie jar).
+ * - Runs a happy-path flow:
+ *   login -> branches -> years -> months -> days -> files -> logout
+ * - Also runs a few negative tests (401/403/400/404).
+ *
+ * Usage:
+ *   node scripts/manual-api-client-flow.mjs \
+ *     --baseUrl=http://localhost:3000 \
+ *     --username=branchuser \
+ *     --password=secret-password \
+ *     --branch=NL01
+ */
+
+import {
+	apiFetch,
+	login,
+	logout,
+	getBranches,
+	getYears,
+	getMonths,
+	getDays,
+	getFiles,
+	ApiClientError,
+} from "../lib/frontend/apiClient.js";
+
+function getArg(name, fallback = null) {
+	const prefix = `--${name}=`;
+	const hit = process.argv.find((a) => a.startsWith(prefix));
+	if (!hit) return fallback;
+	return hit.slice(prefix.length);
+}
+
+function pickLatest(items) {
+	if (!Array.isArray(items) || items.length === 0) return null;
+	// Backend currently returns ascending; "latest" is the last one.
+	return items[items.length - 1];
+}
+
+function logStep(title) {
+	console.log(`\n=== ${title} ===`);
+}
+
+/**
+ * Minimal cookie jar:
+ * - Extracts auth_session from Set-Cookie
+ * - Sends it back via Cookie header on subsequent requests
+ */
+function createCookieJarFetch() {
+	const jar = new Map(); // cookieName -> cookieValue
+
+	return async function cookieFetch(url, init = {}) {
+		const headers = new Headers(init.headers || {});
+
+		// Attach cookies (only what we stored).
+		if (jar.size > 0) {
+			const cookieHeader = Array.from(jar.entries())
+				.map(([k, v]) => `${k}=${v}`)
+				.join("; ");
+			headers.set("cookie", cookieHeader);
+		}
+
+		const res = await fetch(url, { ...init, headers });
+
+		// Node/undici provides getSetCookie() in many versions. Fallback to get('set-cookie').
+		const setCookies =
+			typeof res.headers.getSetCookie === "function"
+				? res.headers.getSetCookie()
+				: res.headers.get("set-cookie")
+				? [res.headers.get("set-cookie")]
+				: [];
+
+		for (const sc of setCookies) {
+			if (!sc) continue;
+
+			// We only need auth_session for this project.
+			const match = sc.match(/(?:^|;\s*)auth_session=([^;]+)/);
+			if (match && match[1]) {
+				jar.set("auth_session", match[1]);
+			}
+		}
+
+		return res;
+	};
+}
+
+async function expectApiError(promise, { status, code }) {
+	try {
+		await promise;
+		console.error("Expected an error, but the call succeeded.");
+		process.exitCode = 1;
+	} catch (err) {
+		if (!(err instanceof ApiClientError)) {
+			console.error("Expected ApiClientError, got:", err);
+			process.exitCode = 1;
+			return;
+		}
+
+		console.log("Got expected error:", {
+			status: err.status,
+			code: err.code,
+			message: err.message,
+		});
+
+		if (status !== undefined && err.status !== status) {
+			console.error(`Expected status=${status}, got ${err.status}`);
+			process.exitCode = 1;
+		}
+		if (code !== undefined && err.code !== code) {
+			console.error(`Expected code=${code}, got ${err.code}`);
+			process.exitCode = 1;
+		}
+	}
+}
+
+async function main() {
+	const baseUrl = getArg("baseUrl", "http://localhost:3000");
+	const username = getArg("username");
+	const password = getArg("password");
+	const preferredBranch = getArg("branch"); // optional
+
+	if (!username || !password) {
+		console.error(
+			"Missing credentials. Provide --username=... and --password=..."
+		);
+		process.exit(1);
+	}
+
+	const fetchImpl = createCookieJarFetch();
+
+	logStep("NEGATIVE: Unauthenticated call should be 401");
+	await expectApiError(getBranches({ baseUrl, fetchImpl }), {
+		status: 401,
+		code: "AUTH_UNAUTHENTICATED",
+	});
+
+	logStep("LOGIN");
+	const loginRes = await login({ username, password }, { baseUrl, fetchImpl });
+	console.log("login:", loginRes);
+
+	logStep("OPTIONAL: /api/auth/me (if implemented)");
+	try {
+		const me = await apiFetch("/api/auth/me", { baseUrl, fetchImpl });
+		console.log("me:", me);
+	} catch (err) {
+		// If the endpoint doesn't exist, Next may return HTML 404 -> CLIENT_HTTP_ERROR.
+		console.log("me endpoint not available (ok):", {
+			status: err?.status,
+			code: err?.code,
+		});
+	}
+
+	logStep("GET BRANCHES");
+	const branchesRes = await getBranches({ baseUrl, fetchImpl });
+	console.log("branches:", branchesRes);
+
+	const branch =
+		preferredBranch ||
+		(Array.isArray(branchesRes?.branches) ? branchesRes.branches[0] : null);
+
+	if (!branch) {
+		console.error("No branch found. Check NAS mount / fixtures.");
+		process.exit(1);
+	}
+
+	logStep("NEGATIVE: Forbidden branch access should be 403 (branch users)");
+	// Even if only one branch exists, NL99 should trigger forbidden for branch role.
+	await expectApiError(getYears("NL99", { baseUrl, fetchImpl }), {
+		status: 403,
+		code: "AUTH_FORBIDDEN_BRANCH",
+	});
+
+	logStep(`GET YEARS for ${branch}`);
+	const yearsRes = await getYears(branch, { baseUrl, fetchImpl });
+	console.log("years:", yearsRes);
+
+	const year = pickLatest(yearsRes?.years);
+	if (!year) {
+		console.error("No year folders found for branch:", branch);
+		process.exit(1);
+	}
+
+	logStep("NEGATIVE: Not found year should be 404 (authorized)");
+	await expectApiError(getMonths(branch, "2099", { baseUrl, fetchImpl }), {
+		status: 404,
+		code: "FS_NOT_FOUND",
+	});
+
+	logStep(`GET MONTHS for ${branch}/${year}`);
+	const monthsRes = await getMonths(branch, year, { baseUrl, fetchImpl });
+	console.log("months:", monthsRes);
+
+	const month = pickLatest(monthsRes?.months);
+	if (!month) {
+		console.error("No month folders found for:", branch, year);
+		process.exit(1);
+	}
+
+	logStep(`GET DAYS for ${branch}/${year}/${month}`);
+	const daysRes = await getDays(branch, year, month, { baseUrl, fetchImpl });
+	console.log("days:", daysRes);
+
+	const day = pickLatest(daysRes?.days);
+	if (!day) {
+		console.error("No day folders found for:", branch, year, month);
+		process.exit(1);
+	}
+
+	logStep(
+		"NEGATIVE: Validation error on /api/files (missing query params) -> 400"
+	);
+	await expectApiError(apiFetch("/api/files", { baseUrl, fetchImpl }), {
+		status: 400,
+		code: "VALIDATION_MISSING_QUERY",
+	});
+
+	logStep(`GET FILES for ${branch}/${year}/${month}/${day}`);
+	const filesRes = await getFiles(branch, year, month, day, {
+		baseUrl,
+		fetchImpl,
+	});
+	console.log("files:", filesRes);
+
+	logStep("LOGOUT");
+	const logoutRes = await logout({ baseUrl, fetchImpl });
+	console.log("logout:", logoutRes);
+
+	console.log("\nDone.");
+}
+
+main().catch((err) => {
+	console.error("Script failed:", err);
+	process.exit(1);
+});