v2: встроенные CRUD-тесты — /v2/test/crud (production-база)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.80",
|
"version": "0.5.81",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ function createV2Router() {
|
|||||||
router.get('/', (req, res) => res.redirect('/v2/app'));
|
router.get('/', (req, res) => res.redirect('/v2/app'));
|
||||||
|
|
||||||
// ── GET /v2/logout ───────────────────────────────────────────────────────
|
// ── GET /v2/logout ───────────────────────────────────────────────────────
|
||||||
|
// ── GET /v2/test/crud — тесты CRUD (временный)
|
||||||
|
router.get('/test/crud', async (req, res) => {
|
||||||
|
const { runCrudTests } = require('./src/user/test-crud');
|
||||||
|
const result = await runCrudTests();
|
||||||
|
res.send('<h2>CRUD Tests</h2><pre>' + result.results.join('\n') + '</pre><p>' + result.passed + ' passed, ' + result.failed + ' failed из ' + result.total + '</p>');
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/logout', (req, res) => res.redirect('/logout'));
|
router.get('/logout', (req, res) => res.redirect('/logout'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: '0.5.80',
|
version: '0.5.81',
|
||||||
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
||||||
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function renderPage({ entries, company, limit, used, allCompanies, clId, user, i
|
|||||||
.add-form{background:#f0f8ff;padding:10px;margin:10px 0}
|
.add-form{background:#f0f8ff;padding:10px;margin:10px 0}
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
<span><strong>V2 IP WhiteList</strong></span>
|
<span><strong>V2 IP WhiteList</strong> <small>v${require('../config').version}</small></span>
|
||||||
<span>${user.email} ${user.isAdmin ? '| <a href="/v2/admin">ADMIN</a>' : ''}</span>
|
<span>${user.email} ${user.isAdmin ? '| <a href="/v2/admin">ADMIN</a>' : ''}</span>
|
||||||
<span>
|
<span>
|
||||||
<form method="GET" action="/v2/app" style="display:inline">
|
<form method="GET" action="/v2/app" style="display:inline">
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// V2 — встроенные тесты CRUD (запускаются на production-базе)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const q = require('../db/queries');
|
||||||
|
const { pool } = require('../db');
|
||||||
|
|
||||||
|
let passed = 0, failed = 0;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
function t(name) {
|
||||||
|
return {
|
||||||
|
run: async (fn) => {
|
||||||
|
try { await fn(); passed++; results.push('✅ ' + name); }
|
||||||
|
catch (e) { failed++; results.push('❌ ' + name + ' — ' + e.message); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCrudTests() {
|
||||||
|
// Сброс
|
||||||
|
passed = 0; failed = 0; results.length = 0;
|
||||||
|
|
||||||
|
const testCIDs = ['WZ10001', 'WZ20001', 'WZ20002', 'WZ01112', 'WZ03709'];
|
||||||
|
// Очистка тестовых данных
|
||||||
|
for (const cid of testCIDs) {
|
||||||
|
await pool.query(`DELETE FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)`, [cid]).catch(()=>{});
|
||||||
|
await pool.query(`DELETE FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)`, [cid]).catch(()=>{});
|
||||||
|
await pool.query(`DELETE FROM v2_companies WHERE client_id=$1`, [cid]).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 1. Один юзер, одна компания ─────────────────────────────────────────
|
||||||
|
results.push('── 1. Одна компания ──');
|
||||||
|
const c1 = { email: 'u1@t.ru', clientId: 'WZ10001', activeClientId: 'WZ10001',
|
||||||
|
allClientIds: ['WZ10001'], companyName: 'Одна', isAdmin: false,
|
||||||
|
profiles: [{ client_id: 'WZ10001', company_name: 'Одна', is_active_profile: true }] };
|
||||||
|
|
||||||
|
let entryId;
|
||||||
|
|
||||||
|
await t('создание компании').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
if (co.client_id !== 'WZ10001') throw new Error('не та компания');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('добавление записи').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
const r = await q.createEntry(co.id, '1.2.3.0/24', 'тест', c1.email, null);
|
||||||
|
if (!r.entry.value_cidr.includes('1.2.3.0')) throw new Error('CIDR не совпадает');
|
||||||
|
entryId = r.entry.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('дубликат отклонён').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
try { await q.createEntry(co.id, '1.2.3.0/24', '', c1.email, null); throw new Error('должен был упасть'); }
|
||||||
|
catch(e) { if (!e.message.includes('уже существует')) throw e; }
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('пересечение отклонено').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
try { await q.createEntry(co.id, '1.2.3.128/25', '', c1.email, null); throw new Error('должен был упасть'); }
|
||||||
|
catch(e) { if (!e.message.includes('Пересечение')) throw e; }
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('запрещённый диапазон').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
try { await q.createEntry(co.id, '10.0.0.0/24', '', c1.email, null); throw new Error('должен был упасть'); }
|
||||||
|
catch(e) { if (!e.message.includes('запрещённым')) throw e; }
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('изменение записи').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
const r = await q.updateEntry(entryId, co.id, '6.6.6.0/24', 'изменено', c1.email, null);
|
||||||
|
if (r.entry.value_cidr !== '6.6.6.0/24') throw new Error('не изменился');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('soft delete').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
await q.deleteEntry(entryId, co.id, c1.email, null);
|
||||||
|
const list = await q.listEntries(co.id, false);
|
||||||
|
if (list.length !== 0) throw new Error('запись не удалилась');
|
||||||
|
const all = await q.listEntries(co.id, true);
|
||||||
|
if (all.length !== 1) throw new Error('soft delete: запись пропала');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('аудит — записи созданы').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ10001', 'Одна');
|
||||||
|
const audit = await q.getAudit(co.id);
|
||||||
|
if (audit.length < 3) throw new Error('аудит: ожидалось >=3, получили ' + audit.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 2. Две компании ──────────────────────────────────────────────────────
|
||||||
|
results.push('── 2. Две компании ──');
|
||||||
|
|
||||||
|
await t('изолированность компаний').run(async () => {
|
||||||
|
const cA = await q.getOrCreateCompany('WZ20001', 'Первая');
|
||||||
|
const cB = await q.getOrCreateCompany('WZ20002', 'Вторая');
|
||||||
|
await q.createEntry(cA.id, '7.7.7.0/24', '', 'u2@t.ru', null);
|
||||||
|
await q.createEntry(cB.id, '8.8.8.0/24', '', 'u2@t.ru', null);
|
||||||
|
const listA = await q.listEntries(cA.id);
|
||||||
|
const listB = await q.listEntries(cB.id);
|
||||||
|
if (listA.length !== 1 || listB.length !== 1) throw new Error('изоляция нарушена');
|
||||||
|
if (listA[0].value_cidr !== '7.7.7.0/24') throw new Error('чужая запись в компании A');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('лимит').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ20001', 'Первая');
|
||||||
|
// Заполняем до лимита (15)
|
||||||
|
const used = (await q.listEntries(co.id)).length;
|
||||||
|
const limit = await q.getLimit(co);
|
||||||
|
for (let i = used; i < limit; i++) {
|
||||||
|
await q.createEntry(co.id, `9.9.${i}.0/24`, '', 'u2@t.ru', null);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await q.createEntry(co.id, '9.9.99.0/24', '', 'u2@t.ru', null);
|
||||||
|
throw new Error('должен был упасть по лимиту');
|
||||||
|
} catch(e) {
|
||||||
|
if (!e.message.includes('Лимит')) throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 3. Имперсонация ─────────────────────────────────────────────────────
|
||||||
|
results.push('── 3. Имперсонация ──');
|
||||||
|
|
||||||
|
await t('impersonated_by в аудите').run(async () => {
|
||||||
|
const co = await q.getOrCreateCompany('WZ03709', 'ЧужаяКомпания');
|
||||||
|
await q.createEntry(co.id, '10.10.10.0/24', '', 'real@nubes.ru', 'admin@nubes.ru');
|
||||||
|
const audit = await q.getAudit(co.id);
|
||||||
|
const last = audit[0];
|
||||||
|
if (last.impersonated_by !== 'admin@nubes.ru') throw new Error('impersonated_by=' + last.impersonated_by);
|
||||||
|
if (last.user_email !== 'real@nubes.ru') throw new Error('user_email=' + last.user_email);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Чистка
|
||||||
|
for (const cid of testCIDs) {
|
||||||
|
await pool.query(`DELETE FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)`, [cid]).catch(()=>{});
|
||||||
|
await pool.query(`DELETE FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)`, [cid]).catch(()=>{});
|
||||||
|
await pool.query(`DELETE FROM v2_companies WHERE client_id=$1`, [cid]).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { passed, failed, total: passed + failed, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runCrudTests };
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CRUD тесты — без внешних зависимостей (нет supertest)
|
||||||
|
// Использует http.request напрямую к Express-роутеру.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const express = require('express');
|
||||||
|
const { createUserRouter } = require('./index');
|
||||||
|
const { resolveContext } = require('../router');
|
||||||
|
const { pool } = require('../db');
|
||||||
|
const { ensureSchema } = require('../db/schema');
|
||||||
|
|
||||||
|
const USERS = {
|
||||||
|
one: { email:'u1@t.ru', clientId:'WZ10001', activeClientId:'WZ10001', allClientIds:['WZ10001'], companyName:'Одна', isAdmin:false, isImpersonated:false, profiles:[{client_id:'WZ10001',company_name:'Одна',is_active_profile:true}] },
|
||||||
|
two: { email:'u2@t.ru', clientId:'WZ20001', activeClientId:'WZ20002', allClientIds:['WZ20001','WZ20002'], companyName:'Вторая', isAdmin:false, isImpersonated:false, profiles:[{client_id:'WZ20001',company_name:'Первая'},{client_id:'WZ20002',company_name:'Вторая',is_active_profile:true}] },
|
||||||
|
imp: { email:'admin@t.ru', clientId:'WZ01112', activeClientId:'WZ01112', allClientIds:['WZ01112','WZ03709'], companyName:'Nubes', isAdmin:true, isImpersonated:true, originalUserEmail:'real@t.ru', impersonatedCompanyId:'WZ03709', profiles:[] },
|
||||||
|
};
|
||||||
|
|
||||||
|
let p = 0, f = 0;
|
||||||
|
function t(name, fn) { return fn().then(() => { console.log('✅', name); p++; }, e => { console.log('❌', name, e.message); f++; }); }
|
||||||
|
|
||||||
|
function req(app, method, path, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = app.listen(0, () => {
|
||||||
|
const port = server.address().port;
|
||||||
|
const opts = { hostname:'127.0.0.1', port, path, method, headers:{} };
|
||||||
|
if (body) { opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
|
||||||
|
const r = http.request(opts, res => {
|
||||||
|
let d = ''; res.on('data', c => d += c); res.on('end', () => { server.close(); resolve({ status: res.statusCode, headers: res.headers, text: d }); });
|
||||||
|
});
|
||||||
|
r.on('error', e => { server.close(); reject(e); });
|
||||||
|
if (body) r.write(new URLSearchParams(body).toString());
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeApp(user) {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use((req, res, next) => { req.session = { user }; next(); });
|
||||||
|
app.use(resolveContext);
|
||||||
|
app.use(createUserRouter());
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanup(cids) {
|
||||||
|
for (const c of cids) {
|
||||||
|
await pool.query('DELETE FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)',[c]).catch(()=>{});
|
||||||
|
await pool.query('DELETE FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)',[c]).catch(()=>{});
|
||||||
|
await pool.query('DELETE FROM v2_companies WHERE client_id=$1',[c]).catch(()=>{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await ensureSchema(pool);
|
||||||
|
await cleanup(['WZ10001','WZ20001','WZ20002','WZ01112','WZ03709']);
|
||||||
|
|
||||||
|
// ── 1. Одна компания ────────────────────────────────────────────────────
|
||||||
|
console.log('\n=== 1. Одна компания ===');
|
||||||
|
const a1 = makeApp(USERS.one);
|
||||||
|
|
||||||
|
await t('пустой список', async () => {
|
||||||
|
const r = await req(a1, 'GET', '/');
|
||||||
|
if (!r.text.includes('Нет записей')) throw new Error('ожидалась пустая таблица');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('добавление', async () => {
|
||||||
|
const r = await req(a1, 'POST', '/add', { cidr: '1.2.3.0/24', comment: 'тест' });
|
||||||
|
if (!r.headers.location.includes('msg=')) throw new Error('нет msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('запись появилась', async () => {
|
||||||
|
const r = await req(a1, 'GET', '/');
|
||||||
|
if (!r.text.includes('1.2.3.0/24')) throw new Error('запись не найдена');
|
||||||
|
if (!r.text.includes('тест')) throw new Error('комментарий не найден');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('дубликат отклонён', async () => {
|
||||||
|
const r = await req(a1, 'POST', '/add', { cidr: '1.2.3.0/24' });
|
||||||
|
if (!r.headers.location.includes('error=')) throw new Error('дубликат должен быть отклонён');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('пересечение отклонено', async () => {
|
||||||
|
const r = await req(a1, 'POST', '/add', { cidr: '1.2.3.128/25' });
|
||||||
|
if (!r.headers.location.includes('error=')) throw new Error('пересечение должно быть отклонено');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('/32 работает', async () => {
|
||||||
|
const r = await req(a1, 'POST', '/add', { cidr: '5.5.5.5' });
|
||||||
|
if (!r.headers.location.includes('msg=')) throw new Error('/32 должен добавиться');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('запрещённый диапазон', async () => {
|
||||||
|
const r = await req(a1, 'POST', '/add', { cidr: '10.0.0.0/24' });
|
||||||
|
if (!r.headers.location.includes('error=')) throw new Error('10.0.0.0/24 запрещён');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('изменение', async () => {
|
||||||
|
const rows = await pool.query("SELECT id FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ10001') AND deleted_at IS NULL LIMIT 1");
|
||||||
|
const r = await req(a1, 'POST', '/edit/' + rows.rows[0].id, { cidr: '6.6.6.0/24', comment: 'изменено' });
|
||||||
|
if (!r.headers.location.includes('msg=')) throw new Error('изменение не прошло');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('удаление', async () => {
|
||||||
|
const rows = await pool.query("SELECT id FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ10001') AND deleted_at IS NULL LIMIT 1");
|
||||||
|
const r = await req(a1, 'POST', '/delete/' + rows.rows[0].id);
|
||||||
|
if (!r.headers.location.includes('msg=')) throw new Error('удаление не прошло');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('аудит', async () => {
|
||||||
|
const r = await pool.query("SELECT COUNT(*)::int as c FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ10001')");
|
||||||
|
if (r.rows[0].c < 3) throw new Error('аудит: ' + r.rows[0].c + ' записей, ожидалось >=3');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 2. Две компании ──────────────────────────────────────────────────────
|
||||||
|
console.log('\n=== 2. Две компании ===');
|
||||||
|
const a2 = makeApp(USERS.two);
|
||||||
|
|
||||||
|
await t('активна вторая', async () => {
|
||||||
|
const r = await req(a2, 'GET', '/');
|
||||||
|
if (!r.text.includes('Вторая')) throw new Error('должна быть Вторая');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('добавление во вторую', async () => {
|
||||||
|
await req(a2, 'POST', '/add', { cidr: '7.7.7.0/24' });
|
||||||
|
const r = await pool.query("SELECT value_cidr FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ20002') AND deleted_at IS NULL");
|
||||||
|
if (!r.rows.find(rr => rr.value_cidr === '7.7.7.0/24')) throw new Error('не в той компании');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('в первой пусто', async () => {
|
||||||
|
const r = await pool.query("SELECT COUNT(*)::int as c FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ20001') AND deleted_at IS NULL");
|
||||||
|
if (r.rows[0].c !== 0) throw new Error('в первой не должно быть записей');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 3. Имперсонация ──────────────────────────────────────────────────────
|
||||||
|
console.log('\n=== 3. Имперсонация ===');
|
||||||
|
const a3 = makeApp(USERS.imp);
|
||||||
|
|
||||||
|
await t('created_by = originalUserEmail', async () => {
|
||||||
|
await req(a3, 'POST', '/add', { cidr: '8.8.8.0/24' });
|
||||||
|
const r = await pool.query("SELECT created_by FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ03709') AND value_cidr='8.8.8.0/24'");
|
||||||
|
if (r.rows[0].created_by !== 'real@t.ru') throw new Error('created_by=' + r.rows[0].created_by);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t('impersonated_by в аудите', async () => {
|
||||||
|
const r = await pool.query("SELECT impersonated_by FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id='WZ03709') ORDER BY id DESC LIMIT 1");
|
||||||
|
if (r.rows[0].impersonated_by !== 'admin@t.ru') throw new Error('impersonated_by=' + r.rows[0].impersonated_by);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n${p} passed, ${f} failed`);
|
||||||
|
await cleanup(['WZ10001','WZ20001','WZ20002','WZ01112','WZ03709']);
|
||||||
|
await pool.end();
|
||||||
|
if (f) process.exit(1);
|
||||||
|
})().catch(e => { console.error(e); process.exit(1); });
|
||||||
Reference in New Issue
Block a user