index.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { describe, expect, it } from "@jest/globals";
  2. import { Server, WebSocket } from "mock-socket";
  3. import type { Server as HttpServer } from "node:http";
  4. import { Realm } from "../../../src/models/realm.ts";
  5. import { WebSocketServer } from "../../../src/services/webSocketServer/index.ts";
  6. import { Errors, MessageType } from "../../../src/enums.ts";
  7. import { wait } from "../../utils.ts";
  8. type Destroyable<T> = T & { destroy?: () => Promise<void> };
  9. const checkOpen = async (c: WebSocket): Promise<boolean> => {
  10. return new Promise((resolve) => {
  11. c.onmessage = (event: object & { data?: string }): void => {
  12. const message = JSON.parse(event.data as string);
  13. resolve(message.type === MessageType.OPEN);
  14. };
  15. });
  16. };
  17. const checkSequence = async (
  18. c: WebSocket,
  19. msgs: { type: MessageType; error?: Errors }[],
  20. ): Promise<boolean> => {
  21. return new Promise((resolve) => {
  22. const restMessages = [...msgs];
  23. const finish = (success = false): void => {
  24. resolve(success);
  25. };
  26. c.onmessage = (event: object & { data?: string }): void => {
  27. const [mes] = restMessages;
  28. if (!mes) {
  29. return finish();
  30. }
  31. restMessages.shift();
  32. const message = JSON.parse(event.data as string);
  33. if (message.type !== mes.type) {
  34. return finish();
  35. }
  36. const isOk = !mes.error || message.payload?.msg === mes.error;
  37. if (!isOk) {
  38. return finish();
  39. }
  40. if (restMessages.length === 0) {
  41. finish(true);
  42. }
  43. };
  44. });
  45. };
  46. const createTestServer = ({
  47. realm,
  48. config,
  49. url,
  50. }: {
  51. realm: Realm;
  52. config: { path: string; key: string; concurrent_limit: number };
  53. url: string;
  54. }): Destroyable<WebSocketServer> => {
  55. const server = new Server(url) as Server & HttpServer;
  56. const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({
  57. server,
  58. realm,
  59. config,
  60. });
  61. server.on(
  62. "connection",
  63. (
  64. socket: WebSocket & {
  65. on?: (eventName: string, callback: () => void) => void;
  66. },
  67. ) => {
  68. const s = webSocketServer.socketServer;
  69. s.emit("connection", socket, { url: socket.url });
  70. socket.onclose = (): void => {
  71. const userId = socket.url
  72. .split("?")[1]
  73. ?.split("&")
  74. .find((p) => p.startsWith("id"))
  75. ?.split("=")[1];
  76. if (!userId) return;
  77. const client = realm.getClientById(userId);
  78. const clientSocket = client?.getSocket();
  79. if (!clientSocket) return;
  80. (clientSocket as unknown as WebSocket).listeners[
  81. "server::close"
  82. ]?.forEach((s: () => void) => s());
  83. };
  84. socket.onmessage = (event: object & { data?: string }): void => {
  85. const userId = socket.url
  86. .split("?")[1]
  87. ?.split("&")
  88. .find((p) => p.startsWith("id"))
  89. ?.split("=")[1];
  90. if (!userId) return;
  91. const client = realm.getClientById(userId);
  92. const clientSocket = client?.getSocket();
  93. if (!clientSocket) return;
  94. (clientSocket as unknown as WebSocket).listeners[
  95. "server::message"
  96. ]?.forEach((s: (data: object) => void) => s(event));
  97. };
  98. },
  99. );
  100. webSocketServer.destroy = async (): Promise<void> => {
  101. server.close();
  102. };
  103. return webSocketServer;
  104. };
  105. describe("WebSocketServer", () => {
  106. it("should return valid path", () => {
  107. const realm = new Realm();
  108. const config = { path: "/", key: "testKey", concurrent_limit: 1 };
  109. const config2 = { ...config, path: "path" };
  110. const server = new Server("path1") as Server & HttpServer;
  111. const server2 = new Server("path2") as Server & HttpServer;
  112. const webSocketServer = new WebSocketServer({ server, realm, config });
  113. expect(webSocketServer.path).toBe("/peerjs");
  114. const webSocketServer2 = new WebSocketServer({
  115. server: server2,
  116. realm,
  117. config: config2,
  118. });
  119. expect(webSocketServer2.path).toBe("path/peerjs");
  120. server.stop();
  121. server2.stop();
  122. });
  123. it(`should check client's params`, async () => {
  124. const realm = new Realm();
  125. const config = { path: "/", key: "testKey", concurrent_limit: 1 };
  126. const fakeURL = "ws://localhost:8080/peerjs";
  127. const getError = async (
  128. url: string,
  129. validError: Errors = Errors.INVALID_WS_PARAMETERS,
  130. ): Promise<boolean> => {
  131. const webSocketServer = createTestServer({ url, realm, config });
  132. const ws = new WebSocket(url);
  133. const errorSent = await checkSequence(ws, [
  134. { type: MessageType.ERROR, error: validError },
  135. ]);
  136. ws.close();
  137. await webSocketServer.destroy?.();
  138. return errorSent;
  139. };
  140. expect(await getError(fakeURL)).toBe(true);
  141. expect(await getError(`${fakeURL}?key=${config.key}`)).toBe(true);
  142. expect(await getError(`${fakeURL}?key=${config.key}&id=1`)).toBe(true);
  143. expect(
  144. await getError(
  145. `${fakeURL}?key=notValidKey&id=userId&token=userToken`,
  146. Errors.INVALID_KEY,
  147. ),
  148. ).toBe(true);
  149. });
  150. it(`should check concurrent limit`, async () => {
  151. const realm = new Realm();
  152. const config = { path: "/", key: "testKey", concurrent_limit: 1 };
  153. const fakeURL = "ws://localhost:8080/peerjs";
  154. const createClient = (id: string): Destroyable<WebSocket> => {
  155. // id in the path ensures that all mock servers listen on different urls
  156. const url = `${fakeURL}${id}?key=${config.key}&id=${id}&token=${id}`;
  157. const webSocketServer = createTestServer({ url, realm, config });
  158. const ws: Destroyable<WebSocket> = new WebSocket(url);
  159. ws.destroy = async (): Promise<void> => {
  160. ws.close();
  161. await wait(10);
  162. webSocketServer.destroy?.();
  163. await wait(10);
  164. ws.destroy = undefined;
  165. };
  166. return ws;
  167. };
  168. const c1 = createClient("1");
  169. expect(await checkOpen(c1)).toBe(true);
  170. const c2 = createClient("2");
  171. expect(
  172. await checkSequence(c2, [
  173. { type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED },
  174. ]),
  175. ).toBe(true);
  176. await c1.destroy?.();
  177. await c2.destroy?.();
  178. await wait(10);
  179. expect(realm.getClientsIds().length).toBe(0);
  180. const c3 = createClient("3");
  181. expect(await checkOpen(c3)).toBe(true);
  182. await c3.destroy?.();
  183. });
  184. });