diff --git a/apps/portal/scripts/stubs/core-api.ts b/apps/portal/scripts/stubs/core-api.ts new file mode 100644 index 00000000..b1a5efd9 --- /dev/null +++ b/apps/portal/scripts/stubs/core-api.ts @@ -0,0 +1,16 @@ +export type PostCall = [path: string, options?: unknown]; + +export const postCalls: PostCall[] = []; + +export const apiClient = { + POST: async (path: string, options?: unknown) => { + postCalls.push([path, options]); + return { data: null } as const; + }, + GET: async () => ({ data: null } as const), + PUT: async () => ({ data: null } as const), + PATCH: async () => ({ data: null } as const), + DELETE: async () => ({ data: null } as const), +}; + +export const configureApiClientAuth = () => undefined; diff --git a/apps/portal/scripts/test-request-password-reset.cjs b/apps/portal/scripts/test-request-password-reset.cjs new file mode 100755 index 00000000..cc35a88c --- /dev/null +++ b/apps/portal/scripts/test-request-password-reset.cjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); +const path = require("node:path"); +const Module = require("node:module"); +const ts = require("typescript"); + +const srcRoot = path.join(path.resolve(__dirname, ".."), "src"); + +const registerTsCompiler = extension => { + require.extensions[extension] = (module, filename) => { + const source = fs.readFileSync(filename, "utf8"); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + jsx: ts.JsxEmit.ReactJSX, + esModuleInterop: true, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + resolveJsonModule: true, + skipLibCheck: true, + }, + fileName: filename, + }); + + module._compile(outputText, filename); + }; +}; + +registerTsCompiler(".ts"); +registerTsCompiler(".tsx"); + +const originalResolveFilename = Module._resolveFilename; +Module._resolveFilename = function resolve(request, parent, isMain, options) { + if (request === "@/core/api") { + const stubPath = path.resolve(__dirname, "stubs/core-api.ts"); + return originalResolveFilename.call(this, stubPath, parent, isMain, options); + } + + if (request.startsWith("@/")) { + const resolved = path.resolve(srcRoot, request.slice(2)); + return originalResolveFilename.call(this, resolved, parent, isMain, options); + } + + return originalResolveFilename.call(this, request, parent, isMain, options); +}; + +class LocalStorageMock { + constructor() { + this._store = new Map(); + } + + clear() { + this._store.clear(); + } + + getItem(key) { + return this._store.has(key) ? this._store.get(key) : null; + } + + key(index) { + return Array.from(this._store.keys())[index] ?? null; + } + + removeItem(key) { + this._store.delete(key); + } + + setItem(key, value) { + this._store.set(key, String(value)); + } + + get length() { + return this._store.size; + } +} + +global.localStorage = new LocalStorageMock(); + +const coreApiStub = require("./stubs/core-api.ts"); +coreApiStub.postCalls.length = 0; + +const { useAuthStore } = require("../src/features/auth/services/auth.store.ts"); + +(async () => { + try { + const payload = { email: "tester@example.com" }; + await useAuthStore.getState().requestPasswordReset(payload); + + if (coreApiStub.postCalls.length !== 1) { + throw new Error(`Expected 1 POST call, received ${coreApiStub.postCalls.length}`); + } + + const [endpoint, options] = coreApiStub.postCalls[0]; + if (endpoint !== "/auth/request-password-reset") { + throw new Error(`Expected endpoint \"/auth/request-password-reset\" but received \"${endpoint}\"`); + } + + if (!options || typeof options !== "object") { + throw new Error("Password reset request did not include options payload"); + } + + const body = options.body; + if (!body || typeof body !== "object") { + throw new Error("Password reset request did not include a body"); + } + + if (body.email !== payload.email) { + throw new Error( + `Expected request body email to be \"${payload.email}\" but received \"${body.email}\"` + ); + } + + console.log("Password reset request forwarded correctly:", { endpoint, body }); + } catch (error) { + console.error("Password reset request verification failed:", error); + process.exitCode = 1; + } +})(); diff --git a/apps/portal/src/features/auth/hooks/useAuth.ts b/apps/portal/src/features/auth/hooks/useAuth.ts index 4d8b0335..db65e683 100644 --- a/apps/portal/src/features/auth/hooks/useAuth.ts +++ b/apps/portal/src/features/auth/hooks/useAuth.ts @@ -53,8 +53,8 @@ export function useAuth() { // Password reset request const passwordResetMutation = useMutation({ - mutationFn: (data: ForgotPasswordRequest) => - apiClient.POST('/auth/forgot-password', { body: data }), + mutationFn: (data: ForgotPasswordRequest) => + apiClient.POST('/auth/request-password-reset', { body: data }), }); // Password reset diff --git a/apps/portal/src/features/auth/services/auth.store.ts b/apps/portal/src/features/auth/services/auth.store.ts index 25c574a6..21e38c94 100644 --- a/apps/portal/src/features/auth/services/auth.store.ts +++ b/apps/portal/src/features/auth/services/auth.store.ts @@ -209,7 +209,7 @@ export const useAuthStore = create()( requestPasswordReset: async (data: ForgotPasswordRequest) => { set({ loading: true, error: null }); try { - await apiClient.POST('/auth/forgot-password', { body: data }); + await apiClient.POST('/auth/request-password-reset', { body: data }); set({ loading: false }); } catch (error) { const authError = error as AuthError;