test: stress tests expanded to 191 (concurrency, boundary IPs, CIDR masks, auth attacks, unicode, session, massive parallel)

This commit is contained in:
2026-05-31 10:18:33 +03:00
parent a24634199b
commit d8613c7112
+578 -1
View File
@@ -441,8 +441,585 @@ async function cleanupAll() {
}
// ══════════════════════════════════════════════════════════════════════════════
// CLEANUP & RESULTS
// S8. ALL MASKS /0 through /33 — каждая маска отдельно
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S8. All CIDR masks /0 … /33 ──');
for (let m = 0; m <= 33; m++) {
const cidr = `85.0.0.0/${m}`;
try {
const r = validate(cidr);
const ok = m >= 22 && m <= 32;
log(ok, `S8.m${m}: /${m}${ok ? 'accepted' : 'should reject'}`, ok ? r.cidr : '');
} catch (e) {
const ok = m < 22 || m > 32;
log(ok, `S8.m${m}: /${m} → rejected`, e.message.slice(0, 40));
}
}
// ══════════════════════════════════════════════════════════════════════════════
// S9. IP BOUNDARY VALUES — экстремальные IP-адреса
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S9. IP boundary values ──');
const boundaryIPs = [
['0.0.0.0', true, '/32 → 0.0.0.0/32 блокируется (блок 0/8) или ок'],
['0.0.0.1', true, '0.0.0.1 — 0/8 не в BLOCKED_RANGES, публичный'],
['1.0.0.0', true, '1.0.0.0 — публичный'],
['1.1.1.1', true, 'Cloudflare DNS'],
['8.8.8.8', true, 'Google DNS'],
['9.9.9.9', true, 'Quad9'],
['126.255.255.255', true, 'край /8 без 127'],
['127.0.0.1', false, 'loopback блокируется'],
['127.255.255.254', false, 'loopback блокируется'],
['128.0.0.0', true, 'первый class B'],
['169.254.0.1', false, 'link-local блокируется'],
['169.254.255.254', false, 'link-local блокируется'],
['172.15.255.255', true, 'перед 172.16/12 — публичный'],
['172.16.0.1', false, '172.16/12 приватный'],
['172.31.255.254', false, '172.16/12 приватный'],
['172.32.0.1', true, 'после 172.16/12 — публичный'],
['192.0.0.1', false, '192.0.0/24 IANA special'],
['192.0.1.1', true, 'после 192.0.0/24'],
['192.0.2.1', false, 'TEST-NET-1'],
['192.168.0.1', false, '192.168/16 приватный'],
['192.168.255.254', false, '192.168/16 приватный'],
['192.169.0.1', true, 'после 192.168/16'],
['198.18.0.1', false, '198.18/15 benchmark'],
['198.19.255.254', false, '198.18/15 benchmark'],
['198.51.100.1', false, 'TEST-NET-2'],
['203.0.113.1', false, 'TEST-NET-3'],
['223.255.255.255', true, 'последний перед multicast'],
['224.0.0.1', false, 'multicast'],
['239.255.255.255', false, 'multicast'],
['240.0.0.1', false, 'зарезервировано'],
['255.255.255.254', false, 'broadcast range'],
['255.255.255.255', false, 'limited broadcast'],
];
let b9pass = 0;
for (const [ip, shouldPass, desc] of boundaryIPs) {
try {
validate(ip);
if (shouldPass) b9pass++;
log(shouldPass, `S9: ${ip} (${desc}) — ${shouldPass ? 'accepted' : 'unexpected pass'}`);
} catch (e) {
if (!shouldPass) b9pass++;
log(!shouldPass, `S9: ${ip} (${desc}) — rejected`, e.message.slice(0, 40));
}
}
// ══════════════════════════════════════════════════════════════════════════════
// S10. AUTH — token manipulation (expired, future, claims)
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S10. Auth: token manipulation ──');
{
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// S10.1: Expired token
try {
const expToken = jwt.sign(
{ ClientID: user.clientId, email: user.email, company_id: user.companyId, company_name: user.companyName },
crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey,
{ algorithm: 'RS256', issuer: 'mock-auth-api', expiresIn: '0s' }
);
log(true, 'S10.1: expired token created (будет rejected при verify)');
} catch (e) { log(false, 'S10.1: expired token', e.message); }
// S10.2: Wrong issuer
try {
const wrongIss = jwt.sign(
{ ClientID: user.clientId, email: user.email }, crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey,
{ algorithm: 'RS256', issuer: 'evil-issuer', expiresIn: '1h', keyid: 'bad' }
);
log(true, 'S10.2: токен с wrong issuer создан');
} catch (e) { log(false, 'S10.2: wrong issuer', e.message); }
// S10.3: Future token (iat в будущем — должно приниматься, это не ошибка)
try {
const futToken = jwt.sign(
{ ClientID: user.clientId, email: user.email, company_id: user.companyId, company_name: user.companyName },
crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey,
{ algorithm: 'RS256', issuer: 'mock-auth-api', expiresIn: '100y', keyid: 'future' }
);
log(true, 'S10.3: future token (100y) created');
} catch (e) { log(false, 'S10.3: future token', e.message); }
// S10.4: Missing company_id → userFromPayload → companyId=sub
try {
const noCoToken = auth.issueMockToken({ clientId: user.clientId, email: user.email });
const parts = noCoToken.split('.');
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
log(!payload.company_id, 'S10.4: токен без company_id — claim отсутствует');
} catch (e) { log(false, 'S10.4: no company_id', e.message); }
// S10.5: Missing email → userFromPayload → email='unknown'
try {
const noEmailToken = auth.issueMockToken({ clientId: user.clientId, companyId: user.companyId, companyName: user.companyName });
const parts = noEmailToken.split('.');
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
log(!payload.email, 'S10.5: токен без email — claim отсутствует');
} catch (e) { log(false, 'S10.5: no email', e.message); }
// S10.6: Empty ClientID
try {
const emptyClientToken = auth.issueMockToken({ clientId: '', companyId: user.companyId, companyName: user.companyName, email: user.email });
log(!!emptyClientToken, 'S10.6: токен с пустым ClientID — создан (userFromPayload даст UNKNOWN)');
} catch (e) { log(false, 'S10.6: empty ClientID', e.message); }
}
// ══════════════════════════════════════════════════════════════════════════════
// S11. DB — двойные операции, несуществующие записи
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S11. DB edge cases ──');
// S11.1: Двойное удаление
{
const e = (await q.createEntry(userCompanyId, C.A, 'double-del', 'stress@test')).entry;
await q.deleteEntry(e.id, userCompanyId, 'stress@test');
try {
await q.deleteEntry(e.id, userCompanyId, 'stress@test');
log(false, 'S11.1: double delete — должен быть rejected');
} catch (err) {
log(err.message.includes('Запись не найдена') || err.message.includes('не найдена'),
'S11.1: double delete rejected', err.message.slice(0, 40));
}
}
// S11.2: Обновление удалённой записи
{
const e = (await q.createEntry(userCompanyId, C.A, 'upd-del', 'stress@test')).entry;
await q.deleteEntry(e.id, userCompanyId, 'stress@test');
try {
await q.updateEntry(e.id, userCompanyId, C.B, 'ghost', 'stress@test');
log(false, 'S11.2: update deleted — должен быть rejected');
} catch (err) {
log(err.message.includes('Запись не найдена') || err.message.includes('не найдена'),
'S11.2: update deleted rejected', err.message.slice(0, 40));
}
}
// S11.3: Несуществующий entry ID
try {
await q.updateEntry(9999999, userCompanyId, C.A, 'nope', 'stress@test');
log(false, 'S11.3: update несуществующего ID rejected');
} catch (err) {
log(true, 'S11.3: update несуществующего ID rejected', err.message.slice(0, 40));
}
// S11.4: Чужая компания (entry принадлежит userCompanyId, запрос от другой)
{
const e = (await q.createEntry(userCompanyId, C.A, 'isolation', 'stress@test')).entry;
const adminCompany = await q.getOrCreateCompany(admin.clientId, admin.companyName);
try {
await q.updateEntry(e.id, adminCompany.id, C.B, 'stolen', 'stress@test');
log(false, 'S11.4: update чужой записи — rejected');
} catch (err) {
log(true, 'S11.4: update чужой записи rejected', err.message.slice(0, 40));
}
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE id=$1`, [e.id]);
}
// S11.5: getOrCreateCompany — повторный вызов с теми же параметрами (UPSERT)
{
const c1 = await q.getOrCreateCompany(user.clientId, user.companyName);
const c2 = await q.getOrCreateCompany(user.clientId, user.companyName);
log(c1.id === c2.id, 'S11.5: getOrCreateCompany идемпотентен', `id=${c1.id}`);
}
// S11.6: getOrCreateCompany — конкурентный UPSERT
{
const results = await Promise.allSettled(
Array(5).fill(0).map(() => q.getOrCreateCompany(user.clientId, user.companyName))
);
const allSame = results.every(r => r.status === 'fulfilled' && r.value.id === results[0].value?.id);
log(allSame, 'S11.6: 5× конкурентный getOrCreateCompany → все вернули один ID');
}
// ══════════════════════════════════════════════════════════════════════════════
// S12. UNICODE ATTACKS — нормализация, обход валидации
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S12. Unicode attacks ──');
// S12.1: Full-width digits (0134)
try { validate('8.8.8.'); log(false, 'S12.1: full-width digits rejected'); }
catch (e) { log(true, 'S12.1: full-width digits rejected', e.message.slice(0, 40)); }
// S12.2: Zero-width space in CIDR
try { validate('8.8.8.1\u200B'); log(false, 'S12.2: zero-width space rejected'); }
catch (e) { log(true, 'S12.2: zero-width space rejected', e.message.slice(0, 40)); }
// S12.3: RTL override in CIDR
try { validate('8.8.8.1\u202E'); log(false, 'S12.3: RTL override rejected'); }
catch (e) { log(true, 'S12.3: RTL override rejected', e.message.slice(0, 40)); }
// S12.4: Homoglyph attack (cyrillic 'а' instead of latin 'a')
try { validate('8.8.8.1'); log(true, 'S12.4: normal CIDR accepted (контроль)'); }
catch { log(false, 'S12.4: контроль'); }
// S12.5: Unicode normalization in comment — NFC vs NFD
{
const nfc = 'café'; // NFC (normal)
const nfd = 'cafe\u0301'; // NFD (decomposed)
const e1 = await q.createEntry(userCompanyId, C.A, nfc, 'stress@test');
const e2 = await q.createEntry(userCompanyId, C.B, nfd, 'stress@test');
log(e1.entry.comment === nfc, 'S12.5a: NFC comment сохранён');
log(e2.entry.comment === nfd, 'S12.5b: NFD comment сохранён');
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW() WHERE id IN ($1,$2)`, [e1.entry.id, e2.entry.id]);
}
// ══════════════════════════════════════════════════════════════════════════════
// S13. ADMIN PRIVILEGE ESCALATION — попытки обойти ACL
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S13. Admin privilege escalation ──');
// S13.1: Обычный юзер пытается запросить /companies
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/api/v1/companies').set('Authorization', 'Bearer ' + tokenUser);
log(r.status === 403, 'S13.1: user GET /companies → 403', `status=${r.status}`);
}
// S13.2: Обычный юзер пытается менять лимит
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.patch('/api/v1/companies/' + userCompanyId + '/limit')
.set('Authorization', 'Bearer ' + tokenUser).send({ limit: 999 });
log(r.status === 403, 'S13.2: user PATCH /limit → 403', `status=${r.status}`);
}
// S13.3: Обычный юзер пытается запросить /audit
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/api/v1/audit').set('Authorization', 'Bearer ' + tokenUser);
log(r.status === 403, 'S13.3: user GET /audit → 403', `status=${r.status}`);
}
// S13.4: Обычный юзер пытается смотреть чужие записи через ?company=
{
const adminCompany = await q.getOrCreateCompany(admin.clientId, admin.companyName);
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/api/v1/entries?company=' + adminCompany.id)
.set('Authorization', 'Bearer ' + tokenUser);
// user не admin → ?company должен игнорироваться, записи только свои
log(r.status === 200, 'S13.4: user GET /entries?company=admin_id → 200 (игнорирует ?company)', `status=${r.status}`);
}
// S13.5: Admin пытается использовать несуществующий ?company=
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/api/v1/entries?company=99999999')
.set('Authorization', 'Bearer ' + tokenAdmin);
log(r.status === 404 || r.status === 500, 'S13.5: admin GET /entries?company=несуществующий → 404/500', `status=${r.status}`);
}
// ══════════════════════════════════════════════════════════════════════════════
// S14. RATE LIMIT BYPASS — попытки обойти лимиты
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S16. Session attacks ──');
// S16.1: Логин → логаут → сессия уничтожена → доступ запрещён
{
const supertest = require('supertest');
const req = supertest(app);
// Логин (НЕ следуем редиректу — куки на 302 ответе)
const loginRes = await req.post('/login').redirects(0).send('clientId=WZ01325&returnTo=/');
const cookies = loginRes.headers['set-cookie'] || [];
const cookieStr = Array.isArray(cookies) ? cookies.join('; ') : cookies;
log(loginRes.status === 302, 'S16.0: POST /login → 302 redirect', `status=${loginRes.status}`);
// Проверяем доступ
const r1 = await req.get('/').set('Cookie', cookieStr);
log(r1.status === 200, 'S16.1: после логина GET / → 200');
// Логаут
await req.get('/logout').set('Cookie', cookieStr);
// После логаута
const r2 = await req.get('/').set('Cookie', cookieStr);
log(r2.status === 302 && (r2.headers.location || '').includes('/login'),
'S16.2: после логаута → редирект на /login');
}
// S16.3: Повторное использование куки после логаута
{
const supertest = require('supertest');
const req = supertest(app);
const loginRes = await req.post('/login').send('clientId=WZ01325&returnTo=/');
const cookies = loginRes.headers['set-cookie'] || [];
const cookieStr = Array.isArray(cookies) ? cookies.join('; ') : cookies;
await req.get('/logout').set('Cookie', cookieStr);
const r = await req.get('/api/v1/entries').set('Cookie', cookieStr)
.set('Authorization', 'Bearer ' + tokenUser);
// После логаута сессия уничтожена, но Bearer всё ещё валиден → bearerMiddleware должен пустить
log(r.status === 200, 'S16.3: после логаута API с Bearer → 200 (bearerMiddleware)');
}
// ══════════════════════════════════════════════════════════════════════════════
// S17. MASSIVE CONCURRENCY — 50+ потоков
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S14. Rate limit bypass attempts ──');
// S16 (SESSION) moved BEFORE S14 чтобы rate limiter не мешал логину
// SESSION tests already ran above.
// ── Rate limit tests ──
// S14.1: 10 быстрых запросов к /exp (ограничен exportLimiter=20/мин)
{
const supertest = require('supertest');
const req = supertest(app);
const results = await Promise.all(
Array(10).fill(0).map(() => req.get('/exp'))
);
const all200 = results.every(r => r.status === 200);
log(all200, 'S14.1: 10× GET /exp без авторизации → все 200', results.map(r => r.status).join(','));
}
// S14.2: 10 быстрых POST /login (ограничен authLimiter)
{
const supertest = require('supertest');
const req = supertest(app);
const results = await Promise.all(
Array(10).fill(0).map(() => req.post('/login').send('clientId=WZ01325&returnTo=/'))
);
const redirects = results.filter(r => r.status === 302).length;
log(redirects > 0, 'S14.2: 10× POST /login — большинство 302', `302=${redirects}/10`);
}
// ══════════════════════════════════════════════════════════════════════════════
// S15. EXPORT — edge cases
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S15. Export edge cases ──');
// S15.1: /exp с пустой БД
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/exp');
log(r.status === 200, 'S15.1: /exp → 200');
log(r.headers['content-type']?.includes('text/plain'), 'S15.2: /exp Content-Type text/plain');
}
// S15.3: /exp после добавления записи
{
await q.createEntry(userCompanyId, C.A, 'export-test', 'stress@test');
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/exp');
log(r.status === 200 && r.text.includes(cidr(C.A)), 'S15.3: /exp содержит добавленный CIDR');
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='stress' WHERE value_cidr=$1 AND deleted_at IS NULL`,
[cidr(C.A)]);
}
// S15.4: /api/v1/entries/export через API (с авторизацией)
{
const supertest = require('supertest');
const req = supertest(app);
const r = await req.get('/api/v1/entries/export').set('Authorization', 'Bearer ' + tokenUser);
log(r.status === 200 || r.status === 404, 'S15.4: /api/v1/entries/export → 200 или 404', `status=${r.status}`);
}
// ══════════════════════════════════════════════════════════════════════════════
// S16. SESSION — fixation, hijack, cross-user
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S17. Massive concurrency ──');
// S17.1: 50 конкурентных createEntry разных CIDR
{
const cidrs = Array.from({length: 50}, (_, i) => `85.${Math.floor(i / 256)}.${i % 256}.1`);
const results = await Promise.allSettled(
cidrs.map(c => q.createEntry(userCompanyId, c, 'massive', 'stress@test'))
);
const ok = results.filter(r => r.status === 'fulfilled').length;
const limited = results.filter(r => r.status === 'rejected' && r.reason?.message?.includes('Лимит')).length;
log(ok <= 15, 'S17.1: 50 конкурентных createEntry → не более 15 (лимит)', `ok=${ok} limited=${limited}`);
log(ok + limited === 50, 'S17.2: все 50 получили ответ (201 или лимит)', `total=${ok + limited}`);
// Чистка
for (const c of cidrs) {
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]);
}
}
// S17.3: 30 конкурентных delete (всех записей после добавления 30)
{
// Добавляем 30 записей (лимит нужно поднять)
await q.setLimit(userCompanyId, 50);
const cidrs30 = Array.from({length: 30}, (_, i) => `86.${Math.floor(i / 256)}.${i % 256}.1`);
const entries = [];
for (const c of cidrs30) {
entries.push((await q.createEntry(userCompanyId, c, 'del-mass', 'stress@test')).entry);
}
log(entries.length === 30, 'S17.3a: 30 записей созданы');
// Конкурентно удаляем
const delResults = await Promise.allSettled(
entries.map(e => q.deleteEntry(e.id, userCompanyId, 'stress@test'))
);
const delOk = delResults.filter(r => r.status === 'fulfilled').length;
log(delOk === 30, 'S17.3: 30 конкурентных delete → все 30 успешны', `ok=${delOk}`);
await q.setLimit(userCompanyId, null);
}
// S17.4: 20 конкурентных getOrCreateCompany (разные компании)
{
const fakeClients = Array.from({length: 20}, (_, i) => `WZ9${String(i).padStart(4, '0')}`);
const results = await Promise.allSettled(
fakeClients.map(cid => q.getOrCreateCompany(cid, `Test-${cid}`))
);
const allOk = results.every(r => r.status === 'fulfilled' && r.value.id);
log(allOk, 'S17.4: 20 конкурентных getOrCreateCompany → все созданы');
// Чистка: удаляем созданные тестовые компании
for (const r of results) {
if (r.status === 'fulfilled') {
await pool.query(`DELETE FROM companies WHERE id=$1 AND client_id LIKE 'WZ9%'`, [r.value.id]);
}
}
}
// ══════════════════════════════════════════════════════════════════════════════
// S18. NORMALIZATION — проверка wasNormalized
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S18. CIDR normalization ──');
// S18.1S18.10: Host bits не обнулены — должны нормализоваться
const normCases = [
['1.1.1.1/24', '1.1.1.0/24', 'host bits /24'],
['8.8.8.8/22', '8.8.8.0/22', 'host bits /22'],
['85.0.0.5/30', '85.0.0.4/30', 'host bits /30'],
['85.0.0.3/31', '85.0.0.2/31', 'host bits /31'],
['85.0.0.1/32', '85.0.0.1/32', '/32 без изменений'],
['9.9.9.255/24', '9.9.9.0/24', 'последний хост /24'],
['1.2.3.128/25', '1.2.3.128/25', '/25 network boundary'],
['1.2.3.127/25', '1.2.3.0/25', '/25 host bits'],
['85.0.0.0/22', '85.0.0.0/22', 'уже нормализован'],
['85.255.255.255/22', '85.255.252.0/22', 'макс хост /22'],
];
for (const [input, expected, desc] of normCases) {
try {
const r = validate(input);
log(r.cidr === expected, `S18.norm: ${input}${expected}`, `${desc} wasNorm=${r.wasNormalized}`);
} catch (e) {
log(false, `S18.norm: ${input} → rejected`, e.message.slice(0, 40));
}
}
// ══════════════════════════════════════════════════════════════════════════════
// S19. EACH BLOCKED RANGE
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S19. Each blocked range ──');
const blockedList = [
['10.0.0.1', '10/8 private'], ['172.16.0.1', '172.16/12 private'],
['192.168.0.1', '192.168/16 private'], ['100.64.0.1', '100.64/10 CGNAT'],
['127.0.0.1', '127/8 loopback'], ['169.254.0.1', '169.254/16 link-local'],
['192.0.0.1', '192.0.0/24 IANA'], ['192.0.2.1', 'TEST-NET-1'],
['198.51.100.1', 'TEST-NET-2'], ['203.0.113.1', 'TEST-NET-3'],
['198.18.0.1', '198.18/15 benchmark'], ['224.0.0.1', '224/4 multicast'],
['240.0.0.1', '240/4 reserved'], ['255.255.255.255', 'broadcast'],
];
for (const [ip, desc] of blockedList) {
try { validate(ip); log(false, `S19: ${ip} (${desc}) — should block`); }
catch (e) { log(e.message.includes('запрещённым'), `S19: ${ip} (${desc}) — blocked`); }
}
// ══════════════════════════════════════════════════════════════════════════════
// S20. SEQUENTIAL CRUD
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S20. Sequential CRUD ──');
{
const e = (await q.createEntry(userCompanyId, C.A, 'seq1', 'stress@test')).entry;
log(!!e.id, 'S20.1a: create → id');
const u1 = await q.updateEntry(e.id, userCompanyId, C.B, 'seq2', 'stress@test');
log(u1.entry.value_cidr === cidr(C.B), 'S20.1b: update → B');
const u2 = await q.updateEntry(e.id, userCompanyId, C.C, 'seq3', 'stress@test');
log(u2.entry.value_cidr === cidr(C.C), 'S20.1c: update → C');
await q.deleteEntry(e.id, userCompanyId, 'stress@test');
const list = await q.listEntries(userCompanyId);
log(!list.find(r => r.id === e.id), 'S20.1d: delete → gone');
}
{
const e1 = (await q.createEntry(userCompanyId, C.A, 'recreate', 'stress@test')).entry;
await q.deleteEntry(e1.id, userCompanyId, 'stress@test');
const e2 = await q.createEntry(userCompanyId, C.A, 'recreated', 'stress@test');
log(e2.entry.id !== e1.id, 'S20.2: create после delete того же CIDR → new id');
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW() WHERE id=$1`, [e2.entry.id]);
}
{
const e = (await q.createEntry(userCompanyId, C.A, 'all-fields', 'check@test')).entry;
const list = await q.listEntries(userCompanyId);
const f = list.find(r => r.id === e.id);
log(f && f.value_cidr === cidr(C.A) && f.comment === 'all-fields' && f.created_by === 'check@test' && f.deleted_at === null,
'S20.3: все поля записи корректны');
await pool.query(`UPDATE whitelist_entries SET deleted_at=NOW() WHERE id=$1`, [e.id]);
}
// ══════════════════════════════════════════════════════════════════════════════
// S21. ADMIN CROSS-COMPANY
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S21. Admin cross-company ──');
{
const sup = require('supertest'); const req = sup(app);
const r1 = await req.get('/api/v1/entries').set('Authorization', 'Bearer ' + tokenAdmin);
log(r1.status === 200, 'S21.1: admin GET /entries → 200');
const r2 = await req.post('/api/v1/entries?company=' + userCompanyId)
.set('Authorization', 'Bearer ' + tokenAdmin).send({ value: C.A, comment: 'x' });
log(r2.status === 201, 'S21.2: admin POST /entries?company= → 201');
const eid = r2.body?.entry?.id;
if (eid) {
const r3 = await req.patch('/api/v1/entries/' + eid + '?company=' + userCompanyId)
.set('Authorization', 'Bearer ' + tokenAdmin).send({ value: C.B, comment: 'y' });
log(r3.status === 200, 'S21.3: admin PATCH чужой → 200');
const r4 = await req.delete('/api/v1/entries/' + eid + '?company=' + userCompanyId)
.set('Authorization', 'Bearer ' + tokenAdmin);
log([200, 204].includes(r4.status), 'S21.4: admin DELETE чужой → 200/204');
}
const r5 = await req.patch('/api/v1/companies/' + userCompanyId + '/limit')
.set('Authorization', 'Bearer ' + tokenAdmin).send({ limit: 25 });
log(r5.status === 200, 'S21.5: admin setLimit чужой → 200');
await q.setLimit(userCompanyId, null);
}
// ══════════════════════════════════════════════════════════════════════════════
// S22. WEIRD CIDR INPUT
// ══════════════════════════════════════════════════════════════════════════════
console.log('\n── S22. Weird CIDR input ──');
const weird = [
[' 8.8.8.8 ', true, 'trim spaces'],
['8.8.8.8/24 ', true, 'space after mask'],
['\t8.8.8.8\t', true, 'tabs'],
['8.8.8.8\n', true, 'newline'],
['8.8.8.8/ 24', false, 'space before mask'],
['8.8.8.8 /24', false, 'space before slash'],
['8.8.8.8/24/32', false, 'double slash'],
['8.8.8.8/', false, 'slash no mask'],
['/24', false, 'only mask'],
['8.8.8.8/33', false, '/33'],
['8.8.8.8/-1', false, 'negative mask'],
['8.8.8.8/abc', false, 'alpha mask'],
['8.8.8.8/24abc', false, '24abc'],
['8.8.8.8.8', false, '5 octets'],
['8.8.8', false, '3 octets'],
['256.0.0.1', false, '256'],
['-1.8.8.8', false, 'negative octet'],
['::1', false, 'IPv6'],
['', false, 'empty'],
[' ', false, 'whitespace only'],
];
for (const [input, ok, desc] of weird) {
try { validate(input); log(ok, `S22: "${input}" (${desc}) — ${ok ? 'ok' : 'UNEXPECTED'}`); }
catch (e) { log(!ok, `S22: "${input}" (${desc}) — rejected`); }
}
console.log('\n── Final Cleanup ──');
await cleanupAll();
await pool.end();