From f8961c96c52b8cd3ec730ded3679e9b61df9d933 Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Thu, 13 Jun 2024 22:42:26 -0500 Subject: [PATCH 1/6] test: (wip) websocket server duplex connectio tests Signed-off-by: Kevin Viglucci --- .../rsocket-websocket-server/jest.config.ts | 20 ++ .../WebsocketDuplexConnection.spec.ts | 321 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 packages/rsocket-websocket-server/jest.config.ts create mode 100644 packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts diff --git a/packages/rsocket-websocket-server/jest.config.ts b/packages/rsocket-websocket-server/jest.config.ts new file mode 100644 index 00000000..062c08d9 --- /dev/null +++ b/packages/rsocket-websocket-server/jest.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "@jest/types"; +import { pathsToModuleNameMapper } from "ts-jest/utils"; +import { compilerOptions } from "../../tsconfig.json"; + +const config: Config.InitialOptions = { + preset: "ts-jest", + testRegex: "(\\/__tests__\\/.*|\\.(test|spec))\\.(ts)$", + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + // This has to match the baseUrl defined in tsconfig.json. + prefix: "/../../", + }), + modulePathIgnorePatterns: [ + "/__tests__/test-utils", + "/__tests__/*.d.ts", + ], + collectCoverage: true, + collectCoverageFrom: ["/src/**/*.ts", "!**/node_modules/**"], +}; + +export default config; diff --git a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts new file mode 100644 index 00000000..89ab70d5 --- /dev/null +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -0,0 +1,321 @@ +import { mock } from "jest-mock-extended"; +import { + Demultiplexer, + Deserializer, + Flags, + Frame, + FrameHandler, + FrameTypes, + Multiplexer, + serializeFrame, + SetupFrame, +} from "rsocket-core"; +import { WebsocketDuplexConnection } from "../WebsocketDuplexConnection"; +// import { MockSocket } from "../__mocks__/ws"; +import { Duplex } from "stream"; + +// const deserializer = mock(); + +describe("WebsocketDuplexConnection", function () { + describe("when closed", () => { + it("removes listeners from the underlying socket event emitter", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // act + connection.close(); + + // assert + expect(socketStub.removeAllListeners).toBeCalledWith(); + }); + + it("cleans up the socket resource when closed without an error", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // act + connection.close(); + + // assert + expect(socketStub.end).toBeCalledWith(); + }); + + it("cleans up the socket resource when closed with an error", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // act + const error = new Error(); + connection.close(error); + + // assert + expect(socketStub.end).toBeCalledWith(); + }); + + it("calls the onClose callback when one is registered", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + const onCloseCallback = jest.fn(); + + // act + connection.onClose(onCloseCallback); + connection.close(); + + // assert + expect(onCloseCallback).toBeCalledTimes(1); + expect(onCloseCallback).toBeCalledWith(); + }); + + it("when closed with an error it calls the onClose callback when one is registered", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + const onCloseCallback = jest.fn(); + const error = new Error(); + + // act + connection.onClose(onCloseCallback); + connection.close(error); + + // assert + expect(onCloseCallback).toBeCalledTimes(1); + expect(onCloseCallback).toBeCalledWith(error); + }); + // + // it("subsequent calls to close result in only a single invocation of onClose", () => { + // const socketStub = mock(); + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const connection = new WebsocketDuplexConnection( + // socketStub, + // deserializer, + // () => multiplexerDemultiplexer + // ); + // const onCloseCallback = jest.fn(); + // const error = new Error(); + // connection.onClose(onCloseCallback); + // connection.close(error); + // connection.close(error); + // + // expect(onCloseCallback).toBeCalledTimes(1); + // expect(onCloseCallback).toBeCalledWith(error); + // }); + // + // it("the onClose callback is called with an error when the socket is closed unexpectedly", () => { + // const socket = new MockSocket() as unknown as WebSocket; + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const connection = new WebsocketDuplexConnection( + // socket, + // deserializer, + // () => multiplexerDemultiplexer + // ); + // const onCloseCallback = jest.fn(); + // + // connection.onClose(onCloseCallback); + // (socket as unknown as MockSocket).mock.close({}); + // + // expect(onCloseCallback).toBeCalledTimes(1); + // expect(onCloseCallback).toHaveBeenCalledWith( + // new Error("WebsocketDuplexConnection: Socket closed unexpectedly.") + // ); + // }); + // + // it("the onClose callback is called with an error when the socket is closed with an error", () => { + // const socket = new MockSocket() as unknown as WebSocket; + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const connection = new WebsocketDuplexConnection( + // socket, + // deserializer, + // () => multiplexerDemultiplexer + // ); + // const onCloseCallback = jest.fn(); + // const expectedError = new Error( + // "WebsocketDuplexConnection: Test error 1" + // ); + // + // connection.onClose(onCloseCallback); + // (socket as unknown as MockSocket).mock.error({ error: expectedError }); + // + // expect(onCloseCallback).toBeCalledTimes(1); + // expect(onCloseCallback).toHaveBeenCalledWith(expectedError); + // }); + }); + + // describe("send()", () => { + // const setupFrame = { + // type: FrameTypes.SETUP, + // dataMimeType: "application/octet-stream", + // metadataMimeType: "application/octet-stream", + // keepAlive: 60000, + // lifetime: 300000, + // metadata: Buffer.from("hello world"), + // data: Buffer.from("hello world"), + // resumeToken: null, + // streamId: 0, + // majorVersion: 1, + // minorVersion: 0, + // flags: Flags.METADATA, + // } as SetupFrame; + // + // it("serializes and writes the given frame to the underlying socket", () => { + // // arrange + // const socketStub = mock(); + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const connection = new WebsocketDuplexConnection( + // socketStub, + // deserializer, + // () => multiplexerDemultiplexer + // ); + // + // // act + // connection.send(setupFrame); + // + // // assert + // expect(socketStub.send).toBeCalledWith(expect.any(Buffer)); + // }); + // + // it("does not write the given frame to the underlying socket when close was previously called", () => { + // // arrange + // const socketStub = mock(); + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const connection = new WebsocketDuplexConnection( + // socketStub, + // deserializer, + // () => multiplexerDemultiplexer + // ); + // + // // act + // connection.close(); + // connection.send(setupFrame); + // + // // assert + // expect(socketStub.send).toBeCalledTimes(0); + // }); + // }); + // + // describe("when receiving data", () => { + // const setupFrame: SetupFrame = { + // type: FrameTypes.SETUP, + // dataMimeType: "application/octet-stream", + // metadataMimeType: "application/octet-stream", + // keepAlive: 60000, + // lifetime: 300000, + // metadata: Buffer.from("hello world"), + // data: Buffer.from("hello world"), + // resumeToken: null, + // streamId: 0, + // majorVersion: 1, + // minorVersion: 0, + // flags: Flags.METADATA, + // }; + // + // describe("when buffer contains a single frame", () => { + // it("deserializes received frames and calls the configured handler", () => { + // // arrange + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const socketStub = new MockSocket() as unknown as WebSocket; + // const connection = new WebsocketDuplexConnection( + // socketStub, + // new Deserializer(), + // () => multiplexerDemultiplexer + // ); + // + // // act + // (socketStub as unknown as MockSocket).mock.message({ + // data: serializeFrame(setupFrame), + // }); + // + // // assert + // expect(multiplexerDemultiplexer.handle).toBeCalledTimes(1); + // + // const [call0] = multiplexerDemultiplexer.handle.mock.calls; + // const [arg0] = call0; + // expect(arg0).toMatchSnapshot(); + // }); + // }); + // + // describe("causes an error", () => { + // it("the connection is closed", () => { + // // arrange + // const multiplexerDemultiplexer = mock< + // Multiplexer & Demultiplexer & FrameHandler + // >(); + // const socketStub = new MockSocket() as unknown as WebSocket; + // const deserializerStub = mock(); + // const connection = new WebsocketDuplexConnection( + // socketStub as unknown as WebSocket, + // deserializerStub, + // () => multiplexerDemultiplexer + // ); + // deserializerStub.deserializeFrame.mockImplementation(() => { + // throw new Error("Mock error"); + // }); + // const onCloseCallback = jest.fn(); + // const data = Buffer.allocUnsafe(0).toString(); + // + // // act + // connection.onClose(onCloseCallback); + // (socketStub as unknown as MockSocket).mock.message({ data }); + // + // // assert + // expect(onCloseCallback).toBeCalledTimes(1); + // expect(onCloseCallback).toBeCalledWith(expect.any(Error)); + // }); + // }); + // }); +}); From 0979ee14880c6d8c6aaea0d4d7e2974108d31da6 Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Fri, 14 Jun 2024 22:11:30 -0500 Subject: [PATCH 2/6] test: additional server WebsocketDuplexConnection tests Signed-off-by: Kevin Viglucci --- .../WebsocketDuplexConnection.spec.ts | 141 ++++++++---------- 1 file changed, 65 insertions(+), 76 deletions(-) diff --git a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts index 89ab70d5..c6462d47 100644 --- a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -1,20 +1,23 @@ import { mock } from "jest-mock-extended"; -import { - Demultiplexer, - Deserializer, - Flags, - Frame, - FrameHandler, - FrameTypes, - Multiplexer, - serializeFrame, - SetupFrame, -} from "rsocket-core"; +import { Demultiplexer, Frame, FrameHandler, Multiplexer } from "rsocket-core"; import { WebsocketDuplexConnection } from "../WebsocketDuplexConnection"; -// import { MockSocket } from "../__mocks__/ws"; import { Duplex } from "stream"; -// const deserializer = mock(); +function makeDuplexStub() { + const listeners = { + close: [], + }; + return mock({ + on(event, cb) { + listeners[event]?.push(cb); + return this; + }, + destroy(error?: Error) { + listeners.close.forEach((cb) => cb(error)); + return this; + }, + }); +} describe("WebsocketDuplexConnection", function () { describe("when closed", () => { @@ -125,69 +128,55 @@ describe("WebsocketDuplexConnection", function () { expect(onCloseCallback).toBeCalledTimes(1); expect(onCloseCallback).toBeCalledWith(error); }); - // - // it("subsequent calls to close result in only a single invocation of onClose", () => { - // const socketStub = mock(); - // const multiplexerDemultiplexer = mock< - // Multiplexer & Demultiplexer & FrameHandler - // >(); - // const connection = new WebsocketDuplexConnection( - // socketStub, - // deserializer, - // () => multiplexerDemultiplexer - // ); - // const onCloseCallback = jest.fn(); - // const error = new Error(); - // connection.onClose(onCloseCallback); - // connection.close(error); - // connection.close(error); - // - // expect(onCloseCallback).toBeCalledTimes(1); - // expect(onCloseCallback).toBeCalledWith(error); - // }); - // - // it("the onClose callback is called with an error when the socket is closed unexpectedly", () => { - // const socket = new MockSocket() as unknown as WebSocket; - // const multiplexerDemultiplexer = mock< - // Multiplexer & Demultiplexer & FrameHandler - // >(); - // const connection = new WebsocketDuplexConnection( - // socket, - // deserializer, - // () => multiplexerDemultiplexer - // ); - // const onCloseCallback = jest.fn(); - // - // connection.onClose(onCloseCallback); - // (socket as unknown as MockSocket).mock.close({}); - // - // expect(onCloseCallback).toBeCalledTimes(1); - // expect(onCloseCallback).toHaveBeenCalledWith( - // new Error("WebsocketDuplexConnection: Socket closed unexpectedly.") - // ); - // }); - // - // it("the onClose callback is called with an error when the socket is closed with an error", () => { - // const socket = new MockSocket() as unknown as WebSocket; - // const multiplexerDemultiplexer = mock< - // Multiplexer & Demultiplexer & FrameHandler - // >(); - // const connection = new WebsocketDuplexConnection( - // socket, - // deserializer, - // () => multiplexerDemultiplexer - // ); - // const onCloseCallback = jest.fn(); - // const expectedError = new Error( - // "WebsocketDuplexConnection: Test error 1" - // ); - // - // connection.onClose(onCloseCallback); - // (socket as unknown as MockSocket).mock.error({ error: expectedError }); - // - // expect(onCloseCallback).toBeCalledTimes(1); - // expect(onCloseCallback).toHaveBeenCalledWith(expectedError); - // }); + + it("subsequent calls to close result in only a single invocation of onClose", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + const onCloseCallback = jest.fn(); + const error = new Error(); + connection.onClose(onCloseCallback); + + // act + connection.close(error); + connection.close(error); + + // assert + expect(onCloseCallback).toBeCalledTimes(1); + expect(onCloseCallback).toBeCalledWith(error); + }); + + it("the onClose callback is called with an error when the socket is destroyed unexpectedly", () => { + // arrange + + const socketStub = makeDuplexStub(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + const onCloseCallback = jest.fn(); + + connection.onClose(onCloseCallback); + (socketStub as unknown as Duplex).destroy(new Error("simulated error")); + + expect(onCloseCallback).toBeCalledTimes(1); + expect(onCloseCallback).toHaveBeenCalledWith( + new Error("WebsocketDuplexConnection: Socket closed unexpectedly.") + ); + }); }); // describe("send()", () => { From 967590c4489756c9226cf59392f902ba118f3cb9 Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Tue, 31 Dec 2024 12:26:15 -0600 Subject: [PATCH 3/6] fix: availability can be read after close Signed-off-by: Kevin Viglucci --- .../src/WebsocketDuplexConnection.ts | 2 +- .../WebsocketDuplexConnection.spec.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts b/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts index e2479516..3ede87ae 100644 --- a/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts +++ b/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts @@ -55,7 +55,7 @@ export class WebsocketDuplexConnection } get availability(): number { - return this.websocketDuplex.destroyed ? 0 : 1; + return this.done ? 0 : 1; } close(error?: Error) { diff --git a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts index c6462d47..927f834d 100644 --- a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -177,6 +177,26 @@ describe("WebsocketDuplexConnection", function () { new Error("WebsocketDuplexConnection: Socket closed unexpectedly.") ); }); + + it("declares availability as 0", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // act + connection.close(); + + // assert + expect(connection.availability).toEqual(0); + }); }); // describe("send()", () => { From 73e4a41d5fdf19f73c1cbb0d380b23a25020d891 Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Tue, 31 Dec 2024 13:00:25 -0600 Subject: [PATCH 4/6] test: add test command to rsocket-websocket-server Signed-off-by: Kevin Viglucci --- .../src/__tests__/WebsocketDuplexConnection.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts index 927f834d..6092b9e1 100644 --- a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -105,7 +105,7 @@ describe("WebsocketDuplexConnection", function () { expect(onCloseCallback).toBeCalledWith(); }); - it("when closed with an error it calls the onClose callback when one is registered", () => { + it("with an error it calls the onClose callback when one is registered", () => { // arrange const socketStub = mock(); const multiplexerDemultiplexer = mock< From 6e9a41d7be74215fda03cc9b33f9383fe08aaaa9 Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Tue, 31 Dec 2024 13:04:50 -0600 Subject: [PATCH 5/6] test: rsocket-websocket-server yarn test runs jest Signed-off-by: Kevin Viglucci --- packages/rsocket-websocket-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rsocket-websocket-server/package.json b/packages/rsocket-websocket-server/package.json index d82ce24c..53ee38c3 100644 --- a/packages/rsocket-websocket-server/package.json +++ b/packages/rsocket-websocket-server/package.json @@ -17,7 +17,7 @@ "clean": "rimraf -rf ./dist", "compile": "tsc -p tsconfig.build.json", "prepublishOnly": "yarn run build", - "test": "echo \"Error: no test specified\" && exit 0" + "test": "jest" }, "dependencies": { "rsocket-core": "^1.0.0-alpha.3", From 0a8f547520b3e282568d7bfe2c7686d9047627dd Mon Sep 17 00:00:00 2001 From: Kevin Viglucci Date: Tue, 31 Dec 2024 13:07:41 -0600 Subject: [PATCH 6/6] test: add test for availability being 1 when done is false Signed-off-by: Kevin Viglucci --- .../WebsocketDuplexConnection.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts index 6092b9e1..5b356811 100644 --- a/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -199,6 +199,25 @@ describe("WebsocketDuplexConnection", function () { }); }); + describe("when open", () => { + it("declares availability as 1", () => { + // arrange + const socketStub = mock(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // assert + expect(connection.availability).toEqual(1); + }); + }); + // describe("send()", () => { // const setupFrame = { // type: FrameTypes.SETUP,