* add test suite, mostly covers integration testing, tests are only backend side * workflow runs the correct script * workflow runs the correct script * workflow runs the correct script * unit tests incoming * Fix multer silent rejections and error handler info leak - Revert cb(null, false) to cb(new Error(...)) in auth.ts, collab.ts, and files.ts so invalid uploads return an error instead of silently dropping the file - Error handler in app.ts now always returns 500 / "Internal server error" instead of forwarding err.message to the client * Use statusCode consistently for multer errors and error handler - Error handler in app.ts reads err.statusCode to forward the correct HTTP status while keeping the response body generic
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
/**
|
|
* WebSocket test client helper.
|
|
*
|
|
* Usage:
|
|
* import http from 'http';
|
|
* import { setupWebSocket } from '../../src/websocket';
|
|
* import { WsTestClient, getWsToken } from '../helpers/ws-client';
|
|
*
|
|
* let server: http.Server;
|
|
* let client: WsTestClient;
|
|
*
|
|
* beforeAll(async () => {
|
|
* const app = createApp();
|
|
* server = http.createServer(app);
|
|
* setupWebSocket(server);
|
|
* await new Promise<void>(res => server.listen(0, res));
|
|
* });
|
|
*
|
|
* afterAll(() => server.close());
|
|
*
|
|
* it('connects', async () => {
|
|
* const addr = server.address() as AddressInfo;
|
|
* const token = await getWsToken(addr.port, userId);
|
|
* client = new WsTestClient(`ws://localhost:${addr.port}/ws?token=${token}`);
|
|
* const msg = await client.waitForMessage('welcome');
|
|
* expect(msg.type).toBe('welcome');
|
|
* });
|
|
*/
|
|
|
|
import WebSocket from 'ws';
|
|
|
|
export interface WsMessage {
|
|
type: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export class WsTestClient {
|
|
private ws: WebSocket;
|
|
private messageQueue: WsMessage[] = [];
|
|
private waiters: Array<{ type: string; resolve: (msg: WsMessage) => void; reject: (err: Error) => void }> = [];
|
|
|
|
constructor(url: string) {
|
|
this.ws = new WebSocket(url);
|
|
this.ws.on('message', (data: WebSocket.RawData) => {
|
|
try {
|
|
const msg = JSON.parse(data.toString()) as WsMessage;
|
|
const waiterIdx = this.waiters.findIndex(w => w.type === msg.type || w.type === '*');
|
|
if (waiterIdx >= 0) {
|
|
const waiter = this.waiters.splice(waiterIdx, 1)[0];
|
|
waiter.resolve(msg);
|
|
} else {
|
|
this.messageQueue.push(msg);
|
|
}
|
|
} catch { /* ignore malformed messages */ }
|
|
});
|
|
}
|
|
|
|
/** Wait for a message of the given type (or '*' for any). */
|
|
waitForMessage(type: string, timeoutMs = 5000): Promise<WsMessage> {
|
|
// Check if already in queue
|
|
const idx = this.messageQueue.findIndex(m => type === '*' || m.type === type);
|
|
if (idx >= 0) {
|
|
return Promise.resolve(this.messageQueue.splice(idx, 1)[0]);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
const waiterIdx = this.waiters.findIndex(w => w.resolve === resolve);
|
|
if (waiterIdx >= 0) this.waiters.splice(waiterIdx, 1);
|
|
reject(new Error(`Timed out waiting for WS message type="${type}" after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
|
|
this.waiters.push({
|
|
type,
|
|
resolve: (msg) => { clearTimeout(timer); resolve(msg); },
|
|
reject,
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Send a JSON message. */
|
|
send(msg: Record<string, unknown>): void {
|
|
this.ws.send(JSON.stringify(msg));
|
|
}
|
|
|
|
/** Close the connection. */
|
|
close(): void {
|
|
this.ws.close();
|
|
}
|
|
|
|
/** Wait for the connection to be open. */
|
|
waitForOpen(timeoutMs = 3000): Promise<void> {
|
|
if (this.ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error('WS open timed out')), timeoutMs);
|
|
this.ws.once('open', () => { clearTimeout(timer); resolve(); });
|
|
this.ws.once('error', (err) => { clearTimeout(timer); reject(err); });
|
|
});
|
|
}
|
|
|
|
/** Wait for the connection to close. */
|
|
waitForClose(timeoutMs = 3000): Promise<number> {
|
|
if (this.ws.readyState === WebSocket.CLOSED) return Promise.resolve(1000);
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error('WS close timed out')), timeoutMs);
|
|
this.ws.once('close', (code) => { clearTimeout(timer); resolve(code); });
|
|
});
|
|
}
|
|
}
|