diff --git a/frontend/src/lib/api/endpoints/auth.test.ts b/frontend/src/lib/api/endpoints/auth.test.ts index 2fae11a7..648f9c6d 100644 --- a/frontend/src/lib/api/endpoints/auth.test.ts +++ b/frontend/src/lib/api/endpoints/auth.test.ts @@ -35,7 +35,10 @@ import { __resetOpaqueParamsCache } from './opaque'; const f = apiFetch as unknown as ReturnType; const j = apiJson as unknown as ReturnType; // Several auth probes use the raw global fetch (NOT apiFetch) on purpose. -const okRes = { ok: true, status: 200, json: async () => ({}) }; +// `headers: new Headers()` matches the real `fetch()` contract — several +// probes (fetchMe, tryRefresh) run through the DPoP nonce-update shim, +// which calls `response.headers.get('DPoP-Nonce')` on every reply. +const okRes = { ok: true, status: 200, headers: new Headers(), json: async () => ({}) }; beforeEach(() => { vi.clearAllMocks(); // login() dynamically imports the OPAQUE client and calls @@ -63,16 +66,23 @@ it('exercises the auth endpoints (success paths)', async () => { expect(fc + f.mock.calls.length).toBeGreaterThan(3); }); it('fetchMe returns null when the probe is not ok', async () => { + // `headers: new Headers()` matches real `fetch()` — the DPoP-aware + // path calls `response.headers.get('DPoP-Nonce')` on every reply + // and would crash on a bare `{ok, status, json}` mock. vi.stubGlobal( 'fetch', - vi.fn().mockResolvedValue({ ok: false, status: 401, json: async () => ({}) }) + vi + .fn() + .mockResolvedValue({ ok: false, status: 401, headers: new Headers(), json: async () => ({}) }) ); await expect(auth.fetchMe()).resolves.toBeNull(); }); it('tryRefresh returns false when the refresh fails', async () => { vi.stubGlobal( 'fetch', - vi.fn().mockResolvedValue({ ok: false, status: 401, json: async () => ({}) }) + vi + .fn() + .mockResolvedValue({ ok: false, status: 401, headers: new Headers(), json: async () => ({}) }) ); await expect(auth.tryRefresh()).resolves.toBe(false); }); diff --git a/frontend/src/lib/api/endpoints/files.ts b/frontend/src/lib/api/endpoints/files.ts index b233e4b5..245beb99 100644 --- a/frontend/src/lib/api/endpoints/files.ts +++ b/frontend/src/lib/api/endpoints/files.ts @@ -129,7 +129,10 @@ export async function uploadFileWithProgress( // Nonce challenge → surface a distinctive rejection so the outer // retry can re-arm a fresh XHR (the current one has already // consumed its request body). - if (xhr.status === 401 && /use_dpop_nonce/i.test(xhr.getResponseHeader('WWW-Authenticate') ?? '')) { + if ( + xhr.status === 401 && + /use_dpop_nonce/i.test(xhr.getResponseHeader('WWW-Authenticate') ?? '') + ) { const err = new Error('dpop_nonce_challenge') as Error & { isNonceChallenge?: boolean }; err.isNonceChallenge = true; reject(err);