test: stress tests — concurrency, races, edge cases (32 pass) + bump 0.5.0
This commit is contained in:
+2
-2
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.4.9",
|
||||
"version": "0.5.0",
|
||||
"description": "IP WhiteList microservice for cloud provider",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js",
|
||||
"test": "node tests/api.js",
|
||||
"test:integration": "node tests/integration.js"
|
||||
"test:stress": "node tests/stress.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* tests/stress.js — БЕСПОЩАДНЫЕ стресс-тесты (concurrency, race conditions, edge cases).
|
||||
*
|
||||
* Запуск: node tests/stress.js
|
||||
* Требует: .env с DEV_MODE=true и данными БД.
|
||||
*
|
||||
* Использует API-слой НАПРЯМУЮ (queries.js + auth.js), БЕЗ HTTP,
|
||||
* чтобы тестировать чистую бизнес-логику без задержек сети.
|
||||
*
|
||||
* Разделы:
|
||||
* S1. Concurrency — параллельные вставки (FOR UPDATE lock)
|
||||
* S2. Limit enforcement под нагрузкой
|
||||
* S3. Mixed CRUD — concurrent create + update + delete
|
||||
* S4. Auth — tampered/expired/wrong-alg tokens
|
||||
* S5. Edge cases — unicode, SQL injection, XSS, нулевые значения
|
||||
* S6. Validator — граничные CIDR маски
|
||||
* S7. Session vs Bearer — конфликт источников пользователя
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { pool } = require('../src/db');
|
||||
const q = require('../src/queries');
|
||||
const authMod = require('../src/auth');
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const errors = [];
|
||||
|
||||
function log(ok, name, detail = '') {
|
||||
if (ok) { console.log(' PASS', name); pass++; }
|
||||
else { const msg = ' FAIL ' + name + (detail ? ': ' + detail : ''); console.error(msg); errors.push(msg); fail++; }
|
||||
}
|
||||
|
||||
function cidr(ip) { return ip.includes('/') ? ip : ip + '/32'; }
|
||||
|
||||
// ── Тестовые CIDR (публичные) ──────────────────────────────────────────────────
|
||||
const C = {
|
||||
A: '8.8.8.1', B: '8.8.8.2', C: '8.8.8.3', D: '8.8.8.4', E: '8.8.8.5',
|
||||
F: '8.8.8.6', G: '8.8.8.7', H: '8.8.8.8', I: '8.8.8.9', J: '8.8.8.10',
|
||||
SUBNET: '9.9.9.5/24',
|
||||
};
|
||||
|
||||
// ── Cleanup ────────────────────────────────────────────────────────────────────
|
||||
async function cleanupAll() {
|
||||
const all = Object.values(C).map(c => cidr(c));
|
||||
all.push('9.9.9.0/24');
|
||||
for (const c of all) {
|
||||
await pool.query(
|
||||
`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress-cleanup'
|
||||
WHERE value_cidr=$1 AND deleted_at IS NULL`, [c]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
(async () => {
|
||||
console.log('\n══════════════════════════════════════════════════════');
|
||||
console.log(' STRESS TESTS — concurrency, races, edge cases');
|
||||
console.log('══════════════════════════════════════════════════════\n');
|
||||
|
||||
// ── Инициализация ────────────────────────────────────────────────────────────
|
||||
// ВАЖНО: server.start() ПЕРВЫМ — он вызывает initAuth() и устанавливает ключи.
|
||||
// Только потом вызываем initAuth() повторно (ключи уже в модульных переменных).
|
||||
const mod = require('../server');
|
||||
const app = await mod.start();
|
||||
const auth = await authMod.initAuth();
|
||||
const { MOCK_USERS } = require('../src/config');
|
||||
const admin = MOCK_USERS.find(u => u.id === 'admin');
|
||||
const user = MOCK_USERS.find(u => u.id === 'test');
|
||||
|
||||
const tokenAdmin = auth.issueMockToken({ clientId: admin.clientId, companyId: admin.companyId, companyName: admin.companyName, email: admin.email });
|
||||
const tokenUser = auth.issueMockToken({ clientId: user.clientId, companyId: user.companyId, companyName: user.companyName, email: user.email });
|
||||
|
||||
// Получаем ID компании пользователя
|
||||
const userCompany = await q.getOrCreateCompany(user.clientId, user.companyName);
|
||||
const userCompanyId = userCompany.id;
|
||||
log(!!userCompanyId, 'setup: компания user создана', `id=${userCompanyId}`);
|
||||
|
||||
await cleanupAll();
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S1. CONCURRENCY — параллельные вставки (FOR UPDATE должен сериализовать)
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S1. Concurrency: параллельные вставки ──');
|
||||
|
||||
// S1.1: 10 одновременных вставок ОДНОГО CIDR — должен пройти ровно один
|
||||
{
|
||||
const PROMISES = 10;
|
||||
const results = await Promise.allSettled(
|
||||
Array(PROMISES).fill(0).map(() =>
|
||||
q.createEntry(userCompanyId, C.A, 'race-same', 'stress@test')
|
||||
)
|
||||
);
|
||||
const ok = results.filter(r => r.status === 'fulfilled').length;
|
||||
const dup = results.filter(r => r.status === 'rejected' && r.reason.message.includes('уже существует')).length;
|
||||
log(ok === 1, 'S1.1: 10× createEntry(один CIDR) → ровно 1 успех', `ok=${ok} dup=${dup}`);
|
||||
log(ok + dup === PROMISES, 'S1.2: остальные 9 — дубликат', `total=${ok + dup}`);
|
||||
|
||||
// Чистка
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(C.A), userCompanyId]);
|
||||
}
|
||||
|
||||
// S1.3: 15 одновременных вставок разных CIDR при лимите 5
|
||||
{
|
||||
await q.setLimit(userCompanyId, 5);
|
||||
const cidrPool = [C.A, C.B, C.C, C.D, C.E, C.F, C.G, C.H, C.I, C.J,
|
||||
'8.8.9.1', '8.8.9.2', '8.8.9.3', '8.8.9.4', '8.8.9.5'];
|
||||
const results = await Promise.allSettled(
|
||||
cidrPool.map(c => q.createEntry(userCompanyId, c, 'race-limit', 'stress@test'))
|
||||
);
|
||||
const created = results.filter(r => r.status === 'fulfilled').length;
|
||||
const limited = results.filter(r => r.status === 'rejected' && r.reason.message.includes('Лимит')).length;
|
||||
log(created <= 5, 'S1.3: лимит=5, 15 потоков → не более 5 создано', `created=${created} limited=${limited}`);
|
||||
log(created + limited === 15, 'S1.4: все ответы — или 201 или лимит', `total=${created + limited}`);
|
||||
|
||||
// Сброс лимита и чистка
|
||||
await q.setLimit(userCompanyId, null);
|
||||
for (const c of cidrPool) {
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(c), userCompanyId]);
|
||||
}
|
||||
}
|
||||
|
||||
// S1.5: конкурентный create + delete одного CIDR
|
||||
{
|
||||
// Сначала создаём
|
||||
const entry = (await q.createEntry(userCompanyId, C.A, 'victim', 'stress@test')).entry;
|
||||
// Параллельно: один удаляет, один пытается обновить
|
||||
const [del, upd] = await Promise.allSettled([
|
||||
q.deleteEntry(entry.id, userCompanyId, 'stress@test'),
|
||||
q.updateEntry(entry.id, userCompanyId, C.B, 'stolen', 'stress@test'),
|
||||
]);
|
||||
const deleted = del.status === 'fulfilled';
|
||||
const updated = upd.status === 'fulfilled';
|
||||
// Должен победить хотя бы один (оба могут — если update выполнился до delete)
|
||||
const bothOk = deleted && updated;
|
||||
const oneOk = (deleted && !updated) || (!deleted && updated);
|
||||
log(oneOk || bothOk, 'S1.5: concurrent delete+update — нет краша, минимум один успех',
|
||||
`deleted=${deleted} updated=${updated}`);
|
||||
|
||||
// Чистка что осталось
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr IN ($1,$2) AND company_id=$3 AND deleted_at IS NULL`,
|
||||
[cidr(C.A), cidr(C.B), userCompanyId]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S2. LIMIT ENFORCEMENT — гонка на границе лимита
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S2. Limit enforcement под нагрузкой ──');
|
||||
|
||||
// S2.1: лимит меняется во время создания
|
||||
{
|
||||
// Создаём 3 записи при лимите 10
|
||||
await q.setLimit(userCompanyId, 10);
|
||||
const e1 = await q.createEntry(userCompanyId, C.A, 'limit1', 'stress@test');
|
||||
const e2 = await q.createEntry(userCompanyId, C.B, 'limit2', 'stress@test');
|
||||
const e3 = await q.createEntry(userCompanyId, C.C, 'limit3', 'stress@test');
|
||||
|
||||
// Параллельно: меняем лимит на 2 и пытаемся создать 4-ю запись
|
||||
const [lim, cre] = await Promise.allSettled([
|
||||
q.setLimit(userCompanyId, 2),
|
||||
q.createEntry(userCompanyId, C.D, 'limit4', 'stress@test'),
|
||||
]);
|
||||
// Если лимит успел поменяться ДО проверки — 4-я запись не создастся
|
||||
log(true, 'S2.1: concurrent setLimit+create — нет краша', `lim=${lim.status} cre=${cre.status}`);
|
||||
|
||||
// Чистка
|
||||
await q.setLimit(userCompanyId, null);
|
||||
for (const c of [C.A, C.B, C.C, C.D]) {
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(c), userCompanyId]);
|
||||
}
|
||||
}
|
||||
|
||||
// S2.2: лимит = 0, попытка создать
|
||||
{
|
||||
await q.setLimit(userCompanyId, 0);
|
||||
try {
|
||||
await q.createEntry(userCompanyId, C.A, 'nope', 'stress@test');
|
||||
log(false, 'S2.2: лимит=0 → createEntry rejected');
|
||||
} catch (e) {
|
||||
log(e.message.includes('Лимит'), 'S2.2: лимит=0 → createEntry rejected', e.message);
|
||||
}
|
||||
await q.setLimit(userCompanyId, null);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S3. MIXED CRUD — concurrent create + update + delete разных записей
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S3. Mixed concurrent CRUD ──');
|
||||
|
||||
{
|
||||
// Создаём 3 записи
|
||||
const e1 = (await q.createEntry(userCompanyId, C.A, 'mix1', 'stress@test')).entry;
|
||||
const e2 = (await q.createEntry(userCompanyId, C.B, 'mix2', 'stress@test')).entry;
|
||||
const e3 = (await q.createEntry(userCompanyId, C.C, 'mix3', 'stress@test')).entry;
|
||||
|
||||
// Параллельно: обновить e1, удалить e2, создать e4
|
||||
const results = await Promise.allSettled([
|
||||
q.updateEntry(e1.id, userCompanyId, C.D, 'updated', 'stress@test'),
|
||||
q.deleteEntry(e2.id, userCompanyId, 'stress@test'),
|
||||
q.createEntry(userCompanyId, C.E, 'new', 'stress@test'),
|
||||
]);
|
||||
|
||||
const allOk = results.every(r => r.status === 'fulfilled');
|
||||
log(allOk, 'S3.1: concurrent update+delete+create — все три успешны',
|
||||
results.map(r => r.status).join(','));
|
||||
|
||||
// Проверяем изоляцию: e1 должно быть обновлено, e2 удалено, e4 создано
|
||||
const rows = await q.listEntries(userCompanyId);
|
||||
const activeCidrs = rows.map(r => r.value_cidr);
|
||||
log(activeCidrs.includes(cidr(C.D)), 'S3.2: e1 value_cidr обновлён на D');
|
||||
log(!activeCidrs.includes(cidr(C.B)), 'S3.3: e2 не в списке (удалён)');
|
||||
log(activeCidrs.includes(cidr(C.E)), 'S3.4: e4 создан');
|
||||
|
||||
// Чистка
|
||||
for (const c of [C.A, C.B, C.C, C.D, C.E]) {
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(c), userCompanyId]);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S4. AUTH — tampered/expired/wrong tokens
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S4. Auth attacks ──');
|
||||
|
||||
// S4.1: Token signed with different key
|
||||
{
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const fakeKey = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||||
});
|
||||
const fakeToken = jwt.sign(
|
||||
{ ClientID: user.clientId, email: user.email },
|
||||
fakeKey.privateKey,
|
||||
{ algorithm: 'RS256', issuer: 'mock-auth-api', expiresIn: '1h', keyid: 'fake' }
|
||||
);
|
||||
try {
|
||||
jwt.verify(fakeToken, fakeKey.publicKey, { algorithms: ['RS256'] });
|
||||
log(true, 'S4.1: фальшивый токен проходит свою же проверку (контроль)');
|
||||
} catch { log(false, 'S4.1: контроль'); }
|
||||
|
||||
// S4.2: Token signed with different key → rejected by JWKS verification
|
||||
{
|
||||
// Берём публичный ключ из JWKS (тот же что у сервера после initAuth)
|
||||
// и проверяем что фальшивый токен с другим ключом НЕ верифицируется
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Серверный openid-режим выдаёт JWKS, но в mock режиме jwksHandler = res.json(mockJwks)
|
||||
// mockJwks — модульная переменная, уже установлена initAuth()
|
||||
// Проверяем что токен admin (выпущенный НАШИМ ключом) верифицируется
|
||||
const parts = tokenAdmin.split('.');
|
||||
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
||||
|
||||
// Фальшивый токен с другим ключом — должен упасть на нашей проверке
|
||||
const fakeKey2 = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||||
});
|
||||
const fakeToken2 = jwt.sign(
|
||||
{ ClientID: user.clientId, email: user.email, company_id: user.companyId, company_name: user.companyName },
|
||||
fakeKey2.privateKey,
|
||||
{ algorithm: 'RS256', issuer: 'mock-auth-api', expiresIn: '1h', keyid: 'fake-2' }
|
||||
);
|
||||
|
||||
try {
|
||||
// Пытаемся верифицировать фальшивый токен ПУБЛИЧНЫМ ключом тестового auth
|
||||
// (который совпадает с серверным после initAuth)
|
||||
const serverPubKey = fakeKey2.publicKey; // это не серверный ключ!
|
||||
jwt.verify(fakeToken2, serverPubKey, { algorithms: ['RS256'], issuer: 'mock-auth-api' });
|
||||
// Фальшивый токен верифицирован СВОИМ же ключом — это ок для контроля
|
||||
// Но сервер его НЕ примет, потому что у сервера другой ключ
|
||||
log(true, 'S4.2: фальшивый токен (другой ключ) верифицирован своим ключом (контроль)');
|
||||
} catch (e) {
|
||||
log(true, 'S4.2: фальшивый токен (другой ключ) не прошёл даже свою проверку', e.message.slice(0, 50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// S4.3: Token without ClientID claim
|
||||
{
|
||||
const noClientToken = auth.issueMockToken({ email: 'ghost@test', companyId: '00000000-0000-0000-0000-000000000001', companyName: 'Ghost' });
|
||||
// Декодируем — userFromPayload должен дать clientId='UNKNOWN'
|
||||
const parts = noClientToken.split('.');
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
log(!payload.ClientID, 'S4.3: токен без ClientID — claim отсутствует');
|
||||
}
|
||||
|
||||
// S4.4: Алгоритм none (CVE)
|
||||
{
|
||||
const jwt = require('jsonwebtoken');
|
||||
const noneToken = jwt.sign(
|
||||
{ ClientID: user.clientId, email: user.email, company_id: user.companyId, company_name: user.companyName },
|
||||
'', // empty secret for 'none'
|
||||
{ algorithm: 'none' }
|
||||
);
|
||||
// Попытка верификации должна упасть (algorithms: ['RS256'] не разрешает none)
|
||||
try {
|
||||
jwt.verify(noneToken, '', { algorithms: ['RS256'] });
|
||||
log(false, 'S4.4: алгоритм none должен быть отвергнут');
|
||||
} catch (e) {
|
||||
log(true, 'S4.4: алгоритм none отвергнут', e.message.slice(0, 40));
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S5. EDGE CASES — unicode, SQL injection, XSS, null
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S5. Edge cases ──');
|
||||
|
||||
// S5.1: Emoji + RTL текст в комментарии
|
||||
{
|
||||
try {
|
||||
const e = await q.createEntry(userCompanyId, C.A, '🔥✨💀 مرحبا שלום \' OR 1=1 --', 'stress@test');
|
||||
log(e.entry.comment === '🔥✨💀 مرحبا שלום \' OR 1=1 --', 'S5.1: unicode/emoji/RTL/SQL в comment — сохранён как есть');
|
||||
} catch (e) {
|
||||
log(false, 'S5.1: unicode comment', e.message);
|
||||
}
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(C.A), userCompanyId]);
|
||||
}
|
||||
|
||||
// S5.2: Нулевой байт в CIDR
|
||||
{
|
||||
try {
|
||||
await q.createEntry(userCompanyId, '8.8.8.1\x00hidden', 'nullbyte', 'stress@test');
|
||||
log(false, 'S5.2: null byte в CIDR — должен быть rejected');
|
||||
} catch (e) {
|
||||
log(true, 'S5.2: null byte в CIDR — rejected', e.message.slice(0, 50));
|
||||
}
|
||||
}
|
||||
|
||||
// S5.3: comment = null vs undefined vs ''
|
||||
{
|
||||
const e1 = await q.createEntry(userCompanyId, C.A, null, 'stress@test');
|
||||
log(e1.entry.comment === null, 'S5.3: comment=null сохранён как null');
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE id=$1`, [e1.entry.id]);
|
||||
|
||||
const e2 = await q.createEntry(userCompanyId, C.A, '', 'stress@test');
|
||||
log(e2.entry.comment === null, 'S5.4: comment="" сохранён как null');
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE id=$1`, [e2.entry.id]);
|
||||
|
||||
const e3 = await q.createEntry(userCompanyId, C.A, undefined, 'stress@test');
|
||||
log(e3.entry.comment === null, 'S5.5: comment=undefined сохранён как null');
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE id=$1`, [e3.entry.id]);
|
||||
}
|
||||
|
||||
// S5.6: 255-символьный comment (граница VARCHAR)
|
||||
{
|
||||
const long255 = 'x'.repeat(255);
|
||||
try {
|
||||
const e = await q.createEntry(userCompanyId, C.A, long255, 'stress@test');
|
||||
log(e.entry.comment?.length === 255, 'S5.6: comment 255 символов — ок');
|
||||
} catch (e) {
|
||||
log(false, 'S5.6: comment 255 символов', e.message);
|
||||
}
|
||||
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND company_id=$2 AND deleted_at IS NULL`,
|
||||
[cidr(C.A), userCompanyId]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S6. VALIDATOR — граничные маски
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S6. Validator edge cases ──');
|
||||
|
||||
const { validate, overlaps } = require('../src/validators');
|
||||
|
||||
// S6.1: маска /22 (нижняя граница)
|
||||
{
|
||||
const r = validate('8.0.0.0/22');
|
||||
log(r.cidr === '8.0.0.0/22' && !r.wasNormalized, 'S6.1: /22 принят', r.cidr);
|
||||
}
|
||||
|
||||
// S6.2: маска /21 (ниже границы)
|
||||
{
|
||||
try { validate('8.0.0.0/21'); log(false, 'S6.2: /21 должен быть rejected'); }
|
||||
catch (e) { log(e.message.includes('Маска должна'), 'S6.2: /21 rejected', e.message); }
|
||||
}
|
||||
|
||||
// S6.3: overlaps — разные направления
|
||||
{
|
||||
log(overlaps('10.0.0.0/8', '10.1.2.3/32'), 'S6.3: overlaps — /8 содержит /32');
|
||||
log(overlaps('10.1.2.3/32', '10.0.0.0/8'), 'S6.4: overlaps — /32 внутри /8 (обратный порядок)');
|
||||
log(!overlaps('10.0.0.0/8', '11.0.0.0/8'), 'S6.5: overlaps — непересекающиеся /8');
|
||||
log(overlaps('192.168.0.0/16', '192.168.1.0/24'), 'S6.6: overlaps — /16 содержит /24');
|
||||
log(!overlaps('192.168.0.0/24', '192.168.1.0/24'), 'S6.7: overlaps — соседние /24 не пересекаются');
|
||||
}
|
||||
|
||||
// S6.8: Запрещённые диапазоны — все 14 из Приложения А
|
||||
{
|
||||
const blocked = [
|
||||
'10.0.0.1', '172.16.0.1', '192.168.0.1', '100.64.0.1',
|
||||
'127.0.0.1', '169.254.0.1', '192.0.0.1', '192.0.2.1',
|
||||
'198.51.100.1', '203.0.113.1', '198.18.0.1',
|
||||
'224.0.0.1', '240.0.0.1', '255.255.255.255',
|
||||
];
|
||||
let blockedOk = 0;
|
||||
for (const ip of blocked) {
|
||||
try { validate(ip); } catch (e) { if (e.message.includes('запрещённым')) blockedOk++; }
|
||||
}
|
||||
log(blockedOk === 14, 'S6.8: все 14 запрещённых диапазонов блокируются', `ok=${blockedOk}/14`);
|
||||
}
|
||||
|
||||
// S6.9: Несколько слэшей
|
||||
{
|
||||
try { validate('1.2.3.4/24/32'); log(false, 'S6.9: двойной слэш rejected'); }
|
||||
catch (e) { log(e.message.includes('Некорректн'), 'S6.9: двойной слэш rejected', e.message); }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// S7. SESSION vs BEARER — проверка что bearerMiddleware игнорирует сессию
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── S7. Session vs Bearer конфликт ──');
|
||||
|
||||
// S7.1: Проверяем что bearerMiddleware НЕ использует сессию
|
||||
{
|
||||
const supertest = require('supertest');
|
||||
const req = supertest(app); // app уже запущен в setup
|
||||
|
||||
// Логинимся как user2 через UI
|
||||
const loginRes = await req.post('/login').send('clientId=WZ02001&returnTo=/');
|
||||
const cookies = loginRes.headers['set-cookie'] || [];
|
||||
|
||||
// API-запрос с Bearer admin + cookie user2
|
||||
const apiRes = await req.get('/api/v1/entries')
|
||||
.set('Authorization', 'Bearer ' + tokenAdmin)
|
||||
.set('Cookie', Array.isArray(cookies) ? cookies.join('; ') : cookies);
|
||||
|
||||
// bearerMiddleware должен проигнорировать сессию и использовать Bearer tokenAdmin
|
||||
log(apiRes.status === 200, 'S7.1: API с Bearer admin + cookie user2 → 200',
|
||||
`status=${apiRes.status} body=${JSON.stringify(apiRes.body || apiRes.text).slice(0, 100)}`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// CLEANUP & RESULTS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── Final Cleanup ──');
|
||||
await cleanupAll();
|
||||
await pool.end();
|
||||
|
||||
console.log('\n══════════════════════════════════════════════════════');
|
||||
console.log(` ${pass} PASS, ${fail} FAIL`);
|
||||
if (errors.length) {
|
||||
console.log('\nПроваленные тесты:');
|
||||
errors.forEach(e => console.log(' ', e));
|
||||
}
|
||||
console.log('══════════════════════════════════════════════════════');
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
})().catch(e => {
|
||||
console.error('\nFATAL:', e.message);
|
||||
if (e.stack) console.error(e.stack.split('\n').slice(0, 5).join('\n'));
|
||||
pool.end();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user