255 lines
14 KiB
JavaScript
255 lines
14 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// V2 — Тестовый слой
|
|
//
|
|
// ЭТО: Express-роутер для тестирования ВСЕХ слоёв через curl.
|
|
// НЕ: production-код (отключается флагом ENABLE_TEST_API=false).
|
|
//
|
|
// ЗАЧЕМ:
|
|
// 1. Тестировать слои (crud, db, validators) на реальной БД без KC-сессии.
|
|
// 2. Эмулировать поведение юзера (userFlow).
|
|
// 3. Нагрузочное тестирование (chaos — 40 параллельных операций).
|
|
// 4. Очистка тестовых данных (cleanup).
|
|
//
|
|
// МОК-ЮЗЕРЫ: u1, u2, imp — эмулируют реальных пользователей IAM.
|
|
// u1 — обычный юзер, одна компания (WZ10001)
|
|
// u2 — юзер с двумя компаниями (WZ20001, WZ20002)
|
|
// imp — админ под имперсонацией (WZ01112 → WZ03709)
|
|
// adm — админ без имперсонации, 3 компании (WZ01112, WZ03709, WZ09999)
|
|
//
|
|
// ACTIONS (GET ?action=...):
|
|
// add/edit/delete/list — CRUD через crud/
|
|
// audit — аудит через db/queries
|
|
// limit — лимит компании
|
|
// switch — переключение компании
|
|
// cleanup — удаление тестовых данных
|
|
// userFlow — полная эмуляция юзера с мок-сессией
|
|
//
|
|
// CHAOS (GET /chaos):
|
|
// 4 компании × 10 записей = 40 параллельных Promise.all.
|
|
// Проверяет: дубликаты, лимиты, аудит.
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
const express = require('express');
|
|
const crud = require('../crud'); // API-слой БД
|
|
const q = require('../db/queries'); // прямой доступ для аудита/лимитов
|
|
const { pool } = require('../db'); // прямой доступ для cleanup
|
|
|
|
// Мок-юзеры — эмулируют данные из IAM после KC-входа
|
|
const USERS = {
|
|
u1: { email:'u1@t.ru', clientId:'WZ10001', activeClientId:'WZ10001', allClientIds:['WZ10001'], companyName:'Одна', isAdmin:false, profiles:[{client_id:'WZ10001',company_name:'Одна'}] },
|
|
u2: { email:'u2@t.ru', clientId:'WZ20001', activeClientId:'WZ20002', allClientIds:['WZ20001','WZ20002'], companyName:'Вторая', isAdmin:false, profiles:[{client_id:'WZ20001',company_name:'Первая'},{client_id:'WZ20002',company_name:'Вторая'}] },
|
|
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:[] },
|
|
// admin — админ с 3 компаниями, БЕЗ имперсонации
|
|
adm:{ email:'admin@t.ru', clientId:'WZ01112', activeClientId:'WZ01112', allClientIds:['WZ01112','WZ03709','WZ09999'], companyName:'Nubes', isAdmin:true, profiles:[{client_id:'WZ01112',company_name:'Nubes'},{client_id:'WZ03709',company_name:'Дочка'},{client_id:'WZ09999',company_name:'Третья'}] },
|
|
};
|
|
|
|
function createTestRouter() {
|
|
const router = express.Router();
|
|
|
|
// ── GET / — главный тестовый эндпоинт (?action=...) ─────────────────
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const action = req.query.action;
|
|
const user = USERS[req.query.user || 'u1'];
|
|
if (!user) return res.json({ ok: false, error: 'unknown user' });
|
|
|
|
// Контекст из мок-юзера (аналог resolveContext)
|
|
const email = user.isImpersonated ? user.originalUserEmail : user.email;
|
|
const clientId = user.isImpersonated ? user.impersonatedCompanyId : user.activeClientId;
|
|
const company = await q.getOrCreateCompany(clientId, user.companyName || clientId);
|
|
const impBy = user.isImpersonated ? user.email : null;
|
|
|
|
switch (action) {
|
|
// ── LIST: список записей компании ─────────────────────────
|
|
case 'list': {
|
|
const { entries, used, limit } = await crud.list(clientId, req.query.deleted === '1');
|
|
return res.json({ ok: true, entries, count: entries.length, used, limit }); }
|
|
|
|
// ── ADD: добавить CIDR (crud.add → validate → db) ────────
|
|
case 'add': {
|
|
const r = await crud.add(clientId, req.query.cidr || '', req.query.comment || '', email, impBy);
|
|
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
|
}
|
|
|
|
// ── EDIT: изменить существующую запись ────────────────────
|
|
case 'edit': {
|
|
const r = await crud.edit(parseInt(req.query.id), clientId, req.query.cidr || '', req.query.comment || '', email, impBy);
|
|
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
|
}
|
|
|
|
// ── DELETE: soft delete ───────────────────────────────────
|
|
case 'delete':
|
|
await crud.remove(parseInt(req.query.id), clientId, email, impBy);
|
|
return res.json({ ok: true });
|
|
|
|
// ── AUDIT: журнал действий компании ──────────────────────
|
|
case 'audit': {
|
|
const audit = await q.getAudit(company.id);
|
|
return res.json({ ok: true, audit, count: audit.length });
|
|
}
|
|
|
|
// ── LIMIT: текущий лимит компании ────────────────────────
|
|
case 'limit':
|
|
return res.json({ ok: true, limit: await q.getLimit(company) });
|
|
|
|
// ── SWITCH: переключение активной компании ───────────────
|
|
case 'switch': {
|
|
const to = req.query.to;
|
|
if (!to) return res.json({ ok: false, error: 'no to parameter' });
|
|
if (!user.allClientIds.includes(to)) return res.json({ ok: false, error: 'not in allClientIds' });
|
|
user.activeClientId = to;
|
|
const p = user.profiles.find(p => p.client_id === to);
|
|
if (p) { p.is_active_profile = true; user.profiles.forEach(x => { if (x !== p) x.is_active_profile = false; }); }
|
|
return res.json({ ok: true, switchedTo: to });
|
|
}
|
|
|
|
// ── CLEANUP: удалить все данные тестовых компаний ───────
|
|
case 'cleanup': {
|
|
const cids = (req.query.cids || '').split(',');
|
|
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(()=>{});
|
|
}
|
|
return res.json({ ok: true });
|
|
}
|
|
|
|
// ── WIPE: удалить все записи и аудит компании (clientId) ─
|
|
case 'wipe': {
|
|
const clientId = req.query.clientId;
|
|
if (!clientId) return res.json({ ok: false, error: 'clientId required' });
|
|
const e = await pool.query('DELETE FROM v2_entries WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)', [clientId]);
|
|
const a = await pool.query('DELETE FROM v2_audit WHERE company_id IN (SELECT id FROM v2_companies WHERE client_id=$1)', [clientId]);
|
|
return res.json({ ok: true, entriesDeleted: e.rowCount, auditDeleted: a.rowCount, clientId });
|
|
}
|
|
|
|
// ── CLEAN-ALL: очистить ВСЕ таблицы (для сдачи в прод) ──
|
|
case 'clean-all': {
|
|
const a = await pool.query('DELETE FROM audit_log');
|
|
const e = await pool.query('DELETE FROM whitelist_entries');
|
|
const c = await pool.query('DELETE FROM companies');
|
|
return res.json({ ok: true, audit_log: a.rowCount, whitelist_entries: e.rowCount, companies: c.rowCount });
|
|
}
|
|
|
|
// ── USERFLOW: полная эмуляция юзера через все слои ──────
|
|
// Отличие от add/edit/delete: здесь выставляется мок-сессия
|
|
// как после KC+IAM+resolveContext, и вызывается crud.
|
|
// Показывает слой 'user→crud→db' в ответе.
|
|
case 'userFlow': {
|
|
const sub = req.query.sub; // add | edit | delete | list
|
|
const cidr = req.query.cidr || '';
|
|
const comment = req.query.comment || '';
|
|
const entryId = parseInt(req.query.id) || null;
|
|
|
|
// Мок сессии + контекст (как после KC+IAM+resolveContext)
|
|
req.session = req.session || {};
|
|
req.session.user = {
|
|
email, clientId, activeClientId: clientId,
|
|
allClientIds: [clientId], companyName: clientId,
|
|
isAdmin: user.isAdmin || false,
|
|
profiles: [{ client_id: clientId, company_name: clientId }],
|
|
fio: 'Test User'
|
|
};
|
|
req.email = email;
|
|
req.clientId = clientId;
|
|
req.companyName = clientId;
|
|
req.isAdmin = user.isAdmin || false;
|
|
req.profiles = [{ client_id: clientId, company_name: clientId }];
|
|
req.impersonatedBy = impBy;
|
|
|
|
try {
|
|
if (sub === 'add') {
|
|
const r = await crud.add(clientId, cidr, comment, email, impBy);
|
|
return res.json({ ok: true, layer: 'user→crud→db', action: 'add', entry: r.entry, wasNormalized: r.wasNormalized });
|
|
}
|
|
if (sub === 'edit' && entryId) {
|
|
const r = await crud.edit(entryId, clientId, cidr, comment, email, impBy);
|
|
return res.json({ ok: true, layer: 'user→crud→db', action: 'edit', entry: r.entry, wasNormalized: r.wasNormalized });
|
|
}
|
|
if (sub === 'delete' && entryId) {
|
|
await crud.remove(entryId, clientId, email, impBy);
|
|
return res.json({ ok: true, layer: 'user→crud→db', action: 'delete' });
|
|
}
|
|
if (sub === 'list') {
|
|
const { entries, used, limit } = await crud.list(clientId);
|
|
return res.json({ ok: true, layer: 'user→crud→db', action: 'list', entries, count: entries.length, used, limit });
|
|
}
|
|
return res.json({ ok: false, error: 'userFlow sub: add|edit|delete|list (id для edit/delete)' });
|
|
} catch (e) {
|
|
return res.json({ ok: false, layer: 'user→crud→db', error: e.message });
|
|
}
|
|
}
|
|
|
|
default:
|
|
return res.json({ ok: false, error: 'unknown action: ' + action });
|
|
}
|
|
} catch (e) {
|
|
return res.json({ ok: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// ── GET /chaos — параллельный хаос-тест (40 операций) ──────────────
|
|
router.get('/chaos', async (req, res) => {
|
|
const cids = ['WZ10001','WZ20002','WZ30001','WZ01112'];
|
|
// Очистка перед тестом
|
|
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(()=>{});
|
|
}
|
|
const emails = ['u1@t.ru', 'u2@t.ru', 'u3@t.ru', 'admin@t.ru'];
|
|
// CIDR префиксы от 11 (10.x — заблокирован 10.0.0.0/8!)
|
|
const cidPrefix = { 0: 11, 1: 12, 2: 13, 3: 14 };
|
|
let ok = 0, errors = 0;
|
|
const perCompany = 10; // по 10 на компанию (лимит 15)
|
|
const tasks = [];
|
|
for (let idx = 0; idx < 4; idx++) {
|
|
const email = emails[idx];
|
|
const clientId = cids[idx];
|
|
const impBy = idx === 3 ? 'real@nubes.ru' : null;
|
|
const prefix = cidPrefix[idx];
|
|
tasks.push((async () => {
|
|
for (let j = 0; j < perCompany; j++) {
|
|
try {
|
|
await crud.add(clientId, prefix + '.0.' + j + '.0/24', 'chaos', email, impBy);
|
|
ok++;
|
|
} catch (e) { errors++; }
|
|
}
|
|
})());
|
|
}
|
|
await Promise.all(tasks);
|
|
|
|
// Проверка ВСЕХ компаний после теста
|
|
const checks = [];
|
|
let totalEntries = 0, totalAudit = 0, anyDupes = false, anyOverLimit = false;
|
|
for (const clientId of cids) {
|
|
const { entries, limit } = await crud.list(clientId);
|
|
const cidrs = entries.map(e => e.value_cidr);
|
|
const dupes = cidrs.length !== new Set(cidrs).size;
|
|
const audit = await q.getAudit((await q.getOrCreateCompany(clientId, clientId)).id);
|
|
totalEntries += entries.length;
|
|
totalAudit += audit.length;
|
|
if (dupes) anyDupes = true;
|
|
if (entries.length > limit) anyOverLimit = true;
|
|
checks.push({ clientId, entries: entries.length, audit: audit.length, dupes, overLimit: entries.length > limit, limit });
|
|
}
|
|
|
|
// Очистка после теста
|
|
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(()=>{});
|
|
}
|
|
|
|
res.json({
|
|
ok: !anyDupes && !anyOverLimit && ok === 40,
|
|
stats: { ok, errors, totalEntries, totalAudit },
|
|
checks
|
|
});
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports = { createTestRouter };
|