'use strict'; /** * tests/integration.js — интеграционные тесты CRUD против реальной БД. * * Запускать: node tests/integration.js * Требует: .env с данными БД, DEV_MODE=true. * * Что тестируем: * - Логин через /dev-login (получение сессии) от user и admin * - GET / — страница загружается * - POST /add — создание записи * - POST /edit/:id — редактирование * - POST /delete/:id — удаление * - Изоляция: user не видит чужие записи через company_id * - Лимит: создание сверх лимита → ошибка * - Дубликат CIDR → ошибка * - Невалидный CIDR → ошибка * - Admin: может работать с любой компанией */ require('dotenv').config(); const supertest = require('supertest'); const { pool } = require('../src/db'); 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++; } } // ── Хелперы ────────────────────────────────────────────────────────────────── /** * Создаёт агент (сессию) и логинится под нужным пользователем. * Возвращает agent с уже установленными cookies. */ async function loginAs(app, userId) { const agent = supertest.agent(app); // GET /dev-login → берём CSRF-токен из скрытого поля формы const getRes = await agent.get('/dev-login'); if (getRes.status !== 200) throw new Error(`GET /dev-login: ${getRes.status}`); const csrfMatch = getRes.text.match(/name="_csrf"\s+value="([^"]+)"/); if (!csrfMatch) throw new Error('CSRF-токен не найден в форме /dev-login'); const csrfToken = csrfMatch[1]; // POST /dev-login — логинимся const postRes = await agent .post('/dev-login') .type('form') .send({ preset: userId, _csrf: csrfToken }); // Ожидаем редирект на / if (postRes.status !== 302 && postRes.status !== 200) { throw new Error(`POST /dev-login: ${postRes.status} ${postRes.text.slice(0, 200)}`); } return agent; } /** * Получает CSRF-токен со страницы / (нужен для POST /add, /edit, /delete). */ async function getCsrfFromIndex(agent) { const res = await agent.get('/'); if (res.status !== 200) throw new Error(`GET /: ${res.status}`); const m = res.text.match(/name="_csrf"\s+value="([^"]+)"/); if (!m) throw new Error('CSRF-токен не найден на главной странице'); return m[1]; } /** * Извлекает query-параметр из Location-заголовка редиректа. */ function getRedirectParam(res, param) { const loc = res.headers.location || ''; try { // Location может быть относительным — добавим базу const url = new URL(loc, 'http://localhost'); return url.searchParams.get(param); } catch { return null; } } // ── Тестовые данные ───────────────────────────────────────────────────────── // Используем фиксированные публичные IP из диапазона TEST-NET, не в BLOCKED_RANGES. // Cleanup удаляет их по company после тестов. const TEST_CIDR_1 = '8.8.4.4'; // добавим, проверим дубликат и удалим const TEST_CIDR_2 = '9.9.9.5/24'; // нормализуется в 9.9.9.0/24 const TEST_CIDR_OVERLAP = '9.9.9.100'; // пересекается с 9.9.9.0/24 const TEST_CIDR_ADMIN = '1.1.1.100'; // admin добавит в компанию user const TEST_CLIENT_IDS = ['TEST-INTEG-001', 'TEST-INTEG-002']; // ── Чистка тестовых данных ──────────────────────────────────────────────────── async function cleanup() { // Удаляем конкретные тестовые CIDR из компаний WZ01325 и WZ01112 const testCidrs = ['8.8.4.4/32', '8.8.4.5/32', '9.9.9.0/24', '9.9.9.100/32', '1.1.1.100/32']; for (const cidr of testCidrs) { await pool.query( `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup' WHERE value_cidr = $1 AND deleted_at IS NULL`, [cidr] ); } // Удаляем тестовые компании и все их записи (каскадно через транзакцию) for (const cid of TEST_CLIENT_IDS) { const comp = await pool.query('SELECT id FROM companies WHERE client_id = $1', [cid]); if (comp.rows.length === 0) continue; const compId = comp.rows[0].id; await pool.query('DELETE FROM audit_log WHERE company_id = $1', [compId]); await pool.query('DELETE FROM whitelist_entries WHERE company_id = $1', [compId]); await pool.query('DELETE FROM companies WHERE id = $1', [compId]); } } // ── Основной тест-раннер ────────────────────────────────────────────────────── (async () => { console.log('\n══════════════════════════════════════════════'); console.log(' Интеграционные тесты (реальная БД)'); console.log('══════════════════════════════════════════════\n'); // Запускаем сервер let server, app; try { // server.js экспортирует app после start() const mod = require('../server'); app = await mod.start(); } catch (e) { console.error('FATAL: не удалось запустить сервер:', e.message); await pool.end(); process.exit(1); } await cleanup(); console.log('── Cleanup done ──\n'); // ───────────────────────────────────────────────────────────────────────────── // 1. Логин // ───────────────────────────────────────────────────────────────────────────── console.log('── 1. Логин ──'); let userAgent, adminAgent, user2Agent; try { userAgent = await loginAs(app, 'test'); log(true, 'login as user (test)'); } catch (e) { log(false, 'login as user (test)', e.message); } try { adminAgent = await loginAs(app, 'admin'); log(true, 'login as admin'); } catch (e) { log(false, 'login as admin', e.message); } try { user2Agent = await loginAs(app, 'client2'); log(true, 'login as user2 (client2)'); } catch (e) { log(false, 'login as user2 (client2)', e.message); } if (!userAgent || !adminAgent) { console.error('\nFATAL: не удалось залогиниться, прерываем тесты'); await cleanup(); await pool.end(); process.exit(1); } // ───────────────────────────────────────────────────────────────────────────── // 2. GET / — страница загружается // ───────────────────────────────────────────────────────────────────────────── console.log('\n── 2. GET / ──'); { const res = await userAgent.get('/'); log(res.status === 200, 'user: GET / → 200'); log(res.text.includes('Доверенные адреса'), 'user: страница содержит "Доверенные адреса"'); // company встречается в скрытых полях форм — это нормально log(!res.text.includes('