fix(19): correct admin reset-password client URL (AUTH-LOCAL-08 blocker)

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) <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-17 19:58:17 -04:00
co-authored by Claude Opus 4.8
parent 9b10d875d4
commit 53da4be62b
2 changed files with 34 additions and 3 deletions
+30
View File
@@ -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' }),
);
});
});
+4 -3
View File
@@ -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<void> {
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',