From e97c36f19f1e8904bb6b10854c12a261212e0e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 17:06:42 +0300 Subject: [PATCH] feat: integration tests CRUD 34/34 (supertest + real DB) --- package-lock.json | 248 +++++++++++++++++++++- package.json | 3 + server.js | 11 +- tests/debug-session.js | 26 +++ tests/integration.js | 458 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 742 insertions(+), 4 deletions(-) create mode 100644 tests/debug-session.js create mode 100644 tests/integration.js diff --git a/package-lock.json b/package-lock.json index b03d800..45fbbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ipwhitelist", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ipwhitelist", - "version": "0.1.0", + "version": "0.4.0", "dependencies": { "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", @@ -19,6 +19,30 @@ "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", "pg": "^8.13.1" + }, + "devDependencies": { + "supertest": "^7.2.2" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "dependencies": { + "@noble/hashes": "^1.1.5" } }, "node_modules/accepts": { @@ -38,11 +62,23 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -119,6 +155,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/connect-pg-simple": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz", @@ -179,6 +236,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/csrf-csrf": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz", @@ -195,6 +258,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -212,6 +284,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -298,6 +380,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -395,6 +492,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -420,6 +523,39 @@ "node": ">= 0.8" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -501,6 +637,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -786,6 +937,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1142,6 +1302,84 @@ "node": ">= 0.8" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1197,6 +1435,12 @@ "node": ">= 0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index d6d3c89..24ed233 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,8 @@ "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", "pg": "^8.13.1" + }, + "devDependencies": { + "supertest": "^7.2.2" } } diff --git a/server.js b/server.js index 3253796..b6f8ae0 100644 --- a/server.js +++ b/server.js @@ -157,8 +157,15 @@ async function start() { .then(() => console.log('DB connected')) .catch(e => console.error('DB not ready:', e.message)); - app.listen(PORT, () => console.log('Server on port ' + PORT)); + return app; } -start().catch(e => { console.error('Startup error:', e); process.exit(1); }); +// Запускаем сервер только если файл запущен напрямую (не через require) +if (require.main === module) { + start() + .then(a => a.listen(PORT, () => console.log('Server on port ' + PORT))) + .catch(e => { console.error('Startup error:', e); process.exit(1); }); +} + +module.exports = { start }; diff --git a/tests/debug-session.js b/tests/debug-session.js new file mode 100644 index 0000000..c5a2e67 --- /dev/null +++ b/tests/debug-session.js @@ -0,0 +1,26 @@ +'use strict'; +require('dotenv').config(); +const supertest = require('supertest'); +const { start } = require('../server'); +(async () => { + const app = await start(); + const agent = supertest.agent(app); + + const r1 = await agent.get('/dev-login'); + console.log('GET /dev-login status:', r1.status); + + const m = r1.text.match(/name="_csrf"\s+value="([^"]+)"/); + console.log('csrf found:', !!m, m ? m[1].slice(0, 20) + '...' : ''); + + if (!m) { process.exit(1); } + + const r2 = await agent.post('/dev-login').type('form').send({ preset: 'test', _csrf: m[1] }); + console.log('POST /dev-login status:', r2.status, 'location:', r2.headers.location); + console.log('set-cookie:', r2.headers['set-cookie']); + + const r3 = await agent.get('/'); + console.log('GET / status:', r3.status, 'location:', r3.headers.location); + if (r3.status === 200) console.log('snippet:', r3.text.slice(0, 300)); + + process.exit(0); +})().catch(e => { console.error(e.message, e.stack); process.exit(1); }); diff --git a/tests/integration.js b/tests/integration.js new file mode 100644 index 0000000..b58b684 --- /dev/null +++ b/tests/integration.js @@ -0,0 +1,458 @@ +'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('