From 53da4be62b29f77fbfd5118e641012f8ccc2b5c5 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 19:58:17 -0400 Subject: [PATCH] fix(19): correct admin reset-password client URL (AUTH-LOCAL-08 blocker) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VERIFICATION.md found a cross-layer URL mismatch: fetchAdminResetPassword POSTed to /api/admin/members/:id/reset-password but the API registers the route as /api/admin/members/:id/password (admin.ts), so the Admin reset sheet 404'd on every submit. Confirmed live: old path -> 404, correct path -> 400 (route reached). Unit tests missed it because API tests hit the real path directly and PWA tests mock the fetcher — no test crossed both layers. Fix the client URL and add a URL-contract regression test that pins the exact path (asserts fetch is called with /api/admin/members/:id/password). PWA 266/266 (+1), typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/pwa/src/api/client.test.ts | 30 ++++++++++++++++++++++++++++++ apps/pwa/src/api/client.ts | 7 ++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/pwa/src/api/client.test.ts b/apps/pwa/src/api/client.test.ts index 80d86eb..c4cae79 100644 --- a/apps/pwa/src/api/client.test.ts +++ b/apps/pwa/src/api/client.test.ts @@ -654,3 +654,33 @@ describe('calendarStore — delete/sync keys', () => { expect(state.lastSyncedUid).toBeNull(); }); }); + +// ── Phase 19 (AUTH-LOCAL-08): admin reset-password URL contract ─────────────── +// Regression guard for the post-merge blocker: client.ts targeted +// /members/:id/reset-password but the API registers /members/:id/password, so the +// Admin reset sheet 404'd in production. Unit tests on both sides missed it (API +// tests hit the real path directly; PWA tests mock the fetcher). Pin the exact URL. +describe('fetchAdminResetPassword — URL contract (Phase 19, AUTH-LOCAL-08)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('POSTs to /api/admin/members/:id/password (must match admin.ts route)', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + type: 'basic', + status: 200, + } as unknown as Response); + + const { fetchAdminResetPassword } = await import('./client.js'); + await fetchAdminResetPassword(7, 'new-password-123'); + + expect(fetch).toHaveBeenCalledWith( + '/api/admin/members/7/password', + expect.objectContaining({ method: 'POST' }), + ); + }); +}); diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts index 2ead9ba..7067342 100644 --- a/apps/pwa/src/api/client.ts +++ b/apps/pwa/src/api/client.ts @@ -194,14 +194,15 @@ export async function fetchCreateMember(body: { } /** - * POST /api/admin/members/:id/reset-password — admin reset of a member's password (Surface 11B). - * Admin-only; server enforces requireAdmin. + * POST /api/admin/members/:id/password — admin reset of a member's password (Surface 11B). + * Admin-only; server enforces requireAdmin. (Route is registered as `/members/:id/password` + * in apps/api/src/routes/admin.ts — must match exactly or the sheet 404s.) */ export async function fetchAdminResetPassword( memberId: number, newPassword: string, ): Promise { - const res = await fetch(`/api/admin/members/${memberId}/reset-password`, { + const res = await fetch(`/api/admin/members/${memberId}/password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',