Files
church-translate/app/_test-oplocal.mjs
T

62 lines
3.3 KiB
JavaScript
Raw Normal View History

/* The operator desk must be unreachable from the internet — page, API and
socket — and completely normal on the LAN. */
import { spawn } from 'child_process';
import { WebSocket } from 'ws';
import fs from 'fs'; import os from 'os'; import path from 'path';
import { fileURLToPath } from 'url';
const DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'ct-oplocal-'));
const PORT = 3198;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srv = spawn(process.execPath, [path.join(__dirname, 'server', 'server.js')],
{ env: { ...process.env, PORT: String(PORT), DATA_DIR: DIR }, stdio: ['ignore', 'pipe', 'pipe'] });
const logs = []; srv.stdout.on('data', d => logs.push(String(d))); srv.stderr.on('data', d => logs.push(String(d)));
const sleep = ms => new Promise(r => setTimeout(r, ms));
const B = 'http://127.0.0.1:' + PORT;
const CFG = path.join(DIR, 'config.json');
for (let i = 0; i < 80; i++) { if (fs.existsSync(CFG)) { try { await fetch(B + '/healthz'); break; } catch (_) {} } await sleep(150); }
const cfg = JSON.parse(fs.readFileSync(CFG, 'utf8'));
const PIN = cfg.operatorPin, KEY = cfg.controlToken;
console.log('new-install PIN: ' + PIN.length + ' digits (' + PIN + ')\n');
let pass = 0, fail = 0;
const check = (l, ok, d) => { console.log((ok ? ' ok ' : ' FAIL ') + l + (d ? ' ' + d : '')); ok ? pass++ : fail++; };
const TUN = { 'x-forwarded-for': '203.0.113.7' };
const code = async (p, h) => (await fetch(B + p, { headers: h || {} })).status;
console.log('from the LAN (correct PIN):');
check('/operator page loads', await code('/operator') === 200);
check('/api/config accepts the PIN', await code('/api/config?pin=' + PIN) === 200);
console.log('\nfrom the internet (same correct PIN):');
check('/operator page is 404', await code('/operator', TUN) === 404, 'not even the login box');
const c = await code('/api/config?pin=' + PIN, TUN);
check('/api/config is refused', c === 401 || c === 429, 'HTTP ' + c);
console.log('\nthe listener page must still work from the internet:');
check('/ is served', await code('/', TUN) === 200);
const lm = await code('/live/eng_Latn.m3u8', TUN);
check('/live/*.m3u8 answers', lm === 200 || lm === 404, 'HTTP ' + lm + ' (404 = nothing playing yet)');
console.log('\noperator WEBSOCKET:');
const tryWs = (headers) => new Promise((res) => {
const ws = new WebSocket(B.replace('http','ws') + '/ws?role=operator&pin=' + PIN, { headers });
const t = setTimeout(() => { try { ws.close(); } catch (_) {} res('timeout'); }, 3000);
ws.on('message', () => { clearTimeout(t); ws.close(); res('accepted'); });
ws.on('close', (c, r) => { clearTimeout(t); res('closed: ' + String(r || c)); });
ws.on('error', () => {});
});
check('LAN socket is accepted', await tryWs({}) === 'accepted');
const w = await tryWs(TUN);
check('tunnel socket is closed', w.startsWith('closed'), w);
console.log('\ncontrol API (Stream Deck) is unaffected — it uses a long random token:');
check('control works from the LAN', await code('/api/control/status?key=' + KEY) === 200);
console.log('\n--- log ---');
console.log(logs.join('').split('\n').filter(l => /refused|operator/.test(l)).map(l => ' ' + l).join('\n'));
console.log('\n' + pass + ' passed, ' + fail + ' failed');
srv.kill(); fs.rmSync(DIR, { recursive: true, force: true });
process.exit(fail ? 1 : 0);