import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import ts from 'typescript'; const source = await readFile(new URL('./client.ts', import.meta.url), 'utf8'); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 }, }).outputText; const { ApiClientBase, AuthRecoveryError, AuthSessionExpiredError, ApiNetworkError, } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`); class MemoryStorage { #values = new Map(); getItem(key) { return this.#values.get(key) ?? null; } setItem(key, value) { this.#values.set(key, String(value)); } removeItem(key) { this.#values.delete(key); } clear() { this.#values.clear(); } } function installBrowserGlobals() { const storage = new MemoryStorage(); const target = new EventTarget(); target.setTimeout = setTimeout; target.clearTimeout = clearTimeout; globalThis.window = target; globalThis.localStorage = storage; if (typeof globalThis.CustomEvent === 'undefined') { globalThis.CustomEvent = class CustomEvent extends Event { constructor(type, init = {}) { super(type); this.detail = init.detail; } }; } return storage; } function jsonResponse(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } test('parallel 401 responses share one refresh and both requests resume', async (t) => { const originalFetch = globalThis.fetch; t.after(() => { globalThis.fetch = originalFetch; }); const storage = installBrowserGlobals(); storage.setItem('access_token', 'expired-access'); storage.setItem('refresh_token', 'valid-refresh'); let refreshCalls = 0; globalThis.fetch = async (url, options = {}) => { if (url === '/api/auth/token/refresh/') { refreshCalls += 1; await new Promise((resolve) => setTimeout(resolve, 10)); return jsonResponse({ access: 'fresh-access' }); } if (url === '/resource') { return options.headers.Authorization === 'Bearer fresh-access' ? jsonResponse({ ok: true }) : jsonResponse({ detail: 'expired' }, 401); } throw new Error(`Unexpected URL: ${url}`); }; const client = new ApiClientBase(); const [first, second] = await Promise.all([ client.request('/resource'), client.request('/resource'), ]); assert.deepEqual(first, { ok: true }); assert.deepEqual(second, { ok: true }); assert.equal(refreshCalls, 1); assert.equal(storage.getItem('access_token'), 'fresh-access'); }); test('transient refresh failure rejects every waiter and preserves the session', async (t) => { const originalFetch = globalThis.fetch; t.after(() => { globalThis.fetch = originalFetch; }); const storage = installBrowserGlobals(); storage.setItem('access_token', 'expired-access'); storage.setItem('refresh_token', 'valid-refresh'); let refreshCalls = 0; globalThis.fetch = async (url) => { if (url === '/api/auth/token/refresh/') { refreshCalls += 1; await new Promise((resolve) => setTimeout(resolve, 10)); return jsonResponse({ detail: 'temporary outage' }, 502); } return jsonResponse({ detail: 'expired' }, 401); }; const client = new ApiClientBase(); const settled = await Promise.race([ Promise.allSettled([client.request('/one'), client.request('/two')]), new Promise((resolve) => setTimeout(() => resolve('still-pending'), 500)), ]); assert.notEqual(settled, 'still-pending'); assert.equal(refreshCalls, 1); for (const result of settled) { assert.equal(result.status, 'rejected'); assert.ok(result.reason instanceof AuthRecoveryError); } assert.equal(storage.getItem('access_token'), 'expired-access'); assert.equal(storage.getItem('refresh_token'), 'valid-refresh'); }); test('confirmed invalid refresh clears tokens and expires the session', async (t) => { const originalFetch = globalThis.fetch; t.after(() => { globalThis.fetch = originalFetch; }); const storage = installBrowserGlobals(); storage.setItem('access_token', 'expired-access'); storage.setItem('refresh_token', 'invalid-refresh'); globalThis.fetch = async (url) => { if (url === '/api/auth/token/refresh/') { return jsonResponse({ detail: 'token_not_valid' }, 401); } return jsonResponse({ detail: 'expired' }, 401); }; const client = new ApiClientBase(); await assert.rejects(client.request('/resource'), AuthSessionExpiredError); assert.equal(storage.getItem('access_token'), null); assert.equal(storage.getItem('refresh_token'), null); }); test('request timeout terminates the request without deleting tokens', async (t) => { const originalFetch = globalThis.fetch; t.after(() => { globalThis.fetch = originalFetch; }); const storage = installBrowserGlobals(); storage.setItem('access_token', 'access'); storage.setItem('refresh_token', 'refresh'); globalThis.fetch = async (_url, options = {}) => new Promise((_resolve, reject) => { options.signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError'))); }); const client = new ApiClientBase(); await assert.rejects( client.request('/slow', { timeoutMs: 20 }), ApiNetworkError, ); assert.equal(storage.getItem('access_token'), 'access'); assert.equal(storage.getItem('refresh_token'), 'refresh'); }); test('browser SSO logout does not depend on JWT and includes its cookie', async (t) => { const originalFetch = globalThis.fetch; t.after(() => { globalThis.fetch = originalFetch; }); const storage = installBrowserGlobals(); storage.setItem('access_token', 'possibly-expired-access'); storage.setItem('refresh_token', 'possibly-expired-refresh'); const requests = []; globalThis.fetch = async (url, options = {}) => { requests.push({ url, options }); return new Response(null, { status: 204 }); }; const client = new ApiClientBase(); await client.logoutBrowserSession(); assert.equal(requests.length, 1); assert.equal(requests[0].url, '/api/auth/browser-session/logout/'); assert.equal(requests[0].options.method, 'POST'); assert.equal(requests[0].options.credentials, 'include'); assert.equal(requests[0].options.headers?.Authorization, undefined); assert.equal(storage.getItem('access_token'), 'possibly-expired-access'); assert.equal(storage.getItem('refresh_token'), 'possibly-expired-refresh'); });