// ═══════════════════════════════════════════════════════════════════════════════ // 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 };