diff --git a/packages/rsocket-websocket-server/jest.config.ts b/packages/rsocket-websocket-server/jest.config.ts new file mode 100644 index 0000000..062c08d --- /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: "<rootDir>/../../", + }), + modulePathIgnorePatterns: [ + "<rootDir>/__tests__/test-utils", + "<rootDir>/__tests__/*.d.ts", + ], + collectCoverage: true, + collectCoverageFrom: ["<rootDir>/src/**/*.ts", "!**/node_modules/**"], +}; + +export default config; diff --git a/packages/rsocket-websocket-server/package.json b/packages/rsocket-websocket-server/package.json index d82ce24..53ee38c 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", diff --git a/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts b/packages/rsocket-websocket-server/src/WebsocketDuplexConnection.ts index e247951..3ede87a 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 new file mode 100644 index 0000000..5b35681 --- /dev/null +++ b/packages/rsocket-websocket-server/src/__tests__/WebsocketDuplexConnection.spec.ts @@ -0,0 +1,349 @@ +import { mock } from "jest-mock-extended"; +import { Demultiplexer, Frame, FrameHandler, Multiplexer } from "rsocket-core"; +import { WebsocketDuplexConnection } from "../WebsocketDuplexConnection"; +import { Duplex } from "stream"; + +function makeDuplexStub() { + const listeners = { + close: [], + }; + return mock<Duplex>({ + 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", () => { + it("removes listeners from the underlying socket event emitter", () => { + // arrange + const socketStub = mock<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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("with an error it calls the onClose callback when one is registered", () => { + // arrange + const socketStub = mock<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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", () => { + // arrange + const socketStub = mock<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + 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<Frame>(); + 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.") + ); + }); + + it("declares availability as 0", () => { + // arrange + const socketStub = mock<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // act + connection.close(); + + // assert + expect(connection.availability).toEqual(0); + }); + }); + + describe("when open", () => { + it("declares availability as 1", () => { + // arrange + const socketStub = mock<Duplex>(); + const multiplexerDemultiplexer = mock< + Multiplexer & Demultiplexer & FrameHandler + >(); + const frame = mock<Frame>(); + const connection = new WebsocketDuplexConnection( + socketStub, + frame, + () => multiplexerDemultiplexer + ); + + // assert + expect(connection.availability).toEqual(1); + }); + }); + + // 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<WebSocket>(); + // 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<WebSocket>(); + // 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<Deserializer>(); + // 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)); + // }); + // }); + // }); +});