manual-api-client-flow.mjs 6.6 KB

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