баг-фикс 30/34

This commit is contained in:
Zuev
2026-07-19 14:40:43 +03:00
parent 3d798c13e3
commit bc0e1ab1b4
172 changed files with 13431 additions and 2910 deletions

View 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');
});