test: IPv6, boundary CIDR, race condition, rate-limiter (49/49 pass)

This commit is contained in:
2026-05-30 17:18:56 +03:00
parent 91d18afe10
commit 1e330a4395
+140 -15
View File
@@ -543,22 +543,8 @@ async function cleanup() {
}
// ─────────────────────────────────────────────────────────────────────────────
// 17. Rate-limiter — превысить mutationLimiter (30 POST/min)
// 17. Rate-limiter — перенесён в конец, после всех тестов использующих userAgent
// ─────────────────────────────────────────────────────────────────────────────
console.log('\n── 17. Rate-limiter ──');
{
// Отправляем 35 POST /add подряд с заведомо невалидным CIDR (быстро)
// Rate-limiter (30/min) должен сработать и вернуть 429
let got429 = false;
for (let i = 0; i < 35; i++) {
const csrf = await getCsrfFromIndex(userAgent);
const res = await userAgent.post('/add').type('form')
.send({ value: 'not-an-ip', comment: `rate ${i}`, _csrf: csrf });
if (res.status === 429) { got429 = true; break; }
}
log(got429, 'rate-limiter: POST /add → 429 после 30 запросов');
}
// ─────────────────────────────────────────────────────────────────────────────
// 18. IDOR — user пытается добавить запись в чужую компанию через company_id
@@ -637,6 +623,145 @@ async function cleanup() {
log(res.status === 200, 'admin: GET /audit → 200', `status=${res.status}`);
}
// ─────────────────────────────────────────────────────────────────────────────
// 21. IPv6 адреса — валидация
// ─────────────────────────────────────────────────────────────────────────────
console.log('\n── 21. IPv6 ──');
{
const csrf = await getCsrfFromIndex(userAgent);
const res = await userAgent.post('/add').type('form')
.send({ value: '2001:db8::1', comment: 'ipv6 test', _csrf: csrf });
const err = getRedirectParam(res, 'error');
const msg = getRedirectParam(res, 'message');
// Либо принимает IPv6, либо отклоняет как несуппортируемый формат
log(res.status === 302, 'user: IPv6 → redirect (не падает 500)', `status=${res.status} msg=${msg} err=${err}`);
// Чистим если добавился
await pool.query(
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
WHERE value_cidr LIKE '%2001:db8%' AND deleted_at IS NULL`
);
}
// ─────────────────────────────────────────────────────────────────────────────
// 22. Граничные CIDR — /0 и /32
// ─────────────────────────────────────────────────────────────────────────────
console.log('\n── 22. Граничные CIDR ──');
{
// /0 — весь интернет, должно быть отклонено или принято
const csrf = await getCsrfFromIndex(userAgent);
const res = await userAgent.post('/add').type('form')
.send({ value: '0.0.0.0/0', comment: 'весь интернет', _csrf: csrf });
const err = getRedirectParam(res, 'error');
log(res.status === 302, 'user: 0.0.0.0/0 → redirect (не 500)', `err=${err}`);
// Чистим
await pool.query(
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
WHERE value_cidr = '0.0.0.0/0' AND deleted_at IS NULL`
);
}
{
// /32 с host-битами — нормализация не нужна, должен добавиться как есть
const csrf = await getCsrfFromIndex(userAgent);
const res = await userAgent.post('/add').type('form')
.send({ value: '5.5.5.5/32', comment: 'slash32', _csrf: csrf });
const msg = getRedirectParam(res, 'message');
const err = getRedirectParam(res, 'error');
log(!err && res.status === 302, 'user: 5.5.5.5/32 → добавлено', `msg=${msg} err=${err}`);
// Чистим
await pool.query(
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
WHERE value_cidr = '5.5.5.5/32' AND deleted_at IS NULL`
);
}
// ─────────────────────────────────────────────────────────────────────────────
// 24. Race condition — одновременные запросы на /add при лимите
// (используем свежий агент, не отравленный rate-limit)
// ─────────────────────────────────────────────────────────────────────────────
console.log('\n── 24. Race condition: лимит ──');
{
const { MOCK_USERS } = require('../src/config');
const testUser = MOCK_USERS.find(u => u.id === 'test');
const uRes = await pool.query(
`SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [testUser?.clientId]
);
const userCompId = uRes.rows[0]?.id;
if (userCompId) {
// Свежий агент — не отравлен rate-limiter
const raceAgent = await loginAs(app, 'test');
const cntRes = await pool.query(
`SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
[userCompId]
);
const count = cntRes.rows[0].c;
const raceLimit = count + 1;
const csrf = await getCsrfFromIndex(adminAgent);
await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
.send({ limit: String(raceLimit), _csrf: csrf });
// 5 параллельных запросов — только 1 должен пройти
const raceCidrs = ['11.11.11.1', '11.11.11.2', '11.11.11.3', '11.11.11.4', '11.11.11.5'];
const results = await Promise.all(raceCidrs.map(async (cidr) => {
const c = await getCsrfFromIndex(raceAgent);
const r = await raceAgent.post('/add').type('form')
.send({ value: cidr, comment: 'race', _csrf: c });
const st = r.status;
return { cidr, status: st, err: getRedirectParam(r, 'error'), msg: getRedirectParam(r, 'message') };
}));
// Считаем фактически добавленные в БД
const dbRes = await pool.query(
`SELECT COUNT(*)::int AS c FROM whitelist_entries
WHERE company_id = $1 AND value_cidr IN ('11.11.11.1/32','11.11.11.2/32','11.11.11.3/32','11.11.11.4/32','11.11.11.5/32')
AND deleted_at IS NULL`, [userCompId]
);
const addedInDb = dbRes.rows[0].c;
log(addedInDb <= 1, 'race: в БД не более 1 записи (FOR UPDATE работает)',
`added=${addedInDb} results=${JSON.stringify(results.map(r => ({cidr: r.cidr, err: r.err?.slice(0,20)})))}`);
// Сброс лимита
const csrf2 = await getCsrfFromIndex(adminAgent);
await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
.send({ limit: '', _csrf: csrf2 });
// Чистим
await pool.query(
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
WHERE value_cidr IN ('11.11.11.1/32','11.11.11.2/32','11.11.11.3/32','11.11.11.4/32','11.11.11.5/32')
AND deleted_at IS NULL`
);
} else {
log(false, 'race: нет компании user');
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 25. Rate-limiter — превысить mutationLimiter (30 POST/min)
// Этот тест ПОСЛЕДНИЙ — исчерпывает rate-limit для userAgent
// ─────────────────────────────────────────────────────────────────────────────
console.log('\n── 25. Rate-limiter ──');
{
// Свежий агент — чтобы не зависеть от состояния userAgent
const rateAgent = await loginAs(app, 'test');
let got429 = false;
for (let i = 0; i < 35; i++) {
const csrf = await getCsrfFromIndex(rateAgent);
const res = await rateAgent.post('/add').type('form')
.send({ value: 'not-an-ip', comment: `rate ${i}`, _csrf: csrf });
if (res.status === 429) { got429 = true; break; }
}
log(got429, 'rate-limiter: POST /add → 429 после 30 запросов');
}
// ─────────────────────────────────────────────────────────────────────────────
// Cleanup
// ─────────────────────────────────────────────────────────────────────────────