Files
Haven/test/socketServerHarness.js
T

97 lines
3.8 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');
}
// Waits for the server to exit before removing its data folder: Windows
// will not delete files a running process still holds open, which left a
// test folder behind in the temp directory on every run.
async function stop() {
for (const s of sockets) s.close();
if (server && server.exitCode === null) {
const exited = new Promise((r) => server.once('exit', r));
server.kill();
await exited;
}
try { fs.rmSync(DATA, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } 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 };