баг-фикс 30/34
This commit is contained in:
174
frontend/tests/auth-session.test.mjs
Normal file
174
frontend/tests/auth-session.test.mjs
Normal file
@@ -0,0 +1,174 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user