restreamer-ui-v2/scripts/test-whip-e2e.js
Cesar Mendivil 00e98a19b3 feat: add InternalWHIP component and associated tests
- Implemented the InternalWHIP component for managing WHIP server configurations.
- Added functionality to load live WHIP state from Core and handle OBS URL generation.
- Included polling for active streams and notifying parent components of state changes.
- Created comprehensive tests for the InternalWHIP component covering various scenarios including fallback mechanisms and state changes.

test: add integration tests for WHIP source component

- Developed end-to-end tests for the InternalWHIP component to verify its behavior under different configurations.
- Ensured that the component correctly handles the loading of WHIP state, displays appropriate messages, and emits the correct onChange events.

test: add Settings WHIP configuration tests

- Implemented tests for the WHIP settings tab to validate loading and saving of WHIP configurations.
- Verified that the correct values are sent back to the Core when the user saves changes.
- Ensured that the UI reflects the current state of the WHIP configuration after Core restarts or changes.
2026-03-14 12:27:53 -07:00

231 lines
9.3 KiB
JavaScript

#!/usr/bin/env node
/**
* E2E: Valida la generación de URL WHIP via LiveKit Ingress.
*
* Ejecuta 3 pruebas en secuencia:
* 1. Conectividad directa al LiveKit Server (HTTPS)
* 2. IngressClient.createIngress() directo (sin pasar por la UI)
* 3. POST /api/whip/info al servidor Node de la UI
*
* Uso:
* node scripts/test-whip-e2e.js
* node scripts/test-whip-e2e.js http://localhost:3000 # UI en otro host
*
* Sin argumentos usa las vars de entorno del docker-compose o sus defaults.
*/
'use strict';
const http = require('http');
const https = require('https');
// ── Configuración (mirrors docker-compose.yml) ─────────────────────────────
const LK_API_KEY = process.env.LIVEKIT_API_KEY || 'APIBTqTGxf9htMK';
const LK_API_SECRET = process.env.LIVEKIT_API_SECRET || '0dOHWPffwneaPg7OYpe4PeAes21zLJfeYJB9cKzSTtXW';
const LK_WS_URL = process.env.LIVEKIT_WS_URL || 'wss://livekit-server.nextream.sytes.net';
const LK_HTTP_URL = LK_WS_URL.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://');
const UI_HOST = (process.argv[2] || 'http://localhost:3002').replace(/\/+$/, '');
const ROOM = 'e2e-whip-test-' + Date.now();
// ── Helpers ────────────────────────────────────────────────────────────────
let failures = 0;
function ok(msg) { console.log(`${msg}`); }
function fail(msg) { console.error(`${msg}`); failures++; }
function section(title) { console.log(`\n━━ ${title} ━━`); }
function httpRequest(url, { method = 'GET', body, headers = {} } = {}) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const lib = parsed.protocol === 'https:' ? https : http;
const raw = body ? JSON.stringify(body) : undefined;
const reqOpts = {
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path: parsed.pathname + (parsed.search || ''),
method,
headers: {
...headers,
...(raw ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(raw) } : {}),
},
// Allow self-signed certs for local testing
rejectUnauthorized: false,
};
const req = lib.request(reqOpts, (res) => {
let buf = '';
res.on('data', (c) => { buf += c; });
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: buf }));
});
req.on('error', reject);
if (raw) req.write(raw);
req.end();
});
}
// ── Test 1: Conectividad al LiveKit Server ─────────────────────────────────
async function testLiveKitConnectivity() {
section('Test 1: Conectividad a LiveKit Server');
console.log(` URL: ${LK_HTTP_URL}`);
try {
const r = await httpRequest(`${LK_HTTP_URL}/`, { method: 'GET' });
// LiveKit devuelve 404 o 405 en /, pero cualquier respuesta HTTP confirma conectividad
if (r.status < 600) {
ok(`LiveKit Server responde HTTP ${r.status} (conectividad OK)`);
} else {
fail(`Respuesta inesperada: ${r.status}`);
}
} catch (err) {
fail(`No se puede conectar a ${LK_HTTP_URL}: ${err.message}`);
console.log(' → Verifica que livekit-server.nextream.sytes.net sea accesible');
}
}
// ── Test 2: IngressClient directo (bypass UI) ──────────────────────────────
async function testIngressClientDirect() {
section('Test 2: IngressClient.createIngress() directo');
console.log(` API Key: ${LK_API_KEY}`);
console.log(` LK Server: ${LK_HTTP_URL}`);
console.log(` Room: ${ROOM}`);
// Construir JWT manualmente usando mismo algoritmo que livekit-server-sdk
// para aislar si el problema es de credenciales o de config del server.
try {
const { IngressClient, IngressInput } = require(require('path').resolve(__dirname, '../server/node_modules/livekit-server-sdk'));
const client = new IngressClient(LK_HTTP_URL, LK_API_KEY, LK_API_SECRET);
const ingress = await client.createIngress(IngressInput.WHIP_INPUT, {
name: 'e2e-test',
roomName: ROOM,
participantIdentity: 'e2e-obs',
participantName: 'E2E OBS',
});
ok(`createIngress() OK → ingressId: ${ingress.ingressId}`);
ok(`streamKey: ${ingress.streamKey}`);
ok(`ingress.url (reportada por LiveKit): ${ingress.url}`);
// Cleanup: intentar borrar el ingress creado
try {
await client.deleteIngress(ingress.ingressId);
ok(`Ingress de prueba eliminado`);
} catch (_) {}
return ingress;
} catch (err) {
fail(`createIngress() falló: ${err.message}`);
if (err.message.includes('401') || err.message.includes('Unauthorized') || err.message.includes('JWT')) {
console.log('\n ── Diagnóstico JWT ───────────────────────────────────────');
console.log(' El error 401/JWT indica que las credenciales API no coinciden');
console.log(' con las que tiene configurado el LiveKit Server.\n');
console.log(' Verifica en tu livekit-server.yaml:');
console.log(' key_file: <ruta>');
console.log(' o');
console.log(' keys:');
console.log(` ${LK_API_KEY}: <secret>`);
console.log(`\n El secret configurado en la UI es: ${LK_API_SECRET}`);
console.log(' Debe coincidir EXACTAMENTE con el que tiene livekit-server.');
}
if (err.message.includes('not found') || err.message.includes('ENOTFOUND')) {
console.log(' → El hostname no resuelve. Verifica DNS / VPN.');
}
return null;
}
}
// ── Test 3: POST /api/whip/info via Node server ────────────────────────────
async function testWhipInfoEndpoint() {
section(`Test 3: POST ${UI_HOST}/api/whip/info`);
try {
const r = await httpRequest(`${UI_HOST}/api/whip/info`, {
method: 'POST',
body: { room: ROOM, identity: 'e2e-test', name: 'E2E Test' },
});
let data;
try { data = JSON.parse(r.body); } catch { data = { _raw: r.body }; }
if (r.status === 200) {
ok(`HTTP 200`);
ok(`whipUrl: ${data.whipUrl}`);
ok(`streamKey: ${data.streamKey}`);
ok(`ingressId: ${data.ingressId}`);
// Validaciones de la URL
try {
const u = new URL(data.whipUrl);
ok(`whipUrl es URL válida (protocol: ${u.protocol})`);
if (data.whipUrl.includes('192.168.')) {
fail(`whipUrl contiene IP privada — OBS externo no puede acceder`);
} else {
ok(`whipUrl no contiene IP privada`);
}
if (data.whipUrl.endsWith(data.streamKey)) {
ok(`whipUrl termina en streamKey`);
} else {
fail(`whipUrl NO termina en streamKey (verifica la construcción)`);
}
} catch {
fail(`whipUrl no es URL válida: ${data.whipUrl}`);
}
} else if (r.status === 404 || r.status === 405) {
fail(`HTTP ${r.status} — la ruta /api/whip/info no existe en el servidor`);
console.log(' → Verifica que el servidor Node (port 3002) tenga la ruta registrada');
console.log(' → Y que Caddy tenga el bloque "handle /api/whip/*"');
} else {
fail(`HTTP ${r.status}: ${JSON.stringify(data)}`);
}
} catch (err) {
fail(`Request falló: ${err.message}`);
console.log(` → Verifica que el servidor esté corriendo en ${UI_HOST}`);
}
}
// ── Test 4: Proxy Caddy /api/whip/* (si se pasa URL del UI en :3000) ───────
async function testCaddyProxy() {
if (!process.argv[2]) return; // Solo si se especificó UI_HOST explícitamente
section(`Test 4: Caddy proxy POST ${process.argv[2]}/api/whip/info`);
try {
const r = await httpRequest(`${process.argv[2]}/api/whip/info`, {
method: 'POST',
body: { room: ROOM + '-caddy', identity: 'e2e-caddy', name: 'E2E Caddy' },
});
const data = JSON.parse(r.body).catch?.() || JSON.parse(r.body);
if (r.status === 200) {
ok(`Caddy proxy OK → whipUrl: ${data.whipUrl}`);
} else {
fail(`Caddy proxy HTTP ${r.status}: ${r.body.slice(0, 200)}`);
}
} catch (err) {
fail(`Caddy proxy error: ${err.message}`);
}
}
// ── Main ───────────────────────────────────────────────────────────────────
(async () => {
console.log('\n╔══════════════════════════════════════╗');
console.log('║ WHIP Ingress E2E Test Suite ║');
console.log('╚══════════════════════════════════════╝');
console.log(` Fecha: ${new Date().toISOString()}`);
// Check livekit-server-sdk disponible (busca en server/node_modules)
const sdkPath = require('path').resolve(__dirname, '../server/node_modules/livekit-server-sdk');
try {
require(sdkPath);
} catch {
console.error('\n✗ livekit-server-sdk no encontrado en server/node_modules.');
console.error(' Ejecuta: cd server && npm install');
process.exit(1);
}
await testLiveKitConnectivity();
await testIngressClientDirect();
await testWhipInfoEndpoint();
await testCaddyProxy();
console.log('\n━━ Resultado final ━━');
if (failures === 0) {
console.log(' ✅ TODOS los tests pasaron\n');
} else {
console.log(`${failures} test(s) fallaron — revisa los errores arriba\n`);
process.exit(1);
}
})();