@@ -0,0 +1,278 @@
/**
* 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 ) ;
} ) ;