/** * 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 * as apiClientNs from "../lib/frontend/apiClient.js"; /** * Compatibility layer: * - If apiClient.js is loaded as CommonJS, Node exposes it as { default: module.exports }. * - If apiClient.js is loaded as ESM, Node exposes named exports directly. */ const apiClient = apiClientNs.default ?? apiClientNs; const { apiFetch, login, logout, getBranches, getYears, getMonths, getDays, getFiles, ApiClientError, } = apiClient; 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); });