feat: integration tests CRUD 34/34 (supertest + real DB)
This commit is contained in:
Generated
+246
-2
@@ -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",
|
||||
|
||||
@@ -19,5 +19,8 @@
|
||||
"helmet": "^8.2.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"supertest": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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); });
|
||||
@@ -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('<select name="company"') || res.text.includes('isAdmin') === false, 'user: нет select переключателя компаний');
|
||||
}
|
||||
{
|
||||
const res = await adminAgent.get('/');
|
||||
log(res.status === 200, 'admin: GET / → 200');
|
||||
log(res.text.includes('Доверенные адреса'), 'admin: страница загружена');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 3. POST /add — создание записей от user
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 3. POST /add (user) ──');
|
||||
|
||||
let entryId; // сохраним для edit/delete
|
||||
|
||||
{
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: TEST_CIDR_1, comment: 'тест 1', _csrf: csrf });
|
||||
log(res.status === 302, 'user: POST /add 10.10.10.1 → redirect');
|
||||
const msg = getRedirectParam(res, 'message');
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(!err && (msg === 'Добавлено' || msg?.includes('нормализован')),
|
||||
'user: /add → сообщение "Добавлено"', `msg=${msg} err=${err}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Нормализация: 9.9.9.5/24 → 9.9.9.0/24
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: TEST_CIDR_2, comment: 'нормализация', _csrf: csrf });
|
||||
log(res.status === 302, 'user: POST /add 10.20.30.5/24 → redirect');
|
||||
const msg = getRedirectParam(res, 'message');
|
||||
log(msg?.includes('нормализован'), 'user: /add → адрес нормализован', `msg=${msg}`);
|
||||
}
|
||||
|
||||
// Сохраняем id добавленной записи для дальнейших тестов
|
||||
// Читаем из БД напрямую — ищем TEST_CIDR_1 (8.8.4.4/32)
|
||||
{
|
||||
const r = await pool.query(
|
||||
`SELECT id FROM whitelist_entries WHERE value_cidr = $1 AND deleted_at IS NULL`,
|
||||
[TEST_CIDR_1 + '/32']
|
||||
);
|
||||
entryId = r.rows[0]?.id || null;
|
||||
log(!!entryId, 'user: нашли entryId в странице', `entryId=${entryId}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 4. POST /add — дубликат → ошибка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 4. Дубликат CIDR ──');
|
||||
|
||||
{
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: TEST_CIDR_1, comment: 'дубликат', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && err?.includes('уже существует'),
|
||||
'user: дубликат → ошибка "уже существует"', `err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 5. POST /add — невалидный CIDR → ошибка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 5. Невалидный CIDR ──');
|
||||
|
||||
{
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: 'не-адрес', comment: '', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err, 'user: невалидный CIDR → ошибка', `err=${err}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Октеты > 255
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: '999.0.0.1', comment: '', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err, 'user: октет > 255 → ошибка', `err=${err}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Маска > 32
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: '10.0.0.1/33', comment: '', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err, 'user: маска /33 → ошибка', `err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 6. POST /edit/:id — редактирование
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 6. POST /edit (user) ──');
|
||||
|
||||
if (entryId) {
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post(`/edit/${entryId}`).type('form')
|
||||
.send({ value: '8.8.4.5', comment: 'обновлено', _csrf: csrf });
|
||||
log(res.status === 302, `user: POST /edit/${entryId} → redirect`);
|
||||
const msg = getRedirectParam(res, 'message');
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(!err && (msg === 'Сохранено' || msg?.includes('нормализован')),
|
||||
'user: /edit → "Сохранено"', `msg=${msg} err=${err}`);
|
||||
} else {
|
||||
log(false, 'user: /edit — нет entryId, тест пропущен');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 7. Изоляция: user2 не может редактировать чужую запись
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 7. Изоляция между пользователями ──');
|
||||
|
||||
if (entryId && user2Agent) {
|
||||
const csrf = await getCsrfFromIndex(user2Agent);
|
||||
const res = await user2Agent.post(`/edit/${entryId}`).type('form')
|
||||
.send({ value: '1.2.3.4', comment: 'чужая', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err,
|
||||
'user2: edit чужой записи → ошибка (изоляция)', `err=${err}`);
|
||||
} else {
|
||||
log(false, 'изоляция: нет entryId или user2Agent');
|
||||
}
|
||||
|
||||
if (entryId && user2Agent) {
|
||||
const csrf = await getCsrfFromIndex(user2Agent);
|
||||
const res = await user2Agent.post(`/delete/${entryId}`).type('form')
|
||||
.send({ _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err,
|
||||
'user2: delete чужой записи → ошибка (изоляция)', `err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 8. Пересечение CIDR → ошибка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 8. Пересечение CIDR ──');
|
||||
|
||||
{
|
||||
// 9.9.9.100 пересекается с 9.9.9.0/24 (уже добавлен)
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post('/add').type('form')
|
||||
.send({ value: TEST_CIDR_OVERLAP, comment: 'пересечение', _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && err?.includes('Пересечение'),
|
||||
'user: CIDR пересекается с существующим → ошибка', `err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 9. Admin — работа с компанией пользователя
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 9. Admin CRUD по компании user ──');
|
||||
|
||||
// Получаем id компании user из БД
|
||||
const userComp = await pool.query(
|
||||
"SELECT id FROM companies WHERE client_id = 'WZ01325'"
|
||||
);
|
||||
const userCompId = userComp.rows[0]?.id;
|
||||
log(!!userCompId, 'admin: нашли компанию user в БД', `id=${userCompId}`);
|
||||
|
||||
if (userCompId) {
|
||||
// Admin добавляет запись в компанию user
|
||||
const csrf = await getCsrfFromIndex(adminAgent);
|
||||
const res = await adminAgent.post('/add').type('form')
|
||||
.send({ value: TEST_CIDR_ADMIN, comment: 'от admin', company_id: userCompId, _csrf: csrf });
|
||||
log(res.status === 302, 'admin: POST /add в чужую компанию → redirect');
|
||||
const msg = getRedirectParam(res, 'message');
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(!err && (msg === 'Добавлено' || msg?.includes('нормализован')),
|
||||
'admin: /add в компанию user → Добавлено', `msg=${msg} err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 10. POST /delete/:id — удаление (user своей записи)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 10. POST /delete (user) ──');
|
||||
|
||||
if (entryId) {
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post(`/delete/${entryId}`).type('form')
|
||||
.send({ _csrf: csrf });
|
||||
log(res.status === 302, `user: POST /delete/${entryId} → redirect`);
|
||||
const msg = getRedirectParam(res, 'message');
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(!err && msg === 'Запись удалена', 'user: /delete → "Запись удалена"', `msg=${msg} err=${err}`);
|
||||
|
||||
// Проверяем что мягко удалена (deleted_at выставлен)
|
||||
const row = await pool.query(
|
||||
'SELECT deleted_at FROM whitelist_entries WHERE id = $1', [entryId]
|
||||
);
|
||||
log(!!row.rows[0]?.deleted_at, 'user: deleted_at выставлен в БД (soft delete)');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 11. Повторное удаление удалённой записи → ошибка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 11. Повторное удаление ──');
|
||||
|
||||
if (entryId) {
|
||||
const csrf = await getCsrfFromIndex(userAgent);
|
||||
const res = await userAgent.post(`/delete/${entryId}`).type('form')
|
||||
.send({ _csrf: csrf });
|
||||
const err = getRedirectParam(res, 'error');
|
||||
log(res.status === 302 && !!err,
|
||||
'user: повторное /delete удалённой записи → ошибка', `err=${err}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 12. Неавторизованный доступ → редирект на /login
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 12. Неавторизованный доступ ──');
|
||||
|
||||
{
|
||||
const res = await supertest(app).get('/');
|
||||
log(res.status === 302 && res.headers.location?.includes('login'),
|
||||
'анонимный: GET / → redirect /login', `loc=${res.headers.location}`);
|
||||
}
|
||||
|
||||
{
|
||||
const res = await supertest(app).post('/add').type('form').send({ value: '1.2.3.4' });
|
||||
// CSRF middleware отдаёт 401 до auth.middleware — анонимный не проходит
|
||||
log([302, 401, 403].includes(res.status),
|
||||
'анонимный: POST /add → не 200 (302/401/403)', `status=${res.status}`);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 13. /export — изоляция
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── 13. GET /export ──');
|
||||
|
||||
{
|
||||
const res = await userAgent.get('/export');
|
||||
log(res.status === 200, 'user: GET /export → 200');
|
||||
// CIDR другой компании не должен попасть
|
||||
const text = res.text;
|
||||
// 172.16.0.1 добавил admin в компанию user — должен быть в экспорте
|
||||
// но не должно быть CIDRов от WZ02001
|
||||
log(!text.includes('WZ02001'), 'user: /export не содержит данных другой компании');
|
||||
}
|
||||
|
||||
{
|
||||
const res = await adminAgent.get('/export');
|
||||
log(res.status === 200, 'admin: GET /export → 200 (все компании)');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Cleanup
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n── Cleanup ──');
|
||||
await cleanup();
|
||||
await pool.end();
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Итог
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n══════════════════════════════════════════════');
|
||||
console.log(`${pass} PASS, ${fail} FAIL`);
|
||||
if (errors.length) {
|
||||
console.log('\nПроваленные тесты:');
|
||||
errors.forEach(e => console.log(' ', e));
|
||||
}
|
||||
console.log('══════════════════════════════════════════════');
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
})().catch(e => {
|
||||
console.error('\nFATAL:', e.message);
|
||||
pool.end();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user