Files
Haven/test/socketServerHarness.js
T

90 lines
3.4 KiB
JavaScript

'use strict';
// Shared by the socket access tests: boot a throwaway server on a scratch
// port and data dir, register accounts, connect sockets and wait for events.
// Loading this file on its own does nothing.
const http = require('node:http');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const { spawn } = require('node:child_process');
const { io } = require('socket.io-client');
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
function createHarness(port, name) {
const BASE = `http://localhost:${port}`;
const DATA = path.join(os.tmpdir(), `haven-${name}-${Date.now()}`);
const sockets = [];
let server;
const post = (p, body) => new Promise((res, rej) => {
const d = JSON.stringify(body);
const r = http.request({ host: 'localhost', port, path: p, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) } },
(x) => { let b = ''; x.on('data', (c) => (b += c)); x.on('end', () => { try { res(JSON.parse(b)); } catch { res({ raw: b }); } }); });
r.on('error', rej); r.write(d); r.end();
});
async function start() {
fs.mkdirSync(DATA, { recursive: true });
server = spawn(process.execPath, ['server.js'], {
cwd: path.join(__dirname, '..'),
env: { ...process.env, PORT: String(port), HAVEN_DATA_DIR: DATA, ADMIN_USERNAME: 'admin', FORCE_HTTP: 'true' },
stdio: 'ignore',
});
for (let i = 0; i < 60; i++) {
try {
await new Promise((res, rej) => http.get(`${BASE}/api/health`, (r) => (r.statusCode === 200 ? res() : rej())).on('error', rej));
return;
} catch { await wait(500); }
}
throw new Error('server did not start');
}
function stop() {
for (const s of sockets) s.close();
if (server) server.kill();
try { fs.rmSync(DATA, { recursive: true, force: true }); } catch (err) { console.warn('could not remove test data:', err.message); }
}
async function register(username) {
const r = await post('/api/auth/register', { username, password: 'access12345', eulaVersion: '2.0', ageVerified: true });
if (!r.token) throw new Error(`register ${username} failed: ${JSON.stringify(r)}`);
return { token: r.token, id: r.user.id };
}
function connect(token) {
const s = io(BASE, { auth: { token }, transports: ['websocket'], forceNew: true });
sockets.push(s);
return new Promise((res, rej) => { s.on('connect', () => res(s)); s.on('connect_error', rej); });
}
// The next matching event, or null after `ms`.
function next(sock, event, filter = () => true, ms = 3000) {
return new Promise((res) => {
const t = setTimeout(() => { sock.off(event, h); res(null); }, ms);
const h = (...args) => { if (!filter(...args)) return; clearTimeout(t); sock.off(event, h); res(args.length > 1 ? args : args[0]); };
sock.on(event, h);
});
}
// Emit with an acknowledgement callback.
const ask = (sock, event, data, ms = 3000) => new Promise((res) => {
const t = setTimeout(() => res(null), ms);
sock.emit(event, data, (reply) => { clearTimeout(t); res(reply); });
});
// Every channel the socket's user can see, by name.
async function channels(sock) {
const list = next(sock, 'channels-list');
sock.emit('get-channels');
return (await list) || [];
}
return { start, stop, register, connect, next, ask, channels, post, dataDir: DATA };
}
module.exports = { createHarness, wait };