diff --git a/src/__mocks__/net.ts b/src/__mocks__/net.ts new file mode 100644 index 00000000..ec95d592 --- /dev/null +++ b/src/__mocks__/net.ts @@ -0,0 +1,112 @@ +import { EventEmitter } from 'events' +const sockets: Array = [] +const onNextSocket: Array<(socket: Socket) => void> = [] + +const orgSetImmediate = setImmediate + +export class Socket extends EventEmitter { + public onWrite?: (buff: Buffer, encoding: string) => void + public onConnect?: (port: number, host: string) => void + public onClose?: () => void + + // private _port: number + // private _host: string + private _connected = false + + public destroyed = false + + constructor() { + super() + + const cb = onNextSocket.shift() + if (cb) { + cb(this) + } + + sockets.push(this) + } + + public static mockSockets(): Socket[] { + return sockets + } + public static openSockets(): Socket[] { + return sockets.filter((s) => !s.destroyed) + } + public static mockOnNextSocket(cb: (s: Socket) => void): void { + onNextSocket.push(cb) + } + public static clearMockOnNextSocket(): void { + onNextSocket.splice(0, 99999) + } + // this.emit('connect') + // this.emit('close') + // this.emit('end') + + public connect(port: number, host = 'localhost', cb?: () => void): void { + // this._port = port + // this._host = host + + if (this.onConnect) this.onConnect(port, host) + orgSetImmediate(() => { + if (cb) { + cb() + } + this.setConnected() + }) + } + public write(buf: Buffer, cb?: () => void): void + public write(buf: Buffer, encoding?: BufferEncoding, cb?: () => void): void + public write(buf: Buffer, encodingOrCb?: BufferEncoding | (() => void), cb?: () => void): void { + const DEFAULT_ENCODING = 'utf-8' + cb = typeof encodingOrCb === 'function' ? encodingOrCb : cb + const encoding = typeof encodingOrCb === 'function' ? DEFAULT_ENCODING : encodingOrCb + if (this.onWrite) { + this.onWrite(buf, encoding ?? DEFAULT_ENCODING) + } + if (cb) cb() + } + public end(): void { + this.setEnd() + this.setClosed() + } + + public mockClose(): void { + this.setClosed() + } + public mockData(data: Buffer): void { + this.emit('data', data) + } + + public setNoDelay(_noDelay?: boolean): void { + // noop + } + + public setEncoding(_encoding?: BufferEncoding): void { + // noop + } + + public destroy(): void { + this.destroyed = true + } + + private setConnected() { + if (this._connected !== true) { + this._connected = true + } + this.emit('connect') + } + private setClosed() { + if (this._connected !== false) { + this._connected = false + } + this.destroyed = true + this.emit('close') + if (this.onClose) this.onClose() + } + private setEnd() { + if (this._connected !== false) { + this._connected = false + } + this.emit('end') + } +} diff --git a/src/__tests__/connection.spec.ts b/src/__tests__/connection.spec.ts index 4369e186..90c227b9 100644 --- a/src/__tests__/connection.spec.ts +++ b/src/__tests__/connection.spec.ts @@ -2,32 +2,195 @@ import { Version } from '../enums' import { Connection } from '../connection' import { serializersV21, serializers } from '../serializers' import { deserializers } from '../deserializers' +import { Socket as OrgSocket } from 'net' +import { Socket as MockSocket } from '../__mocks__/net' +import { Commands } from '../commands' + +jest.mock('net') + +const SocketMock = OrgSocket as any as typeof MockSocket describe('connection', () => { - function setupConnectionClass(v = Version.v23x) { - const conn = new Connection('127.0.0.1', 5250, false) - conn.version = v + describe('version handing', () => { + function setupConnectionClass(v = Version.v23x) { + const conn = new Connection('127.0.0.1', 5250, false) + conn.version = v - return conn - } - it('should use 2.1 serializers for 2.1 connection', () => { - const conn = setupConnectionClass(Version.v21x) + return conn + } + it('should use 2.1 serializers for 2.1 connection', () => { + const conn = setupConnectionClass(Version.v21x) - expect(conn['_getVersionedSerializers']()).toBe(serializersV21) - }) - it('should use 2.3 serializers for 2.3 connection', () => { - const conn = setupConnectionClass() + expect(conn['_getVersionedSerializers']()).toBe(serializersV21) + }) + it('should use 2.3 serializers for 2.3 connection', () => { + const conn = setupConnectionClass() - expect(conn['_getVersionedSerializers']()).toBe(serializers) - }) - it('should use 2.1 deserializers for 2.1 connection', () => { - const conn = setupConnectionClass(Version.v21x) + expect(conn['_getVersionedSerializers']()).toBe(serializers) + }) + it('should use 2.1 deserializers for 2.1 connection', () => { + const conn = setupConnectionClass(Version.v21x) - expect(conn['_getVersionedDeserializers']()).toBe(deserializers) + expect(conn['_getVersionedDeserializers']()).toBe(deserializers) + }) + it('should use 2.3 deserializers for 2.3 connection', () => { + const conn = setupConnectionClass() + + expect(conn['_getVersionedDeserializers']()).toBe(deserializers) + }) }) - it('should use 2.3 deserializers for 2.3 connection', () => { - const conn = setupConnectionClass() - expect(conn['_getVersionedDeserializers']()).toBe(deserializers) + describe('receiving', () => { + const onSocketCreate = jest.fn() + const onConnection = jest.fn() + const onSocketClose = jest.fn() + const onSocketWrite = jest.fn() + const onConnectionChanged = jest.fn() + + function setupSocketMock() { + SocketMock.mockOnNextSocket((socket: any) => { + onSocketCreate() + + socket.onConnect = onConnection + socket.onWrite = onSocketWrite + socket.onClose = onSocketClose + }) + } + beforeEach(() => { + setupSocketMock() + }) + afterEach(() => { + const sockets = SocketMock.openSockets() + // Destroy any lingering sockets, to prevent a failing test from affecting other tests: + sockets.forEach((s) => s.destroy()) + + SocketMock.clearMockOnNextSocket() + onSocketCreate.mockClear() + onConnection.mockClear() + onSocketClose.mockClear() + onSocketWrite.mockClear() + onConnectionChanged.mockClear() + + // Just a check to ensure that the unit tests cleaned up the socket after themselves: + // eslint-disable-next-line jest/no-standalone-expect + expect(sockets).toHaveLength(0) + }) + + it('receive whole response', async () => { + const conn = new Connection('127.0.0.1', 5250, true) + try { + expect(conn).toBeTruthy() + + const onConnError = jest.fn() + const onConnData = jest.fn() + conn.on('error', onConnError) + conn.on('data', onConnData) + + const sockets = SocketMock.openSockets() + expect(sockets).toHaveLength(1) + + // Dispatch a command + const sendError = await conn.sendCommand({ + command: Commands.Info, + params: {}, + }) + expect(sendError).toBeFalsy() + expect(onConnError).toHaveBeenCalledTimes(0) + expect(onConnData).toHaveBeenCalledTimes(0) + + // Info was sent + expect(onSocketWrite).toHaveBeenCalledTimes(1) + expect(onSocketWrite).toHaveBeenLastCalledWith('INFO\r\n', 'utf-8') + + // Reply with a single blob + sockets[0].mockData( + Buffer.from( + `201 INFO OK\r\n\n\r\n\r\n` + ) + ) + + // Wait for deserializer to run + await new Promise(process.nextTick.bind(process)) + + expect(onConnError).toHaveBeenCalledTimes(0) + expect(onConnData).toHaveBeenCalledTimes(1) + + // Check result looks good + expect(onConnData).toHaveBeenLastCalledWith({ + command: 'INFO', + data: [ + { + channel: { + test: [''], + }, + }, + ], + message: 'The command has been executed and data is being returned.', + reqId: undefined, + responseCode: 201, + type: 'OK', + }) + } finally { + // Ensure cleaned up + conn.disconnect() + } + }) + + it('receive fragmented response', async () => { + const conn = new Connection('127.0.0.1', 5250, true) + try { + expect(conn).toBeTruthy() + + const onConnError = jest.fn() + const onConnData = jest.fn() + conn.on('error', onConnError) + conn.on('data', onConnData) + + const sockets = SocketMock.openSockets() + expect(sockets).toHaveLength(1) + + // Dispatch a command + const sendError = await conn.sendCommand({ + command: Commands.Info, + params: {}, + }) + expect(sendError).toBeFalsy() + expect(onConnError).toHaveBeenCalledTimes(0) + expect(onConnData).toHaveBeenCalledTimes(0) + + // Info was sent + expect(onSocketWrite).toHaveBeenCalledTimes(1) + expect(onSocketWrite).toHaveBeenLastCalledWith('INFO\r\n', 'utf-8') + + // Reply with a fragmented message + sockets[0].mockData(Buffer.from(`201 INFO OK\r\n\n`)) + sockets[0].mockData(Buffer.from(`\r\n\r\n`)) + + // Wait for deserializer to run + await new Promise(process.nextTick.bind(process)) + + expect(onConnError).toHaveBeenCalledTimes(0) + expect(onConnData).toHaveBeenCalledTimes(1) + + // Check result looks good + expect(onConnData).toHaveBeenLastCalledWith({ + command: 'INFO', + data: [ + { + channel: { + test: [''], + }, + }, + ], + message: 'The command has been executed and data is being returned.', + reqId: undefined, + responseCode: 201, + type: 'OK', + }) + } finally { + // Ensure cleaned up + conn.disconnect() + } + }) }) })