Files
ipwhitelist-app/tests/real-users-smoke.js
T
naeel 9fd808bc1f chore: DB defaults for managed platform, reduce required env vars
- db.js: DB_HOST default postgresqlk8s-master, DB_NAME default ipwhitelist, DB_USER default super
- server.js: fail-fast for DB_PASS in production (instead of IAM_API_URL)
- After this change: only DB_PASS is required for mock mode
2026-06-09 19:20:29 +04:00

279 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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=<id>)
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);
});