/** * tests/real-users-smoke.js — smoke test с реальными токенами IAM. * * ⛔ ТОЛЬКО ЧТЕНИЕ — никаких POST/PATCH/DELETE. * ⛔ Все запросы к IAM API и ipwhitelist GET-эндпоинтам. * ✅ Безопасно для прода/стейджинга — ничего не создаёт, не удаляет, не меняет. * * Использование: * cp tokens_all.txt ../nubes/tokens_all.txt * node tests/real-users-smoke.js * * Переменные окружения: * IAM_API_URL — по умолчанию https://auth-api.ngcloud.ru (prod) * APP_URL — по умолчанию https://italo.kube5s.ru (тестовый стенд) */ 'use strict'; const https = require('https'); const http = require('http'); const fs = require('fs'); const path = require('path'); const IAM_API_URL = (process.env.IAM_API_URL || 'https://auth-api.ngcloud.ru').replace(/\/$/, ''); const APP_URL = (process.env.APP_URL || 'https://italo.kube5s.ru').replace(/\/$/, ''); // ── Парсинг файла с токенами ──────────────────────────────────────────────── function parseTokensFile(filePath) { const text = fs.readFileSync(filePath, 'utf8'); const lines = text.split('\n'); const accounts = []; let i = 0; while (i < lines.length) { const l = lines[i].trim(); if (!l || l.startsWith('eyJ')) { i++; continue; } const email = l.replace(/^=+\s*|\s*=+$/g, '').trim(); i++; while (i < lines.length && !lines[i].trim()) i++; const tokParts = []; while (i < lines.length) { const t = lines[i].trim(); if (!t || t.includes('===')) break; tokParts.push(t); i++; } const token = tokParts.join(''); if (token) accounts.push({ email, token }); } return accounts; } // ── HTTP helper ────────────────────────────────────────────────────────────── function httpRequest(url, method = 'GET', headers = {}, body = null, timeout = 10000) { return new Promise((resolve, reject) => { const u = new URL(url); const lib = u.protocol === 'https:' ? https : http; const opts = { hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80), path: u.pathname + u.search, method, headers, timeout, }; const req = lib.request(opts, (res) => { let data = ''; res.on('data', c => { data += c; }); res.on('end', () => { resolve({ status: res.statusCode, headers: res.headers, body: data }); }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); if (body) req.write(body); req.end(); }); } // ── JWT decode ────────────────────────────────────────────────────────────── function jwtPayload(token) { const parts = token.split('.'); if (parts.length !== 3) return null; return JSON.parse(Buffer.from(parts[1], 'base64url').toString()); } // ── Тесты ─────────────────────────────────────────────────────────────────── let passed = 0; let failed = 0; function test(name, fn) { return Promise.resolve().then(fn).then(() => { console.log(' ✅ ' + name); passed++; }).catch(e => { console.log(' ❌ ' + name + ' — ' + e.message); failed++; }); } async function run() { // Путь к файлу с токенами const tokFile = path.resolve(__dirname, '..', '..', 'nubes', 'tokens_all.txt'); let accounts; try { accounts = parseTokensFile(tokFile); } catch (e) { console.log('❌ Не найден файл с токенами:', tokFile); console.log(' cp /home/naeel/nubes/tokens_all.txt ../nubes/tokens_all.txt'); process.exit(1); } console.log('🧪 Smoke-тесты с реальными IAM токенами'); console.log(' IAM:', IAM_API_URL); console.log(' App:', APP_URL); console.log(' Учётных записей:', accounts.length); console.log(); for (const acc of accounts) { const tok = acc.token; const email = acc.email; const payload = jwtPayload(tok); const label = `${email} (${payload?.ClientID || '?'})`; console.log(`\n══════════ ${label} ══════════`); // ── 1. IAM API: GET /auth/user ────────────────────────────────────────── console.log(' ── IAM API ──'); await test('IAM API доступен', async () => { const res = await httpRequest(`${IAM_API_URL}/api/v1/auth/user`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'application/json', }); if (res.status !== 200) throw new Error('HTTP ' + res.status + ': ' + res.body.slice(0, 100)); }); let iamUser = null; await test('IAM API: profiles[] не пуст', async () => { const res = await httpRequest(`${IAM_API_URL}/api/v1/auth/user`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'application/json', }); iamUser = JSON.parse(res.body); if (!iamUser.profiles || iamUser.profiles.length === 0) throw new Error('profiles[] пуст'); }); await test('IAM API: ClientID совпадает с JWT', () => { const jwtCid = payload.ClientID; const iamCid = iamUser.userInfo?.clientID; if (jwtCid !== iamCid) throw new Error(`JWT ClientID=${jwtCid} !== IAM userInfo.clientID=${iamCid}`); }); await test('IAM API: profiles[0].client_id совпадает', () => { const profileCid = iamUser.profiles[0].client_id; const jwtCid = payload.ClientID; if (profileCid !== jwtCid) throw new Error(`profile client_id=${profileCid} !== JWT ClientID=${jwtCid}`); }); await test('IAM API: email совпадает', () => { const iamEmail = iamUser.userInfo?.email || ''; if (iamEmail && iamEmail !== email) throw new Error(`IAM email=${iamEmail} !== ожидаемый ${email}`); }); await test('IAM API: isAdmin флаг есть в ответе', () => { if (typeof iamUser.userInfo?.isAdmin !== 'boolean') throw new Error('isAdmin отсутствует или не boolean'); }); // ── 2. IAM API: JWKS endpoint ────────────────────────────────────────── await test('IAM API: JWKS endpoint доступен и содержит ключи', async () => { const res = await httpRequest(`${IAM_API_URL}/.well-known/openid-configuration`, 'GET', { 'Accept': 'application/json', }); if (res.status === 404) { // Не у всех IAM есть well-known, пропускаем не фатально return; } const data = JSON.parse(res.body); if (!data.jwks_uri) throw new Error('Нет jwks_uri'); }); // ── 3. ipwhitelist app: login + read-only endpoints ──────────────────── console.log(' ── ipwhitelist app ──'); // Получаем сессионную куку для следующих запросов let sessionCookie = ''; // Логинимся и запоминаем куку await test('POST /login-token + установка cookie', async () => { const loginRes = await httpRequest(`${APP_URL}/login-token`, 'POST', { 'Content-Type': 'application/x-www-form-urlencoded', }, 'token=' + encodeURIComponent(tok) + '&returnTo=/'); if (loginRes.status !== 302) throw new Error('Ожидался 302, получен ' + loginRes.status); const sc = loginRes.headers['set-cookie']; if (!sc) throw new Error('Нет set-cookie'); sessionCookie = sc.map(c => c.split(';')[0]).join('; '); }); // GET / — для admin без ?company= ожидается 302 (редирект на ?company=) await test('GET /: страница загружается', async () => { const res = await httpRequest(`${APP_URL}/`, 'GET', { 'Cookie': sessionCookie }); if (res.status === 200 || res.status === 302) return; // оба OK throw new Error('HTTP ' + res.status); }); // GET /export — доступен всем аутентифицированным await test('GET /export: txt-файл отдаётся', async () => { const res = await httpRequest(`${APP_URL}/export`, 'GET', { 'Cookie': sessionCookie }); if (res.status !== 200) throw new Error('HTTP ' + res.status); const ct = res.headers['content-type'] || ''; if (!ct.includes('text/plain')) throw new Error('Не text/plain: ' + ct); }); // GET /api/v1/entries — API endpoint await test('GET /api/v1/entries: записи читаются', async () => { const res = await httpRequest(`${APP_URL}/api/v1/entries`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'application/json', }); if (res.status !== 200) throw new Error('HTTP ' + res.status); const data = JSON.parse(res.body); if (!Array.isArray(data.entries)) throw new Error('entries не массив'); if (typeof data.limit !== 'number') throw new Error('limit не число'); }); // GET /api/v1/entries/export — API export await test('GET /api/v1/entries/export: API export доступен', async () => { const res = await httpRequest(`${APP_URL}/api/v1/entries/export`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'text/plain', }); if (res.status !== 200) throw new Error('HTTP ' + res.status); }); // Admin-only тесты if (payload.ClientID === 'WZ01112' || iamUser?.userInfo?.isAdmin) { await test('GET /api/v1/companies: admin может читать (JSON)', async () => { const res = await httpRequest(`${APP_URL}/api/v1/companies`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'application/json', }); if (res.status !== 200) throw new Error('HTTP ' + res.status); const data = JSON.parse(res.body); if (!Array.isArray(data.companies)) throw new Error('companies не массив'); }); await test('GET /admin: admin UI доступна', async () => { const res = await httpRequest(`${APP_URL}/admin`, 'GET', { 'Cookie': sessionCookie }); if (res.status !== 200) throw new Error('HTTP ' + res.status); }); } else { // Проверяем что не-admin не может получить /admin await test('GET /admin: не-admin получает 302 или 403', async () => { const res = await httpRequest(`${APP_URL}/admin`, 'GET', { 'Cookie': sessionCookie }); if (res.status === 200) throw new Error('Не-admin получил /admin без прав'); }); await test('GET /api/v1/companies: не-admin 403', async () => { const res = await httpRequest(`${APP_URL}/api/v1/companies`, 'GET', { 'Authorization': 'Bearer ' + tok, 'Accept': 'application/json', }); if (res.status !== 403) throw new Error('Ожидался 403, получен ' + res.status); }); } } // ── Итог ────────────────────────────────────────────────────────────────── console.log(`\n══════════ ИТОГ ══════════`); console.log(` ✅ Пройдено: ${passed}`); console.log(` ❌ Провалено: ${failed}`); console.log(` Всего: ${passed + failed}`); process.exit(failed > 0 ? 1 : 0); } run().catch(e => { console.error('FATAL:', e.message); process.exit(1); });