add tests for WebSocketServer
This commit is contained in:
parent
fe6f513b01
commit
c4f04b2ff8
@ -18,6 +18,19 @@
|
||||
"no-var": "error",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off"
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -3123,6 +3123,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mock-socket": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-8.0.5.tgz",
|
||||
"integrity": "sha512-dE2EbcxJKQCeYLZSsI7BAiMZCe/bHbJ2LHb5aGwUuDmfoOINEJ8QI6qYJ85NHsSNkNa90F3s6onZcmt/+MppFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"url-parse": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@ -3700,6 +3709,12 @@
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
||||
"dev": true
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@ -3848,6 +3863,12 @@
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
|
||||
@ -4776,6 +4797,16 @@
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
|
||||
"dev": true
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
||||
|
@ -47,6 +47,7 @@
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.7.2",
|
||||
"mocha": "^6.2.2",
|
||||
"mock-socket": "8.0.5",
|
||||
"nodemon": "1.19.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"rimraf": "3.0.0",
|
||||
|
@ -42,7 +42,9 @@ export const TransmissionHandler = ({ realm }: { realm: IRealm; }): (client: ICl
|
||||
} else {
|
||||
// Wait for this client to connect/reconnect (XHR) for important
|
||||
// messages.
|
||||
if (type !== MessageType.LEAVE && type !== MessageType.EXPIRE && dstId) {
|
||||
const ignoredTypes = [MessageType.LEAVE, MessageType.EXPIRE];
|
||||
|
||||
if (!ignoredTypes.includes(type) && dstId) {
|
||||
realm.addMessageToQueue(dstId, message);
|
||||
} else if (type === MessageType.LEAVE && !dstId) {
|
||||
realm.removeClientById(srcId);
|
||||
|
@ -8,16 +8,18 @@ export interface IMessagesExpire {
|
||||
stopMessagesExpiration(): void;
|
||||
}
|
||||
|
||||
type CustomConfig = Pick<IConfig, 'cleanup_out_msgs' | 'expire_timeout'>;
|
||||
|
||||
export class MessagesExpire implements IMessagesExpire {
|
||||
private readonly realm: IRealm;
|
||||
private readonly config: IConfig;
|
||||
private readonly config: CustomConfig;
|
||||
private readonly messageHandler: IMessageHandler;
|
||||
|
||||
private timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor({ realm, config, messageHandler }: {
|
||||
realm: IRealm;
|
||||
config: IConfig;
|
||||
config: CustomConfig;
|
||||
messageHandler: IMessageHandler;
|
||||
}) {
|
||||
this.realm = realm;
|
||||
|
@ -18,26 +18,32 @@ interface IAuthParams {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
type CustomConfig = Pick<IConfig, 'path' | 'key' | 'concurrent_limit'>;
|
||||
|
||||
const WS_PATH = 'peerjs';
|
||||
|
||||
export class WebSocketServer extends EventEmitter implements IWebSocketServer {
|
||||
|
||||
public readonly path: string;
|
||||
private readonly realm: IRealm;
|
||||
private readonly config: IConfig;
|
||||
private readonly webSocketServer: WebSocketLib.Server;
|
||||
private readonly config: CustomConfig;
|
||||
public readonly socketServer: WebSocketLib.Server;
|
||||
|
||||
constructor({ server, realm, config }: { server: any, realm: IRealm, config: IConfig; }) {
|
||||
constructor({ server, realm, config }: { server: any, realm: IRealm, config: CustomConfig; }) {
|
||||
super();
|
||||
|
||||
this.setMaxListeners(0);
|
||||
|
||||
this.realm = realm;
|
||||
this.config = config;
|
||||
|
||||
const path = this.config.path;
|
||||
this.path = path + (path[path.length - 1] !== "/" ? "/" : "") + "peerjs";
|
||||
this.path = `${path}${path.endsWith('/') ? "" : "/"}${WS_PATH}`;
|
||||
|
||||
this.webSocketServer = new WebSocketLib.Server({ path, server });
|
||||
this.socketServer = new WebSocketLib.Server({ path, server });
|
||||
|
||||
this.webSocketServer.on("connection", (socket: MyWebSocket, req) => this._onSocketConnection(socket, req));
|
||||
this.webSocketServer.on("error", (error: Error) => this._onSocketError(error));
|
||||
this.socketServer.on("connection", (socket: MyWebSocket, req) => this._onSocketConnection(socket, req));
|
||||
this.socketServer.on("error", (error: Error) => this._onSocketError(error));
|
||||
}
|
||||
|
||||
private _onSocketConnection(socket: MyWebSocket, req: IncomingMessage): void {
|
||||
|
@ -4,7 +4,7 @@ import { Realm } from '../../../src/models/realm';
|
||||
import { CheckBrokenConnections } from '../../../src/services/checkBrokenConnections';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
describe('checkBrokenConnections service', () => {
|
||||
describe('CheckBrokenConnections', () => {
|
||||
it('should remove client after 2 checks', async () => {
|
||||
const realm = new Realm();
|
||||
const doubleCheckTime = 55;//~ equals to checkBrokenConnections.checkInterval * 2
|
||||
|
78
test/services/messagesExpire/index.ts
Normal file
78
test/services/messagesExpire/index.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { expect } from 'chai';
|
||||
import { Client } from '../../../src/models/client';
|
||||
import { Realm } from '../../../src/models/realm';
|
||||
import { IMessage } from '../../../src/models/message';
|
||||
import { MessagesExpire } from '../../../src/services/messagesExpire';
|
||||
import { MessageHandler } from '../../../src/messageHandler';
|
||||
import { MessageType } from '../../../src/enums';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
describe('MessagesExpire', () => {
|
||||
const createTestMessage = (): IMessage => {
|
||||
return {
|
||||
type: MessageType.OPEN,
|
||||
src: 'src',
|
||||
dst: 'dst'
|
||||
};
|
||||
};
|
||||
|
||||
it('should remove client if no read from queue', async () => {
|
||||
const realm = new Realm();
|
||||
const messageHandler = new MessageHandler(realm);
|
||||
const checkInterval = 10;
|
||||
const expireTimeout = 50;
|
||||
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout };
|
||||
|
||||
const messagesExpire = new MessagesExpire({ realm, config, messageHandler });
|
||||
|
||||
const client = new Client({ id: 'id', token: '' });
|
||||
realm.setClient(client, 'id');
|
||||
realm.addMessageToQueue(client.getId(), createTestMessage());
|
||||
|
||||
messagesExpire.startMessagesExpiration();
|
||||
|
||||
await wait(checkInterval * 2);
|
||||
|
||||
expect(realm.getMessageQueueById(client.getId())?.getMessages().length).to.be.eq(1);
|
||||
|
||||
await wait(expireTimeout);
|
||||
|
||||
expect(realm.getMessageQueueById(client.getId())).to.be.undefined;
|
||||
|
||||
messagesExpire.stopMessagesExpiration();
|
||||
});
|
||||
|
||||
it('should fire EXPIRE message', async () => {
|
||||
const realm = new Realm();
|
||||
const messageHandler = new MessageHandler(realm);
|
||||
const checkInterval = 10;
|
||||
const expireTimeout = 50;
|
||||
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout };
|
||||
|
||||
const messagesExpire = new MessagesExpire({ realm, config, messageHandler });
|
||||
|
||||
const client = new Client({ id: 'id', token: '' });
|
||||
realm.setClient(client, 'id');
|
||||
realm.addMessageToQueue(client.getId(), createTestMessage());
|
||||
|
||||
let handled = false;
|
||||
|
||||
messageHandler.handle = (client, message): boolean => {
|
||||
expect(client).to.be.undefined;
|
||||
expect(message.type).to.be.eq(MessageType.EXPIRE);
|
||||
|
||||
handled = true;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
messagesExpire.startMessagesExpiration();
|
||||
|
||||
await wait(checkInterval * 2);
|
||||
await wait(expireTimeout);
|
||||
|
||||
expect(handled).to.be.true;
|
||||
|
||||
messagesExpire.stopMessagesExpiration();
|
||||
});
|
||||
});
|
195
test/services/webSocketServer/index.ts
Normal file
195
test/services/webSocketServer/index.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { expect } from 'chai';
|
||||
import { Server, WebSocket } from 'mock-socket';
|
||||
import { Realm } from '../../../src/models/realm';
|
||||
import { WebSocketServer } from '../../../src/services/webSocketServer';
|
||||
import { Errors, MessageType } from '../../../src/enums';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
type Destroyable<T> = T & { destroy?: () => Promise<void>; };
|
||||
|
||||
const checkOpen = async (c: WebSocket): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
c.onmessage = (event: object & { data?: string; }): void => {
|
||||
c.onmessage = null;
|
||||
const message = JSON.parse(event.data as string);
|
||||
resolve(message.type === MessageType.OPEN);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Errors; }[]): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
const restMessages = [...msgs];
|
||||
|
||||
const finish = (success = false): void => {
|
||||
c.onmessage = null;
|
||||
resolve(success);
|
||||
};
|
||||
|
||||
c.onmessage = (event: object & { data?: string; }): void => {
|
||||
const [mes] = restMessages;
|
||||
|
||||
if (!mes) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
restMessages.shift();
|
||||
|
||||
const message = JSON.parse(event.data as string);
|
||||
if (message.type !== mes.type) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
const isOk = !mes.error || message.payload?.msg === mes.error;
|
||||
|
||||
if (!isOk) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (restMessages.length === 0) {
|
||||
finish(true);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path: string; key: string; concurrent_limit: number; }; url: string; }): Destroyable<WebSocketServer> => {
|
||||
const server = new Server(url);
|
||||
const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({ server, realm, config });
|
||||
|
||||
server.on('connection', (socket: WebSocket & { on?: (eventName: string, callback: () => void) => void; }) => {
|
||||
const s = webSocketServer.socketServer;
|
||||
s.emit('connection', socket, { url: socket.url });
|
||||
|
||||
socket.onclose = (): void => {
|
||||
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1];
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const client = realm.getClientById(userId);
|
||||
|
||||
const clientSocket = client?.getSocket();
|
||||
|
||||
if (!clientSocket) return;
|
||||
|
||||
(clientSocket as unknown as WebSocket).listeners['server::close']?.forEach((s: () => void) => s());
|
||||
};
|
||||
|
||||
socket.onmessage = (event: object & { data?: string; }): void => {
|
||||
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1];
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const client = realm.getClientById(userId);
|
||||
|
||||
const clientSocket = client?.getSocket();
|
||||
|
||||
if (!clientSocket) return;
|
||||
|
||||
(clientSocket as unknown as WebSocket).listeners['server::message']?.forEach((s: (data: object) => void) => s(event));
|
||||
};
|
||||
});
|
||||
|
||||
webSocketServer.destroy = async (): Promise<void> => {
|
||||
server.close();
|
||||
};
|
||||
|
||||
return webSocketServer;
|
||||
};
|
||||
|
||||
describe('WebSocketServer', () => {
|
||||
|
||||
it('should return valid path', () => {
|
||||
const realm = new Realm();
|
||||
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
|
||||
const config2 = { ...config, path: 'path' };
|
||||
const server = new Server('path1');
|
||||
const server2 = new Server('path2');
|
||||
|
||||
const webSocketServer = new WebSocketServer({ server, realm, config });
|
||||
|
||||
expect(webSocketServer.path).to.be.eq('/peerjs');
|
||||
|
||||
const webSocketServer2 = new WebSocketServer({ server: server2, realm, config: config2 });
|
||||
|
||||
expect(webSocketServer2.path).to.be.eq('path/peerjs');
|
||||
|
||||
server.stop();
|
||||
server2.stop();
|
||||
});
|
||||
|
||||
it(`should check client's params`, async () => {
|
||||
const realm = new Realm();
|
||||
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
|
||||
const fakeURL = 'ws://localhost:8080/peerjs';
|
||||
|
||||
const getError = async (url: string, validError: Errors = Errors.INVALID_WS_PARAMETERS): Promise<boolean> => {
|
||||
const webSocketServer = createTestServer({ url, realm, config });
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
const errorSent = await checkSequence(ws, [{ type: MessageType.ERROR, error: validError }]);
|
||||
|
||||
ws.close();
|
||||
|
||||
await webSocketServer.destroy?.();
|
||||
|
||||
return errorSent;
|
||||
};
|
||||
|
||||
expect(await getError(fakeURL)).to.be.true;
|
||||
expect(await getError(`${fakeURL}?key=${config.key}`)).to.be.true;
|
||||
expect(await getError(`${fakeURL}?key=${config.key}&id=1`)).to.be.true;
|
||||
expect(await getError(`${fakeURL}?key=notValidKey&id=userId&token=userToken`, Errors.INVALID_KEY)).to.be.true;
|
||||
});
|
||||
|
||||
it(`should check concurrent limit`, async () => {
|
||||
const realm = new Realm();
|
||||
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
|
||||
const fakeURL = 'ws://localhost:8080/peerjs';
|
||||
|
||||
const createClient = (id: string): Destroyable<WebSocket> => {
|
||||
const url = `${fakeURL}?key=${config.key}&id=${id}&token=${id}`;
|
||||
const webSocketServer = createTestServer({ url, realm, config });
|
||||
const ws: Destroyable<WebSocket> = new WebSocket(url);
|
||||
|
||||
ws.destroy = async (): Promise<void> => {
|
||||
ws.close();
|
||||
|
||||
wait(10);
|
||||
|
||||
webSocketServer.destroy?.();
|
||||
|
||||
wait(10);
|
||||
|
||||
ws.destroy = undefined;
|
||||
};
|
||||
|
||||
return ws;
|
||||
};
|
||||
|
||||
|
||||
const c1 = createClient('1');
|
||||
|
||||
expect(await checkOpen(c1)).to.be.true;
|
||||
|
||||
const c2 = createClient('2');
|
||||
|
||||
expect(await checkSequence(c2, [
|
||||
{ type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED }
|
||||
])).to.be.true;
|
||||
|
||||
await c1.destroy?.();
|
||||
await c2.destroy?.();
|
||||
|
||||
await wait(10);
|
||||
|
||||
expect(realm.getClientsIds().length).to.be.eq(0);
|
||||
|
||||
const c3 = createClient('3');
|
||||
|
||||
expect(await checkOpen(c3)).to.be.true;
|
||||
|
||||
await c3.destroy?.();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user