Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87216389e3 | ||
|
|
09bb56c6f4 | ||
|
|
7d2b6a309f | ||
|
|
1e330a4395 | ||
|
|
91d18afe10 | ||
|
|
1f5c0d8623 | ||
|
|
8ab7eed629 | ||
|
|
e97c36f19f | ||
|
|
093b96b9ae | ||
|
|
0845440f86 | ||
|
|
97c3f66b3c |
Generated
+246
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
@@ -19,6 +19,30 @@
|
|||||||
"helmet": "^8.2.0",
|
"helmet": "^8.2.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.13.1"
|
"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": {
|
"node_modules/accepts": {
|
||||||
@@ -38,11 +62,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"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": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -119,6 +155,27 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/connect-pg-simple": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
|
"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": {
|
"node_modules/csrf-csrf": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
|
||||||
@@ -195,6 +258,15 @@
|
|||||||
"ms": "2.0.0"
|
"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": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -212,6 +284,16 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.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": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -298,6 +380,21 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@@ -395,6 +492,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||||
@@ -420,6 +523,39 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -501,6 +637,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
@@ -786,6 +937,15 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1142,6 +1302,84 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
@@ -1197,6 +1435,12 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -19,5 +19,8 @@
|
|||||||
"helmet": "^8.2.0",
|
"helmet": "^8.2.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"supertest": "^7.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ async function start() {
|
|||||||
authLimiter,
|
authLimiter,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ── REST API v1 (Bearer JWT, без сессий/CSRF) ─────────────────────────────
|
||||||
|
const { createApiRouter } = require('./src/api/index');
|
||||||
|
app.use('/api/v1', createApiRouter({ auth, q }));
|
||||||
|
|
||||||
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
|
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
|
||||||
app.use(auth.middleware);
|
app.use(auth.middleware);
|
||||||
|
|
||||||
@@ -157,8 +161,15 @@ async function start() {
|
|||||||
.then(() => console.log('DB connected'))
|
.then(() => console.log('DB connected'))
|
||||||
.catch(e => console.error('DB not ready:', e.message));
|
.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,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* src/api/index.js — REST API v1.
|
||||||
|
*
|
||||||
|
* Монтируется в server.js ДО общего auth.middleware:
|
||||||
|
* app.use('/api/v1', createApiRouter({ auth, q }))
|
||||||
|
*
|
||||||
|
* Аутентификация: только Bearer JWT (без сессий, без CSRF).
|
||||||
|
* Ответы: JSON везде.
|
||||||
|
* Переиспользует: src/queries.js, src/auth.js (middleware для валидации токена).
|
||||||
|
*
|
||||||
|
* Эндпоинты:
|
||||||
|
* GET /api/v1/entries — список записей компании
|
||||||
|
* POST /api/v1/entries — создать запись
|
||||||
|
* PATCH /api/v1/entries/:id — обновить запись
|
||||||
|
* DELETE /api/v1/entries/:id — удалить запись
|
||||||
|
* GET /api/v1/companies — все компании (admin)
|
||||||
|
* PATCH /api/v1/companies/:id/limit — установить лимит (admin)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const { requireBearer } = require('./middleware/bearerAuth');
|
||||||
|
const { createEntriesRouter } = require('./routes/entries');
|
||||||
|
const { createAdminRouter } = require('./routes/admin');
|
||||||
|
|
||||||
|
function createApiRouter({ auth, q }) {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Все /api/v1/* требуют Bearer — 401 JSON если нет токена
|
||||||
|
router.use(requireBearer);
|
||||||
|
// Валидация токена — переиспользуем auth.middleware из src/auth.js
|
||||||
|
router.use(auth.middleware);
|
||||||
|
|
||||||
|
router.use('/entries', createEntriesRouter({ q }));
|
||||||
|
router.use('/', createAdminRouter({ q }));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createApiRouter };
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware для API: требует Bearer-токен в Authorization.
|
||||||
|
* Если токена нет — сразу 401 JSON (без редиректа на /login).
|
||||||
|
* Валидацию токена делает следующий в цепочке auth.middleware.
|
||||||
|
*/
|
||||||
|
function requireBearer(req, res, next) {
|
||||||
|
const header = (req.headers.authorization || '').trim();
|
||||||
|
if (!/^Bearer\s+\S/i.test(header)) {
|
||||||
|
return res.status(401).json({ error: 'Bearer token required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { requireBearer };
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
// Отдельный requireAdmin для API — возвращает JSON вместо plain text
|
||||||
|
function apiRequireAdmin(req, res, next) {
|
||||||
|
if (!req.user || !req.user.isAdmin) {
|
||||||
|
return res.status(403).json({ error: 'Admin role required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAdminRouter({ q }) {
|
||||||
|
const router = express.Router();
|
||||||
|
const json = express.json({ limit: '8kb' });
|
||||||
|
|
||||||
|
// GET /api/v1/companies — все компании (admin only)
|
||||||
|
router.get('/companies', apiRequireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const companies = await q.getAllCompanies();
|
||||||
|
res.json({ companies });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH /api/v1/companies/:id/limit — установить лимит (admin only)
|
||||||
|
router.patch('/companies/:id/limit', apiRequireAdmin, json, async (req, res) => {
|
||||||
|
const limit = parseInt((req.body || {}).limit, 10);
|
||||||
|
if (isNaN(limit) || limit < 0) {
|
||||||
|
return res.status(400).json({ error: 'limit must be non-negative integer' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await q.setLimit(req.params.id, limit);
|
||||||
|
res.json({ ok: true, limit });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createAdminRouter };
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
function createEntriesRouter({ q }) {
|
||||||
|
const router = express.Router();
|
||||||
|
const json = express.json({ limit: '32kb' });
|
||||||
|
|
||||||
|
// ── Хелпер: маппинг сообщений ошибок на HTTP-коды ───────────────────────
|
||||||
|
function handleQueryError(e, res) {
|
||||||
|
const msg = e.message || '';
|
||||||
|
if (
|
||||||
|
msg.includes('Лимит исчерпан') ||
|
||||||
|
msg.includes('Такой адрес уже существует') ||
|
||||||
|
msg.includes('Пересечение с существующей записью')
|
||||||
|
) {
|
||||||
|
return res.status(409).json({ error: msg });
|
||||||
|
}
|
||||||
|
if (msg.includes('Запись не найдена')) {
|
||||||
|
return res.status(404).json({ error: msg });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: msg });
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/entries — список записей текущей компании
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||||
|
const entries = await q.listEntries(company.id);
|
||||||
|
res.json({ entries });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/v1/entries — создать запись
|
||||||
|
router.post('/', json, async (req, res) => {
|
||||||
|
const { value, comment } = req.body || {};
|
||||||
|
if (!value) return res.status(400).json({ error: 'value required' });
|
||||||
|
try {
|
||||||
|
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||||
|
const { entry, wasNormalized } = await q.createEntry(company.id, value, comment || '', req.user.email);
|
||||||
|
res.status(201).json({ entry, wasNormalized });
|
||||||
|
} catch (e) {
|
||||||
|
handleQueryError(e, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH /api/v1/entries/:id — обновить запись
|
||||||
|
router.patch('/:id', json, async (req, res) => {
|
||||||
|
const { value, comment } = req.body || {};
|
||||||
|
if (!value) return res.status(400).json({ error: 'value required' });
|
||||||
|
try {
|
||||||
|
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||||
|
const { entry, wasNormalized } = await q.updateEntry(req.params.id, company.id, value, comment ?? '', req.user.email);
|
||||||
|
res.json({ entry, wasNormalized });
|
||||||
|
} catch (e) {
|
||||||
|
handleQueryError(e, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/v1/entries/:id — удалить запись
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||||
|
await q.deleteEntry(req.params.id, company.id, req.user.email);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (e) {
|
||||||
|
handleQueryError(e, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createEntriesRouter };
|
||||||
@@ -42,6 +42,16 @@ function initCsrf() {
|
|||||||
// Если сессии нет — пустая строка (допустимо для /login до установки сессии).
|
// Если сессии нет — пустая строка (допустимо для /login до установки сессии).
|
||||||
getSessionIdentifier: (req) => req.sessionID || '',
|
getSessionIdentifier: (req) => req.sessionID || '',
|
||||||
|
|
||||||
|
// ⚠️ csrf-csrf v4 по умолчанию читает токен из заголовка x-csrf-token.
|
||||||
|
// Наши формы (EJS) отправляют его в теле POST как поле _csrf.
|
||||||
|
// Без этой строки токен не находится → 403 на каждый POST.
|
||||||
|
getCsrfTokenFromRequest: (req) => req.body && req.body._csrf,
|
||||||
|
|
||||||
|
// Имя cookie без префикса __Host- (дефолт __Host-psifi.x-csrf-token
|
||||||
|
// требует Secure-флаг; при NODE_ENV≠production secure:false → cookie
|
||||||
|
// отклоняется браузером по HTTPS и валидация падает).
|
||||||
|
cookieName: 'csrf-token',
|
||||||
|
|
||||||
cookieOptions: {
|
cookieOptions: {
|
||||||
// secure: true → cookie передаётся только по HTTPS. Важно в продакшене.
|
// secure: true → cookie передаётся только по HTTPS. Важно в продакшене.
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ function createSessionMiddleware(pool) {
|
|||||||
store,
|
store,
|
||||||
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
|
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
// true: сессия сохраняется даже пустой (нужно чтобы sessionID не менялся
|
||||||
|
// между GET /login и POST /login — иначе CSRF-токен привязанный к sessionID
|
||||||
|
// из GET не совпадёт с sessionID на POST и будет 403).
|
||||||
|
saveUninitialized: true,
|
||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production', // HTTPS only в проде
|
secure: process.env.NODE_ENV === 'production', // HTTPS only в проде
|
||||||
|
|||||||
@@ -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,949 @@
|
|||||||
|
'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 (все компании)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 14. Лимит — добавить сверх лимита → ошибка
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 14. Лимит ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
// client_id пользователя 'test' — WZ01325 (из MOCK_USERS)
|
||||||
|
const { MOCK_USERS } = require('../src/config');
|
||||||
|
const testUser = MOCK_USERS.find(u => u.id === 'test');
|
||||||
|
const userClientId = testUser?.clientId;
|
||||||
|
|
||||||
|
const uRes = await pool.query(
|
||||||
|
`SELECT id FROM companies WHERE client_id = $1 LIMIT 1`,
|
||||||
|
[userClientId]
|
||||||
|
);
|
||||||
|
const userCompanyId = uRes.rows[0]?.id;
|
||||||
|
|
||||||
|
if (userCompanyId) {
|
||||||
|
// Считаем активные записи компании
|
||||||
|
const cntRes = await pool.query(
|
||||||
|
`SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
|
||||||
|
[userCompanyId]
|
||||||
|
);
|
||||||
|
const currentCount = cntRes.rows[0].c;
|
||||||
|
|
||||||
|
// Ставим лимит = текущее количество записей → следующее добавление упадёт
|
||||||
|
const csrf = await getCsrfFromIndex(adminAgent);
|
||||||
|
const limitRes = await adminAgent.post(`/admin/limit/${userCompanyId}`).type('form')
|
||||||
|
.send({ limit: String(currentCount), _csrf: csrf });
|
||||||
|
log(limitRes.status === 302, 'admin: выставил лимит=currentCount для user', `status=${limitRes.status} count=${currentCount}`);
|
||||||
|
|
||||||
|
// Добавляем запись — должна упасть с "Лимит исчерпан"
|
||||||
|
const csrf3 = await getCsrfFromIndex(userAgent);
|
||||||
|
const r2 = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '8.8.4.100', comment: 'лимит тест', _csrf: csrf3 });
|
||||||
|
const err2 = getRedirectParam(r2, 'error');
|
||||||
|
log(err2?.includes('Лимит'), 'user: лимит исчерпан → ошибка', `err=${err2}`);
|
||||||
|
|
||||||
|
// Возвращаем лимит обратно (пустая строка = дефолт)
|
||||||
|
const csrf4 = await getCsrfFromIndex(adminAgent);
|
||||||
|
await adminAgent.post(`/admin/limit/${userCompanyId}`).type('form')
|
||||||
|
.send({ limit: '', _csrf: csrf4 });
|
||||||
|
|
||||||
|
// Чистим тестовые записи лимит-теста
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
|
||||||
|
WHERE value_cidr IN ('8.8.8.8/32', '8.8.4.100/32') AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log(false, 'admin: не нашли компанию user для теста лимита');
|
||||||
|
log(false, 'user: лимит исчерпан → ошибка');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 15. Audit log — операции пишутся в БД
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 15. Audit log ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
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 userCompanyId = uRes.rows[0]?.id;
|
||||||
|
if (userCompanyId) {
|
||||||
|
const r = await pool.query(
|
||||||
|
`SELECT action FROM audit_log WHERE company_id = $1 ORDER BY created_at DESC LIMIT 20`,
|
||||||
|
[userCompanyId]
|
||||||
|
);
|
||||||
|
const actions = r.rows.map(row => row.action);
|
||||||
|
log(actions.some(a => /add|create/i.test(a)),
|
||||||
|
'audit: операция add записана в audit_log', `actions=${JSON.stringify(actions.slice(0,5))}`);
|
||||||
|
log(actions.some(a => /delete/i.test(a)),
|
||||||
|
'audit: операция delete записана в audit_log', `actions=${JSON.stringify(actions.slice(0,5))}`);
|
||||||
|
} else {
|
||||||
|
log(false, 'audit: не нашли компанию user');
|
||||||
|
log(false, 'audit: не нашли компанию user (delete)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 16. Безопасность — XSS и спецсимволы в comment
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 16. Безопасность: XSS в comment ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
const xssPayload = '<script>alert(1)</script>';
|
||||||
|
const csrf = await getCsrfFromIndex(userAgent);
|
||||||
|
const res = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '5.5.5.5', comment: xssPayload, _csrf: csrf });
|
||||||
|
// Запись могла добавиться или нет (зависит от валидации comment)
|
||||||
|
const err = getRedirectParam(res, 'error');
|
||||||
|
|
||||||
|
// Если добавилась — проверяем что скрипт не выполняется в HTML
|
||||||
|
const page = await userAgent.get('/');
|
||||||
|
const rawScript = page.text.includes('<script>alert(1)</script>');
|
||||||
|
log(!rawScript, 'security: XSS в comment — скрипт не рендерится в HTML',
|
||||||
|
rawScript ? 'RAW SCRIPT FOUND — XSS!' : `escaped или отклонено (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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 17. Rate-limiter — перенесён в конец, после всех тестов использующих userAgent
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 18. IDOR — user пытается добавить запись в чужую компанию через company_id
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 18. IDOR: подделка company_id ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
// user2 (WZ02001) пытается добавить запись в компанию user (WZ01325)
|
||||||
|
const { MOCK_USERS } = require('../src/config');
|
||||||
|
const userClient = MOCK_USERS.find(u => u.id === 'test')?.clientId;
|
||||||
|
const uRes = await pool.query(
|
||||||
|
`SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [userClient]
|
||||||
|
);
|
||||||
|
const userCompId = uRes.rows[0]?.id;
|
||||||
|
|
||||||
|
if (userCompId && user2Agent) {
|
||||||
|
const countBefore = (await pool.query(
|
||||||
|
`SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
|
||||||
|
[userCompId]
|
||||||
|
)).rows[0].c;
|
||||||
|
|
||||||
|
const csrf = await getCsrfFromIndex(user2Agent);
|
||||||
|
// user2 подделывает company_id = чужой компании
|
||||||
|
const res = await user2Agent.post('/add').type('form')
|
||||||
|
.send({ value: '5.6.7.8', comment: 'IDOR test', company_id: userCompId, _csrf: csrf });
|
||||||
|
|
||||||
|
const countAfter = (await pool.query(
|
||||||
|
`SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
|
||||||
|
[userCompId]
|
||||||
|
)).rows[0].c;
|
||||||
|
|
||||||
|
log(countBefore === countAfter,
|
||||||
|
'IDOR: user2 не смог добавить в компанию user через company_id',
|
||||||
|
`before=${countBefore} after=${countAfter}`);
|
||||||
|
|
||||||
|
// Чистим на случай если баг сработал
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
|
||||||
|
WHERE value_cidr = '5.6.7.8/32' AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log(false, 'IDOR: нет userCompId или user2Agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 19. /admin страница — user не может зайти
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 19. /admin — только для admin ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await userAgent.get('/admin');
|
||||||
|
log(res.status === 302 || res.status === 403,
|
||||||
|
'user: GET /admin → redirect или 403', `status=${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await adminAgent.get('/admin');
|
||||||
|
log(res.status === 200, 'admin: GET /admin → 200', `status=${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 20. /audit — только для admin, user видит только свои
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 20. /audit ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await userAgent.get('/audit');
|
||||||
|
// /audit только для admin — user получает 403
|
||||||
|
log(res.status === 403 || res.status === 302,
|
||||||
|
'user: GET /audit → 403 (нет доступа)', `status=${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await adminAgent.get('/audit');
|
||||||
|
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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 26. Пустой comment — должен сохранять запись без ошибки
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 26. Пустой comment ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
const csrf = await getCsrfFromIndex(userAgent);
|
||||||
|
const res = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '7.7.7.7', comment: '', _csrf: csrf });
|
||||||
|
const err = getRedirectParam(res, 'error');
|
||||||
|
log(!err && res.status === 302, 'user: добавил запись без comment', `err=${err}`);
|
||||||
|
// Чистим
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
|
||||||
|
WHERE value_cidr = '7.7.7.7/32' AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 27. Export содержит CIDR текущей компании
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 27. Export содержит CIDR ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
// Добавим уникальную запись и проверим что она появляется в export
|
||||||
|
const csrf = await getCsrfFromIndex(userAgent);
|
||||||
|
await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '6.6.6.6', comment: 'export test', _csrf: csrf });
|
||||||
|
|
||||||
|
const res = await userAgent.get('/export');
|
||||||
|
log(res.status === 200, 'user: /export → 200');
|
||||||
|
const hasCidr = res.text.includes('6.6.6.6');
|
||||||
|
log(hasCidr, 'user: добавленный CIDR виден в /export',
|
||||||
|
hasCidr ? 'found' : `text=${res.text.slice(0, 100)}`);
|
||||||
|
|
||||||
|
// Чистим
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
|
||||||
|
WHERE value_cidr = '6.6.6.6/32' AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 28. Удалённая запись не видна в export
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 28. Удалённая запись не в export ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
// Добавляем, потом удаляем, проверяем что нет в export
|
||||||
|
const csrf = await getCsrfFromIndex(userAgent);
|
||||||
|
const addRes = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '4.4.4.4', comment: 'будет удалена', _csrf: csrf });
|
||||||
|
|
||||||
|
// Ищем id в БД
|
||||||
|
const r = await pool.query(
|
||||||
|
`SELECT id FROM whitelist_entries WHERE value_cidr = '4.4.4.4/32' AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
const delId = r.rows[0]?.id;
|
||||||
|
if (delId) {
|
||||||
|
const csrf2 = await getCsrfFromIndex(userAgent);
|
||||||
|
await userAgent.post(`/delete/${delId}`).type('form').send({ _csrf: csrf2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportRes = await userAgent.get('/export');
|
||||||
|
log(!exportRes.text.includes('4.4.4.4'),
|
||||||
|
'user: удалённый CIDR не виден в /export');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 29. Очень длинный comment — не должен падать с 500
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 29. Длинный comment ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
const longComment = 'A'.repeat(10000);
|
||||||
|
const csrf = await getCsrfFromIndex(userAgent);
|
||||||
|
const res = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '3.3.3.3', comment: longComment, _csrf: csrf });
|
||||||
|
log(res.status === 302, 'user: очень длинный comment → redirect (не 500)', `status=${res.status}`);
|
||||||
|
const err = getRedirectParam(res, 'error');
|
||||||
|
// Либо добавилось, либо ошибка валидации — но не 500
|
||||||
|
log(res.status !== 500, 'user: длинный comment не вызывает 500', `err=${err}`);
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
|
||||||
|
WHERE value_cidr = '3.3.3.3/32' AND deleted_at IS NULL`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 30. Admin limit = 0 — компания сразу не может добавить ничего
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 30. Лимит = 0 ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
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) {
|
||||||
|
const csrf = await getCsrfFromIndex(adminAgent);
|
||||||
|
const limitRes = await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
|
||||||
|
.send({ limit: '0', _csrf: csrf });
|
||||||
|
log(limitRes.status === 302, 'admin: выставил лимит=0', `status=${limitRes.status}`);
|
||||||
|
|
||||||
|
const csrf2 = await getCsrfFromIndex(userAgent);
|
||||||
|
const addRes = await userAgent.post('/add').type('form')
|
||||||
|
.send({ value: '2.2.2.2', comment: 'лимит 0', _csrf: csrf2 });
|
||||||
|
const err = getRedirectParam(addRes, 'error');
|
||||||
|
log(err?.includes('Лимит'), 'user: лимит=0 → любое добавление = ошибка', `err=${err}`);
|
||||||
|
|
||||||
|
// Сброс
|
||||||
|
const csrf3 = await getCsrfFromIndex(adminAgent);
|
||||||
|
await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
|
||||||
|
.send({ limit: '', _csrf: csrf3 });
|
||||||
|
} else {
|
||||||
|
log(false, 'admin: нет компании для теста лимита=0');
|
||||||
|
log(false, 'user: лимит=0 test пропущен');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 31. Admin limit с невалидным значением — не должен сломать лимит
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n── 31. Невалидный лимит ──');
|
||||||
|
|
||||||
|
{
|
||||||
|
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) {
|
||||||
|
const csrf = await getCsrfFromIndex(adminAgent);
|
||||||
|
const r = await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
|
||||||
|
.send({ limit: '-5', _csrf: csrf });
|
||||||
|
// Должен либо отклонить (ошибка), либо принять как 0
|
||||||
|
log(r.status === 302 || r.status === 400,
|
||||||
|
'admin: лимит=-5 → redirect или 400', `status=${r.status}`);
|
||||||
|
|
||||||
|
// Проверяем что limit в БД не стал отрицательным
|
||||||
|
const limRes = await pool.query(
|
||||||
|
`SELECT custom_limit FROM companies WHERE id = $1`, [userCompId]
|
||||||
|
);
|
||||||
|
const savedLimit = limRes.rows[0]?.custom_limit;
|
||||||
|
log(savedLimit === null || savedLimit >= 0,
|
||||||
|
'admin: custom_limit в БД не отрицательный', `saved=${savedLimit}`);
|
||||||
|
|
||||||
|
// Чистим если записался -5
|
||||||
|
if (savedLimit !== null && savedLimit < 0) {
|
||||||
|
await pool.query(`UPDATE companies SET custom_limit = NULL WHERE id = $1`, [userCompId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(false, 'admin: нет компании для теста невалидного лимита');
|
||||||
|
log(false, 'admin: пропущено');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
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