mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import http from 'node:http';
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { WebSocketServer } from 'ws';
|
|
|
|
/**
|
|
* A bind failure must stay loud after the ws server moved in front of listen().
|
|
*
|
|
* `ws` registers `error: this.emit.bind(this, 'error')` on the http server it
|
|
* attaches to (websocket-server.js). Since buildApp now creates the ws server
|
|
* BEFORE index.ts calls listen(), the http server already has an error listener
|
|
* by the time the bind is attempted — which is exactly the condition under which
|
|
* Node stops throwing on an unhandled one.
|
|
*
|
|
* The failure that hides behind that is not subtle: EADDRINUSE gets forwarded to
|
|
* the WebSocketServer, and if its handler is a no-op the process survives, the
|
|
* listen callback never runs, so the startup banner never prints and the
|
|
* scheduler never starts. A container that looks healthy and serves nothing.
|
|
*
|
|
* These cases pin the mechanism rather than index.ts itself, because the module
|
|
* calls process.exit and boots the whole app on import.
|
|
*/
|
|
async function occupyAPort(): Promise<{ port: number; release: () => void }> {
|
|
const blocker = http.createServer();
|
|
await new Promise<void>((resolve) => blocker.listen(0, '127.0.0.1', resolve));
|
|
const { port } = blocker.address() as { port: number };
|
|
return { port, release: () => blocker.close() };
|
|
}
|
|
|
|
describe('ws attached before listen()', () => {
|
|
it('WSBIND-001: ws silences the http server, so a no-op handler loses EADDRINUSE entirely', async () => {
|
|
const { port, release } = await occupyAPort();
|
|
try {
|
|
const server = http.createServer();
|
|
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
const swallowed = vi.fn();
|
|
wss.on('error', swallowed); // the shape this used to have
|
|
|
|
let listenCallbackRan = false;
|
|
server.listen(port, '127.0.0.1', () => {
|
|
listenCallbackRan = true;
|
|
});
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
// The bind failed, and the only thing that heard about it was the ws
|
|
// handler. Nothing threw, nothing exited.
|
|
expect(swallowed).toHaveBeenCalledTimes(1);
|
|
expect((swallowed.mock.calls[0][0] as NodeJS.ErrnoException).code).toBe('EADDRINUSE');
|
|
expect(listenCallbackRan).toBe(false);
|
|
|
|
wss.close();
|
|
server.close();
|
|
} finally {
|
|
release();
|
|
}
|
|
});
|
|
|
|
it('WSBIND-002: an explicit http-server error handler is what makes it fatal again', async () => {
|
|
const { port, release } = await occupyAPort();
|
|
try {
|
|
const server = http.createServer();
|
|
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
wss.on('error', () => {});
|
|
|
|
// What index.ts registers. It has to sit on the HTTP server: by the time
|
|
// ws re-emits, the process has already declined to throw.
|
|
const fatal = vi.fn();
|
|
server.on('error', fatal);
|
|
|
|
server.listen(port, '127.0.0.1', () => {});
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
expect(fatal).toHaveBeenCalledTimes(1);
|
|
expect((fatal.mock.calls[0][0] as NodeJS.ErrnoException).code).toBe('EADDRINUSE');
|
|
|
|
wss.close();
|
|
server.close();
|
|
} finally {
|
|
release();
|
|
}
|
|
});
|
|
});
|