| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- /**
- * 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);
- });
|