import assert from 'node:assert/strict'; import test from 'node:test'; let importSequence = 0; class StorageMock { constructor(initial = {}) { this.values = new Map(Object.entries(initial)); this.removed = []; } getItem(key) { return this.values.has(key) ? this.values.get(key) : null; } setItem(key, value) { this.values.set(key, String(value)); } removeItem(key) { this.removed.push(key); this.values.delete(key); } } async function loadAuthModule() { importSequence += 1; return import(`../auth-session.js?test=${importSequence}`); } function installBrowserMocks() { globalThis.localStorage = new StorageMock({ token: 'устаревший-access-token', role: 'ADMIN', departmentId: '7', userId: '11', theme: 'dark' }); globalThis.sessionStorage = new StorageMock({ token: 'устаревший-session-token' }); } function jsonResponse(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); } test('login сохраняет access JWT только в памяти и очищает legacy Web Storage', async () => { installBrowserMocks(); const calls = []; globalThis.fetch = async (url, options) => { calls.push({ url, options }); return jsonResponse({ ok: true }); }; const auth = await loadAuthModule(); const state = auth.storeAuthState({ token: 'access-login', role: 'ADMIN', departmentId: 7, userId: 11, redirect: '/admin/' }); assert.equal(auth.getAccessToken(), 'access-login'); assert.deepEqual(state, { role: 'ADMIN', departmentId: '7', userId: '11', redirect: '/admin/' }); assert.equal(localStorage.getItem('token'), null); assert.equal(localStorage.getItem('role'), null); assert.equal(localStorage.getItem('theme'), 'dark'); await auth.fetchWithAuth('/api/test'); assert.equal(new Headers(calls[0].options.headers).get('Authorization'), 'Bearer access-login'); }); test('reload восстанавливает память через HttpOnly refresh-cookie', async () => { installBrowserMocks(); const calls = []; globalThis.fetch = async (url, options) => { calls.push({ url, options }); return jsonResponse({ token: 'access-after-reload', role: 'DEPARTMENT', departmentId: 4, userId: 15, redirect: '/admin/#department-workspace' }); }; const auth = await loadAuthModule(); const state = await auth.restoreSession(); assert.equal(calls.length, 1); assert.equal(calls[0].url, '/api/auth/refresh'); assert.equal(calls[0].options.method, 'POST'); assert.equal(calls[0].options.credentials, 'same-origin'); assert.equal(auth.getAccessToken(), 'access-after-reload'); assert.equal(state.role, 'DEPARTMENT'); }); test('401 запускает одну ротацию refresh и повторяет запрос с новым access JWT', async () => { installBrowserMocks(); const calls = []; globalThis.fetch = async (url, options) => { calls.push({ url, options }); if (calls.length === 1) return jsonResponse({ message: 'Истёк токен' }, 401); if (url === '/api/auth/refresh') { return jsonResponse({ token: 'access-rotated', role: 'ADMIN' }); } return jsonResponse({ success: true }); }; const auth = await loadAuthModule(); auth.storeAuthState({ token: 'access-old', role: 'ADMIN' }); const response = await auth.fetchWithAuth('/api/protected'); assert.equal(response.status, 200); assert.deepEqual(calls.map(call => call.url), [ '/api/protected', '/api/auth/refresh', '/api/protected' ]); assert.equal(new Headers(calls[0].options.headers).get('Authorization'), 'Bearer access-old'); assert.equal(new Headers(calls[2].options.headers).get('Authorization'), 'Bearer access-rotated'); }); test('параллельные запросы используют одну refresh-ротацию', async () => { installBrowserMocks(); let refreshCalls = 0; let releaseRefresh; const refreshGate = new Promise(resolve => { releaseRefresh = resolve; }); globalThis.fetch = async url => { if (url === '/api/auth/refresh') { refreshCalls += 1; await refreshGate; return jsonResponse({ token: 'access-shared', role: 'ADMIN' }); } return jsonResponse({ success: true }); }; const auth = await loadAuthModule(); const first = auth.refreshAccessToken(); const second = auth.refreshAccessToken(); releaseRefresh(); assert.equal(await first, true); assert.equal(await second, true); assert.equal(refreshCalls, 1); }); test('logout отзывает cookie-сессию и очищает access JWT из памяти', async () => { installBrowserMocks(); const calls = []; globalThis.fetch = async (url, options) => { calls.push({ url, options }); return jsonResponse({ success: true }); }; const auth = await loadAuthModule(); auth.storeAuthState({ token: 'access-before-logout', role: 'STUDENT', userId: 22 }); await auth.logoutSession(); assert.equal(auth.getAccessToken(), null); assert.equal(auth.getAuthState(), null); assert.equal(calls[0].url, '/api/auth/logout'); assert.equal(calls[0].options.credentials, 'same-origin'); });