manual-api-client-flow.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // scripts/manual-api-client-flow.mjs
  2. /**
  3. * Manual API verification script for RHL-008.
  4. *
  5. * What this does:
  6. * - Uses the frontend apiClient helpers against a real running app instance.
  7. * - Implements a tiny cookie jar so login sessions work in Node (fetch has no cookie jar).
  8. * - Runs a happy-path flow:
  9. * login -> branches -> years -> months -> days -> files -> logout
  10. * - Also runs a few negative tests (401/403/400/404).
  11. *
  12. * Usage:
  13. * node scripts/manual-api-client-flow.mjs \
  14. * --baseUrl=http://localhost:3000 \
  15. * --username=branchuser \
  16. * --password=secret-password \
  17. * --branch=NL01
  18. */
  19. import {
  20. apiFetch,
  21. login,
  22. logout,
  23. getBranches,
  24. getYears,
  25. getMonths,
  26. getDays,
  27. getFiles,
  28. ApiClientError,
  29. } from "../lib/frontend/apiClient.js";
  30. function getArg(name, fallback = null) {
  31. const prefix = `--${name}=`;
  32. const hit = process.argv.find((a) => a.startsWith(prefix));
  33. if (!hit) return fallback;
  34. return hit.slice(prefix.length);
  35. }
  36. function pickLatest(items) {
  37. if (!Array.isArray(items) || items.length === 0) return null;
  38. // Backend currently returns ascending; "latest" is the last one.
  39. return items[items.length - 1];
  40. }
  41. function logStep(title) {
  42. console.log(`\n=== ${title} ===`);
  43. }
  44. /**
  45. * Minimal cookie jar:
  46. * - Extracts auth_session from Set-Cookie
  47. * - Sends it back via Cookie header on subsequent requests
  48. */
  49. function createCookieJarFetch() {
  50. const jar = new Map(); // cookieName -> cookieValue
  51. return async function cookieFetch(url, init = {}) {
  52. const headers = new Headers(init.headers || {});
  53. // Attach cookies (only what we stored).
  54. if (jar.size > 0) {
  55. const cookieHeader = Array.from(jar.entries())
  56. .map(([k, v]) => `${k}=${v}`)
  57. .join("; ");
  58. headers.set("cookie", cookieHeader);
  59. }
  60. const res = await fetch(url, { ...init, headers });
  61. // Node/undici provides getSetCookie() in many versions. Fallback to get('set-cookie').
  62. const setCookies =
  63. typeof res.headers.getSetCookie === "function"
  64. ? res.headers.getSetCookie()
  65. : res.headers.get("set-cookie")
  66. ? [res.headers.get("set-cookie")]
  67. : [];
  68. for (const sc of setCookies) {
  69. if (!sc) continue;
  70. // We only need auth_session for this project.
  71. const match = sc.match(/(?:^|;\s*)auth_session=([^;]+)/);
  72. if (match && match[1]) {
  73. jar.set("auth_session", match[1]);
  74. }
  75. }
  76. return res;
  77. };
  78. }
  79. async function expectApiError(promise, { status, code }) {
  80. try {
  81. await promise;
  82. console.error("Expected an error, but the call succeeded.");
  83. process.exitCode = 1;
  84. } catch (err) {
  85. if (!(err instanceof ApiClientError)) {
  86. console.error("Expected ApiClientError, got:", err);
  87. process.exitCode = 1;
  88. return;
  89. }
  90. console.log("Got expected error:", {
  91. status: err.status,
  92. code: err.code,
  93. message: err.message,
  94. });
  95. if (status !== undefined && err.status !== status) {
  96. console.error(`Expected status=${status}, got ${err.status}`);
  97. process.exitCode = 1;
  98. }
  99. if (code !== undefined && err.code !== code) {
  100. console.error(`Expected code=${code}, got ${err.code}`);
  101. process.exitCode = 1;
  102. }
  103. }
  104. }
  105. async function main() {
  106. const baseUrl = getArg("baseUrl", "http://localhost:3000");
  107. const username = getArg("username");
  108. const password = getArg("password");
  109. const preferredBranch = getArg("branch"); // optional
  110. if (!username || !password) {
  111. console.error(
  112. "Missing credentials. Provide --username=... and --password=..."
  113. );
  114. process.exit(1);
  115. }
  116. const fetchImpl = createCookieJarFetch();
  117. logStep("NEGATIVE: Unauthenticated call should be 401");
  118. await expectApiError(getBranches({ baseUrl, fetchImpl }), {
  119. status: 401,
  120. code: "AUTH_UNAUTHENTICATED",
  121. });
  122. logStep("LOGIN");
  123. const loginRes = await login({ username, password }, { baseUrl, fetchImpl });
  124. console.log("login:", loginRes);
  125. logStep("OPTIONAL: /api/auth/me (if implemented)");
  126. try {
  127. const me = await apiFetch("/api/auth/me", { baseUrl, fetchImpl });
  128. console.log("me:", me);
  129. } catch (err) {
  130. // If the endpoint doesn't exist, Next may return HTML 404 -> CLIENT_HTTP_ERROR.
  131. console.log("me endpoint not available (ok):", {
  132. status: err?.status,
  133. code: err?.code,
  134. });
  135. }
  136. logStep("GET BRANCHES");
  137. const branchesRes = await getBranches({ baseUrl, fetchImpl });
  138. console.log("branches:", branchesRes);
  139. const branch =
  140. preferredBranch ||
  141. (Array.isArray(branchesRes?.branches) ? branchesRes.branches[0] : null);
  142. if (!branch) {
  143. console.error("No branch found. Check NAS mount / fixtures.");
  144. process.exit(1);
  145. }
  146. logStep("NEGATIVE: Forbidden branch access should be 403 (branch users)");
  147. // Even if only one branch exists, NL99 should trigger forbidden for branch role.
  148. await expectApiError(getYears("NL99", { baseUrl, fetchImpl }), {
  149. status: 403,
  150. code: "AUTH_FORBIDDEN_BRANCH",
  151. });
  152. logStep(`GET YEARS for ${branch}`);
  153. const yearsRes = await getYears(branch, { baseUrl, fetchImpl });
  154. console.log("years:", yearsRes);
  155. const year = pickLatest(yearsRes?.years);
  156. if (!year) {
  157. console.error("No year folders found for branch:", branch);
  158. process.exit(1);
  159. }
  160. logStep("NEGATIVE: Not found year should be 404 (authorized)");
  161. await expectApiError(getMonths(branch, "2099", { baseUrl, fetchImpl }), {
  162. status: 404,
  163. code: "FS_NOT_FOUND",
  164. });
  165. logStep(`GET MONTHS for ${branch}/${year}`);
  166. const monthsRes = await getMonths(branch, year, { baseUrl, fetchImpl });
  167. console.log("months:", monthsRes);
  168. const month = pickLatest(monthsRes?.months);
  169. if (!month) {
  170. console.error("No month folders found for:", branch, year);
  171. process.exit(1);
  172. }
  173. logStep(`GET DAYS for ${branch}/${year}/${month}`);
  174. const daysRes = await getDays(branch, year, month, { baseUrl, fetchImpl });
  175. console.log("days:", daysRes);
  176. const day = pickLatest(daysRes?.days);
  177. if (!day) {
  178. console.error("No day folders found for:", branch, year, month);
  179. process.exit(1);
  180. }
  181. logStep(
  182. "NEGATIVE: Validation error on /api/files (missing query params) -> 400"
  183. );
  184. await expectApiError(apiFetch("/api/files", { baseUrl, fetchImpl }), {
  185. status: 400,
  186. code: "VALIDATION_MISSING_QUERY",
  187. });
  188. logStep(`GET FILES for ${branch}/${year}/${month}/${day}`);
  189. const filesRes = await getFiles(branch, year, month, day, {
  190. baseUrl,
  191. fetchImpl,
  192. });
  193. console.log("files:", filesRes);
  194. logStep("LOGOUT");
  195. const logoutRes = await logout({ baseUrl, fetchImpl });
  196. console.log("logout:", logoutRes);
  197. console.log("\nDone.");
  198. }
  199. main().catch((err) => {
  200. console.error("Script failed:", err);
  201. process.exit(1);
  202. });