- 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.
231 lines
9.3 KiB
JavaScript
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);
|
|
}
|
|
})();
|