test: добавлен tests/run-tests.js (45 тестов без БД)
This commit is contained in:
@@ -0,0 +1,179 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests/run-tests.js — smoke/unit тесты без внешних зависимостей.
|
||||||
|
* Запускать: node tests/run-tests.js
|
||||||
|
* БД не нужна.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
|
||||||
|
function test(name, actual, expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
console.log(' PASS ' + name);
|
||||||
|
pass++;
|
||||||
|
} else {
|
||||||
|
console.error(' FAIL ' + name + ': got ' + JSON.stringify(actual) + ', want ' + JSON.stringify(expected));
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testThrows(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.error(' FAIL ' + name + ': expected throw, got no error');
|
||||||
|
fail++;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(' PASS ' + name + ' (throws: ' + e.message + ')');
|
||||||
|
pass++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
console.log('\n── ' + title + ' ──');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// 1. Модули загружаются без ошибок
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
section('Загрузка модулей');
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
'src/config.js',
|
||||||
|
'src/auth.js',
|
||||||
|
'src/queries.js',
|
||||||
|
'src/validators.js',
|
||||||
|
'src/middleware/rateLimit.js',
|
||||||
|
'src/middleware/csrf.js',
|
||||||
|
'src/routes/export.js',
|
||||||
|
'src/routes/auth.js',
|
||||||
|
'src/routes/entries.js',
|
||||||
|
'src/routes/admin.js',
|
||||||
|
'server.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const m of modules) {
|
||||||
|
try {
|
||||||
|
require('../' + m);
|
||||||
|
test('require ' + m, true, true);
|
||||||
|
} catch (e) {
|
||||||
|
test('require ' + m, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// 2. src/config.js
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
section('config.js');
|
||||||
|
|
||||||
|
const { MOCK_USERS, backUrl } = require('../src/config');
|
||||||
|
|
||||||
|
test('MOCK_USERS.length === 3', MOCK_USERS.length, 3);
|
||||||
|
test('admin clientId', MOCK_USERS.some(u => u.clientId === 'WZ01112'), true);
|
||||||
|
test('all have companyId', MOCK_USERS.every(u => !!u.companyId), true);
|
||||||
|
test('admin backUrl has company', backUrl(true, 5, {}).includes('company=5'), true);
|
||||||
|
test('user backUrl has no company', backUrl(false, 5, {}).includes('company='), false);
|
||||||
|
test('extra propagated', backUrl(true, 5, { error: 'foo' }).includes('error=foo'), true);
|
||||||
|
test('empty → /', backUrl(false, null, {}), '/');
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// 3. src/validators.js
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
section('validators.js');
|
||||||
|
|
||||||
|
const { aggregateCIDRs, validate, overlaps } = require('../src/validators');
|
||||||
|
|
||||||
|
// aggregateCIDRs
|
||||||
|
test('single /32', aggregateCIDRs(['203.0.114.1/32']).join(), '203.0.114.1/32');
|
||||||
|
test('adjacent /32 → /31', aggregateCIDRs(['8.8.8.0/32', '8.8.8.1/32']).join(), '8.8.8.0/31');
|
||||||
|
test('subnet passthrough /24', aggregateCIDRs(['1.2.3.0/24']).join(), '1.2.3.0/24');
|
||||||
|
test('empty → []', aggregateCIDRs([]).length, 0);
|
||||||
|
|
||||||
|
// validate — корректные входные данные
|
||||||
|
try {
|
||||||
|
const r = validate('8.8.8.8/32');
|
||||||
|
test('valid /32 cidr', r.cidr, '8.8.8.8/32');
|
||||||
|
test('valid /32 wasNormalized', r.wasNormalized, false);
|
||||||
|
} catch (e) { test('valid /32', false, true); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = validate('8.8.8.8'); // без маски → /32
|
||||||
|
test('no-mask → /32', r.cidr, '8.8.8.8/32');
|
||||||
|
} catch (e) { test('no-mask → /32', false, true); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = validate('1.2.3.7/24'); // нормализация host-битов
|
||||||
|
test('normalize host bits', r.cidr, '1.2.3.0/24');
|
||||||
|
test('wasNormalized=true', r.wasNormalized, true);
|
||||||
|
} catch (e) { test('normalize host bits', false, true); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = validate('4.4.0.0/22'); // минимальная допустимая маска
|
||||||
|
test('min mask /22', r.cidr, '4.4.0.0/22');
|
||||||
|
} catch (e) { test('min mask /22', false, true); }
|
||||||
|
|
||||||
|
// validate — некорректные (должны бросать)
|
||||||
|
testThrows('empty throws', () => validate(''));
|
||||||
|
testThrows('private 10.x throws', () => validate('10.1.2.3/32'));
|
||||||
|
testThrows('private 192.168 throws', () => validate('192.168.1.0/24'));
|
||||||
|
testThrows('mask /8 throws', () => validate('1.2.3.4/8'));
|
||||||
|
testThrows('mask /21 throws', () => validate('1.2.3.0/21'));
|
||||||
|
testThrows('bad IP throws', () => validate('999.0.0.1/32'));
|
||||||
|
testThrows('IPv6 throws', () => validate('::1'));
|
||||||
|
|
||||||
|
// overlaps
|
||||||
|
test('overlap subnet in block', overlaps('10.0.1.0/24', '10.0.0.0/8'), true);
|
||||||
|
test('overlap exact match', overlaps('192.168.0.0/24', '192.168.0.0/24'), true);
|
||||||
|
test('no overlap public', overlaps('8.8.8.0/24', '9.9.9.0/24'), false);
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// 4. src/auth.js — DEV_MODE (без JWT, без БД)
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
section('auth.js (DEV_MODE)');
|
||||||
|
|
||||||
|
process.env.DEV_MODE = 'true';
|
||||||
|
process.env.DEV_ADMIN = 'true';
|
||||||
|
process.env.ADMIN_CLIENT_ID = 'WZ01112';
|
||||||
|
|
||||||
|
const { initAuth, requireAdmin } = require('../src/auth');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = initAuth();
|
||||||
|
test('middleware is function', typeof auth.middleware, 'function');
|
||||||
|
test('jwksHandler present in DEV_MODE', typeof auth.jwksHandler, 'function');
|
||||||
|
|
||||||
|
// DEV_MODE: middleware выставляет req.user без JWT
|
||||||
|
const req = { headers: {}, cookies: {} };
|
||||||
|
const res = { redirect: () => {} };
|
||||||
|
let nextCalled = false;
|
||||||
|
auth.middleware(req, res, () => { nextCalled = true; });
|
||||||
|
test('DEV_MODE: next() called', nextCalled, true);
|
||||||
|
test('DEV_MODE: req.user set', !!req.user, true);
|
||||||
|
test('DEV_MODE: req.user.isAdmin = true (DEV_ADMIN=true)', req.user.isAdmin, true);
|
||||||
|
|
||||||
|
// requireAdmin: пропускает admin
|
||||||
|
const reqAdmin = { user: { isAdmin: true } };
|
||||||
|
let adminPassed = false;
|
||||||
|
requireAdmin(reqAdmin, {}, () => { adminPassed = true; });
|
||||||
|
test('requireAdmin: admin passes', adminPassed, true);
|
||||||
|
|
||||||
|
// requireAdmin: блокирует не-admin
|
||||||
|
const reqUser = { user: { isAdmin: false } };
|
||||||
|
let blockedStatus = null;
|
||||||
|
requireAdmin(reqUser, {
|
||||||
|
status: (s) => { blockedStatus = s; return { send: () => {} }; },
|
||||||
|
}, () => {});
|
||||||
|
test('requireAdmin: non-admin blocked (403)', blockedStatus, 403);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('auth.js FATAL:', e.message);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Итог
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
console.log('\n══════════════════════════════');
|
||||||
|
console.log(pass + ' PASS, ' + fail + ' FAIL');
|
||||||
|
console.log('══════════════════════════════');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
Reference in New Issue
Block a user