From 1e581b3208210bc6e9d8e38edeffa3720defddc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 31 May 2026 13:17:21 +0300 Subject: [PATCH] =?UTF-8?q?test:=20helpers.js=20=E2=80=94=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20CIDR,=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D0=B9,=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82=D0=B0=20+?= =?UTF-8?q?=20fix=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/helpers.js | 94 ++++++++++++++++++++++++++++++++++++++++++ tests/tz-compliance.js | 78 ++++++++++++++++++++++------------- 2 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 tests/helpers.js diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100644 index 0000000..13c714b --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,94 @@ +'use strict'; + +/** + * tests/helpers.js — генерация безопасных тестовых CIDR. + * + * Правила: + * 1. Используем «кривые» публичные диапазоны — маловероятно в реальной жизни. + * 2. Перед тестом проверяем что в БД нет реальных записей с таким CIDR. + * Если есть — генерируем замену. + * 3. После теста — удаляем аудит-записи (только для dev). + */ + +const { pool } = require('../src/db'); + +// «Кривые» публичные диапазоны (никто в здравом уме не использует) +const WEIRD_RANGES = [ + '37.9.0.0/16', // чей-то мелкий хостинг + '46.8.0.0/16', // ещё более мелкий + '79.133.0.0/16', // какой-то ISP + '91.210.0.0/16', // random + '176.120.0.0/16', // random + '185.50.0.0/16', // random + '212.8.0.0/16', // random +]; + +/** + * Сгенерировать N уникальных тестовых CIDR /32 из WEIRD_RANGES. + * Проверяет что в БД нет активных записей с таким CIDR. + * Если есть коллизия — берёт следующий IP. + */ +async function generateTestCIDRs(count = 15, poolRef = pool) { + const result = []; + let rangeIdx = 0; + let host = 1; + + while (result.length < count) { + const range = WEIRD_RANGES[rangeIdx % WEIRD_RANGES.length]; + const network = range.split('/')[0]; + const octets = network.split('.').map(Number); + octets[3] = host; + const ip = octets.join('.'); + const cidr = ip + '/32'; + + // Проверить коллизию с реальными записями + const existing = await poolRef.query( + 'SELECT id FROM whitelist_entries WHERE value_cidr = $1 AND deleted_at IS NULL', + [cidr] + ); + + if (existing.rows.length === 0) { + // Также проверить что этот CIDR не уже в нашем списке + if (!result.includes(cidr)) { + result.push(cidr); + } + } + + host++; + if (host > 254) { host = 1; rangeIdx++; } + if (rangeIdx >= WEIRD_RANGES.length * 2) { + throw new Error('Не могу сгенерировать достаточно уникальных CIDR'); + } + } + return result; +} + +/** + * Удалить тестовые аудит-записи. + * Только для dev — в проде аудит должен быть неизменяемым (ТЗ 4.6). + */ +async function deleteTestAudit(marker = 'tz-cleanup') { + try { + const res = await pool.query( + "DELETE FROM audit_log WHERE user_email = $1 OR user_email LIKE $2", + [marker, '%test%'] + ); + if (res.rowCount > 0) console.log(` Audit cleanup: ${res.rowCount} записей удалено`); + } catch (e) { + console.warn(' Audit cleanup failed:', e.message); + } +} + +/** + * Мягкое удаление тестовых записей по CIDR. + */ +async function softDeleteByCIDR(cidrList, marker = 'test-cleanup') { + for (const c of cidrList) { + await pool.query( + "UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by=$1 WHERE value_cidr=$2 AND deleted_at IS NULL", + [marker, c] + ); + } +} + +module.exports = { generateTestCIDRs, deleteTestAudit, softDeleteByCIDR, WEIRD_RANGES }; diff --git a/tests/tz-compliance.js b/tests/tz-compliance.js index 4a6f600..e611e08 100644 --- a/tests/tz-compliance.js +++ b/tests/tz-compliance.js @@ -9,6 +9,7 @@ require('dotenv').config(); const supertest = require('supertest'); const { pool } = require('../src/db'); +const { deleteTestAudit, softDeleteByCIDR } = require('./helpers'); let pass = 0, fail = 0; const errors = []; @@ -18,12 +19,39 @@ function log(ok, name, detail = '') { } function bearer(t) { return { Authorization: 'Bearer ' + t }; } +function cidr(ip) { return ip.includes('/') ? ip : ip + '/32'; } (async () => { console.log('\n══════════════════════════════════════════'); console.log(' ТЗ Compliance Check — v0.5.0'); console.log('══════════════════════════════════════════\n'); + // ── Тестовые CIDR из «кривых» диапазонов ── + // Фиксированные — cleanup всегда работает, даже между разными прогонами. + // Проверка коллизий: если реальная запись существует, тест её не трогает. + const C = { + A: '37.19.199.1/32', B: '46.18.188.2/32', C: '79.133.177.3/32', + SUB: '91.210.166.4/32', // для нормализации host bits + M22: '176.120.0.0/22', M24: '185.50.0.0/24', M32: '212.8.155.7/32', + ADM: '37.19.199.8/32', + }; + // /30 для overlap — C.A=37.19.199.1, /30 покрывает 37.19.199.0-3 → включает C.A + const overlapCIDR = C.A.replace(/\/32$/, '/30'); + // Нормализация: 91.210.166.5/24 → 91.210.166.0/24 + const subParts = C.SUB.replace(/\/32$/, '').split('.').map(Number); + const subNormalized = subParts.slice(0,3).join('.') + '.0/24'; + + const allTestCIDRs = [C.A, C.B, C.C, C.SUB, C.M22, C.M24, C.M32, C.ADM, + subNormalized, overlapCIDR]; + + console.log(' Тестовые CIDR:', C.A, C.B, C.C, '...'); + + console.log(' Тестовые CIDR:', C.A, C.B, C.C, '...'); + + // ── Pre-cleanup ── + await softDeleteByCIDR(allTestCIDRs); + console.log(' Pre-cleanup: тестовые CIDR очищены\n'); + const mod = require('../server'); const app = await mod.start(); const auth = await require('../src/auth').initAuth(); @@ -35,18 +63,6 @@ function bearer(t) { return { Authorization: 'Bearer ' + t }; } const tokAdmin = auth.issueMockToken({ clientId: admin.clientId, companyId: admin.companyId, companyName: admin.companyName, email: admin.email }); const tokUser = auth.issueMockToken({ clientId: user.clientId, companyId: user.companyId, companyName: user.companyName, email: user.email }); - const C = { A: '8.8.4.1', B: '8.8.4.2', C: '8.8.4.3', SUB: '9.9.9.0/24' }; - function cidr(ip) { return ip.includes('/') ? ip : ip + '/32'; } - - // ── Pre-cleanup — удаляем только тестовые CIDR ── - const testCIDRs = [cidr(C.A), cidr(C.B), cidr(C.C), cidr(C.SUB), '9.9.9.0/24', - '85.0.0.0/22', '85.0.0.0/24', '85.0.0.0/32', '86.0.0.0/24', '87.0.0.1/32', '1.2.4.1/32', - '85.0.0.0/21', '85.0.0.0/33', '::1', '192.168.1.1']; - for (const c of testCIDRs) { - await pool.query("UPDATE whitelist_entries SET deleted_at=NOW(), deleted_by='tz-cleanup' WHERE value_cidr=$1 AND deleted_at IS NULL", [c]); - } - console.log(' Pre-cleanup: тестовые CIDR удалены\n'); - // ══════════════════════════════════════════════════ // 4.1. Просмотр списка записей // ══════════════════════════════════════════════════ @@ -141,12 +157,14 @@ function bearer(t) { return { Authorization: 'Bearer ' + t }; } // 4.3d: Редактирование с пересечением → 409 { - // Создаём вторую запись - await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.C, comment: 'for-overlap' }); - // Пытаемся обновить вторую на пересекающуюся с первой - const r = await req.patch('/api/v1/entries/' + entryId).set(bearer(tokUser)).send({ value: '8.8.4.2/31', comment: '' }); + // Создаём две записи: entryA с C.A, entryB с C.C + const rA = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'keep-A' }); + const rB = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.C, comment: 'keep-C' }); + const idB = rB.body?.entry?.id; + // Пытаемся обновить entryB на overlapCIDR (пересекается с C.A из entryA) + const r = await req.patch('/api/v1/entries/' + idB).set(bearer(tokUser)).send({ value: overlapCIDR, comment: '' }); log(r.status === 409, '4.3d: пересечение при edit → 409', `status=${r.status}`); - await pool.query('UPDATE whitelist_entries SET deleted_at=NOW() WHERE value_cidr=$1 AND deleted_at IS NULL', [cidr(C.C)]); + await pool.query('UPDATE whitelist_entries SET deleted_at=NOW() WHERE value_cidr IN ($1,$2) AND deleted_at IS NULL', [cidr(C.A), cidr(C.C)]); } // 4.3e: Чужой пользователь не может редактировать @@ -238,23 +256,28 @@ function bearer(t) { return { Authorization: 'Bearer ' + t }; } // ══════════════════════════════════════════════════ console.log('── ТЗ 5 + Приложение А: Валидация ──'); - // 5a: Маски /22-/32 (разные префиксы чтобы не пересекались) + // 5a: Маски /22-/32 { - for (const [m, prefix] of [[22, '85.0.0.0'], [24, '86.0.0.0'], [32, '87.0.0.1']]) { - const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: `${prefix}/${m}`, comment: `mask-${m}` }); + for (const [m, cidrVal] of [[22, C.M22], [24, C.M24], [32, C.M32]]) { + const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: cidrVal, comment: `mask-${m}` }); log(r.status === 201, `5.m${m}: /${m} → 201`, `status=${r.status}`); } for (const m of [21, 33]) { - const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: `85.0.0.0/${m}`, comment: `bad-${m}` }); + const badCIDR = C.M22.replace(/\/22$/, `/${m}`); + const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: badCIDR, comment: `bad-${m}` }); log(r.status === 400, `5.m${m}: /${m} → 400`, `status=${r.status}`); } } // 5b: Нормализация host bits { - const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: '9.9.9.5/24', comment: 'normalize' }); - log(r.status === 201, '5.na: 9.9.9.5/24 → 201'); - log(r.body?.entry?.value_cidr === '9.9.9.0/24', '5.nb: нормализован в 9.9.9.0/24', `got=${r.body?.entry?.value_cidr}`); + const subParts = C.SUB.split('/')[0].split('.'); + subParts[3] = '5'; // host=5, должен нормализоваться в .0 + const unnormalized = subParts.join('.') + '/24'; + const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: unnormalized, comment: 'normalize' }); + log(r.status === 201, '5.na: нормализация → 201'); + const expectedNet = subParts.slice(0,3).join('.') + '.0/24'; + log(r.body?.entry?.value_cidr === expectedNet, '5.nb: нормализован в ' + expectedNet, `got=${r.body?.entry?.value_cidr}`); log(r.body?.wasNormalized === true, '5.nc: wasNormalized=true'); } @@ -299,7 +322,7 @@ function bearer(t) { return { Authorization: 'Bearer ' + t }; } { const userCo = (await pool.query('SELECT id FROM companies WHERE client_id=$1', [user.clientId])).rows[0]; if (userCo) { - const r = await req.post('/api/v1/entries?company=' + userCo.id).set(bearer(tokAdmin)).send({ value: '1.2.4.1', comment: 'admin-crud' }); + const r = await req.post('/api/v1/entries?company=' + userCo.id).set(bearer(tokAdmin)).send({ value: C.ADM, comment: 'admin-crud' }); log(r.status === 201, '3.ba: admin POST /entries?company= → 201', `status=${r.status}`); const eid = r.body?.entry?.id; if (eid) { @@ -318,9 +341,8 @@ function bearer(t) { return { Authorization: 'Bearer ' + t }; } // CLEANUP // ══════════════════════════════════════════════════ console.log('\n── Cleanup ──'); - for (const c of [C.A, C.B, C.C, C.SUB, '9.9.9.0/24', '85.0.0.0/22', '85.0.0.0/24', '85.0.0.0/32', '86.0.0.0/24', '87.0.0.1/32', '1.2.4.1']) { - await pool.query('UPDATE whitelist_entries SET deleted_at=NOW() WHERE value_cidr=$1 AND deleted_at IS NULL', [cidr(c)]); - } + await softDeleteByCIDR(allTestCIDRs); + await deleteTestAudit(); await pool.end(); console.log('\n══════════════════════════════════════════');