From 0ec1edf9dd7d06c81e30842d3a87ac39d75b4224 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 18 Apr 2024 11:07:20 +0200 Subject: [PATCH 1/2] chore: clean up ClientRequest utils --- .../ClientRequest/MockHttpSocket.ts | 8 +- .../utils/cloneIncomingMessage.test.ts | 26 ------- .../utils/cloneIncomingMessage.ts | 74 ------------------- .../utils/createResponse.test.ts | 53 ------------- .../ClientRequest/utils/createResponse.ts | 55 -------------- .../normalizeClientRequestEndArgs.test.ts | 41 ---------- .../utils/normalizeClientRequestEndArgs.ts | 53 ------------- .../normalizeClientRequestWriteArgs.test.ts | 36 --------- .../utils/normalizeClientRequestWriteArgs.ts | 39 ---------- 9 files changed, 4 insertions(+), 381 deletions(-) delete mode 100644 src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts delete mode 100644 src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts delete mode 100644 src/interceptors/ClientRequest/utils/createResponse.test.ts delete mode 100644 src/interceptors/ClientRequest/utils/createResponse.ts delete mode 100644 src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts delete mode 100644 src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts delete mode 100644 src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts delete mode 100644 src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts diff --git a/src/interceptors/ClientRequest/MockHttpSocket.ts b/src/interceptors/ClientRequest/MockHttpSocket.ts index 8bbb0716..35a60d30 100644 --- a/src/interceptors/ClientRequest/MockHttpSocket.ts +++ b/src/interceptors/ClientRequest/MockHttpSocket.ts @@ -9,7 +9,7 @@ import { Readable } from 'node:stream' import { invariant } from 'outvariant' import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor' import { MockSocket } from '../Socket/MockSocket' -import type { NormalizedWriteArgs } from '../Socket/utils/normalizeWriteArgs' +import type { NormalizedSocketWriteArgs } from '../Socket/utils/normalizeSocketWriteArgs' import { isPropertyAccessible } from '../../utils/isPropertyAccessible' import { baseUrlFromConnectionOptions } from '../Socket/utils/baseUrlFromConnectionOptions' import { parseRawHeaders } from '../Socket/utils/parseRawHeaders' @@ -53,7 +53,7 @@ export class MockHttpSocket extends MockSocket { private onRequest: MockHttpSocketRequestCallback private onResponse: MockHttpSocketResponseCallback - private writeBuffer: Array = [] + private writeBuffer: Array = [] private request?: Request private requestParser: HTTPParser<0> private requestStream?: Readable @@ -133,7 +133,7 @@ export class MockHttpSocket extends MockSocket { // the original socket instance (i.e. write request body). // Exhaust the "requestBuffer" in case this Socket // gets reused for different requests. - let writeArgs: NormalizedWriteArgs | undefined + let writeArgs: NormalizedSocketWriteArgs | undefined let headersWritten = false while ((writeArgs = this.writeBuffer.shift())) { @@ -373,7 +373,7 @@ export class MockHttpSocket extends MockSocket { } private flushWriteBuffer(): void { - let args: NormalizedWriteArgs | undefined + let args: NormalizedSocketWriteArgs | undefined while ((args = this.writeBuffer.shift())) { args?.[2]?.() } diff --git a/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts b/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts deleted file mode 100644 index bfd473bc..00000000 --- a/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { it, expect } from 'vitest' -import { Socket } from 'net' -import { IncomingMessage } from 'http' -import { Stream, Readable, EventEmitter } from 'stream' -import { cloneIncomingMessage, IS_CLONE } from './cloneIncomingMessage' - -it('clones a given IncomingMessage', () => { - const message = new IncomingMessage(new Socket()) - message.statusCode = 200 - message.statusMessage = 'OK' - message.headers = { 'x-powered-by': 'msw' } - const clone = cloneIncomingMessage(message) - - // Prototypes must be preserved. - expect(clone).toBeInstanceOf(IncomingMessage) - expect(clone).toBeInstanceOf(EventEmitter) - expect(clone).toBeInstanceOf(Stream) - expect(clone).toBeInstanceOf(Readable) - - expect(clone.statusCode).toEqual(200) - expect(clone.statusMessage).toEqual('OK') - expect(clone.headers).toHaveProperty('x-powered-by', 'msw') - - // Cloned IncomingMessage must be marked respectively. - expect(clone[IS_CLONE]).toEqual(true) -}) diff --git a/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts b/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts deleted file mode 100644 index 35b21acf..00000000 --- a/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { IncomingMessage } from 'http' -import { PassThrough } from 'stream' - -export const IS_CLONE = Symbol('isClone') - -export interface ClonedIncomingMessage extends IncomingMessage { - [IS_CLONE]: boolean -} - -/** - * Clones a given `http.IncomingMessage` instance. - */ -export function cloneIncomingMessage( - message: IncomingMessage -): ClonedIncomingMessage { - const clone = message.pipe(new PassThrough()) - - // Inherit all direct "IncomingMessage" properties. - inheritProperties(message, clone) - - // Deeply inherit the message prototypes (Readable, Stream, EventEmitter, etc.). - const clonedPrototype = Object.create(IncomingMessage.prototype) - getPrototypes(clone).forEach((prototype) => { - inheritProperties(prototype, clonedPrototype) - }) - Object.setPrototypeOf(clone, clonedPrototype) - - Object.defineProperty(clone, IS_CLONE, { - enumerable: true, - value: true, - }) - - return clone as unknown as ClonedIncomingMessage -} - -/** - * Returns a list of all prototypes the given object extends. - */ -function getPrototypes(source: object): object[] { - const prototypes: object[] = [] - let current = source - - while ((current = Object.getPrototypeOf(current))) { - prototypes.push(current) - } - - return prototypes -} - -/** - * Inherits a given target object properties and symbols - * onto the given source object. - * @param source Object which should acquire properties. - * @param target Object to inherit the properties from. - */ -function inheritProperties(source: object, target: object): void { - const properties = [ - ...Object.getOwnPropertyNames(source), - ...Object.getOwnPropertySymbols(source), - ] - - for (const property of properties) { - if (target.hasOwnProperty(property)) { - continue - } - - const descriptor = Object.getOwnPropertyDescriptor(source, property) - if (!descriptor) { - continue - } - - Object.defineProperty(target, property, descriptor) - } -} diff --git a/src/interceptors/ClientRequest/utils/createResponse.test.ts b/src/interceptors/ClientRequest/utils/createResponse.test.ts deleted file mode 100644 index 13bc8cfa..00000000 --- a/src/interceptors/ClientRequest/utils/createResponse.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { it, expect } from 'vitest' -import { Socket } from 'net' -import * as http from 'http' -import { createResponse } from './createResponse' -import { RESPONSE_STATUS_CODES_WITHOUT_BODY } from '../../../utils/responseUtils' - -it('creates a fetch api response from http incoming message', async () => { - const message = new http.IncomingMessage(new Socket()) - message.statusCode = 201 - message.statusMessage = 'Created' - message.headers['content-type'] = 'application/json' - - const response = createResponse(message) - - message.emit('data', Buffer.from('{"firstName":')) - message.emit('data', Buffer.from('"John"}')) - message.emit('end') - - expect(response.status).toBe(201) - expect(response.statusText).toBe('Created') - expect(response.headers.get('content-type')).toBe('application/json') - expect(await response.json()).toEqual({ firstName: 'John' }) -}) - -/** - * @note Ignore 1xx response status code because those cannot - * be used as the init to the "Response" constructor. - */ -const CONSTRUCTABLE_RESPONSE_STATUS_CODES = Array.from( - RESPONSE_STATUS_CODES_WITHOUT_BODY -).filter((status) => status >= 200) - -it.each(CONSTRUCTABLE_RESPONSE_STATUS_CODES)( - 'ignores message body for %i response status', - (responseStatus) => { - const message = new http.IncomingMessage(new Socket()) - message.statusCode = responseStatus - - const response = createResponse(message) - - // These chunks will be ignored: this response - // cannot have body. We don't forward this error to - // the consumer because it's us who converts the - // internal stream to a Fetch API Response instance. - // Consumers will rely on the Response API when constructing - // mocked responses. - message.emit('data', Buffer.from('hello')) - message.emit('end') - - expect(response.status).toBe(responseStatus) - expect(response.body).toBe(null) - } -) diff --git a/src/interceptors/ClientRequest/utils/createResponse.ts b/src/interceptors/ClientRequest/utils/createResponse.ts deleted file mode 100644 index cf55b3c6..00000000 --- a/src/interceptors/ClientRequest/utils/createResponse.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { IncomingHttpHeaders, IncomingMessage } from 'http' -import { isResponseWithoutBody } from '../../../utils/responseUtils' - -/** - * Creates a Fetch API `Response` instance from the given - * `http.IncomingMessage` instance. - */ -export function createResponse(message: IncomingMessage): Response { - const responseBodyOrNull = isResponseWithoutBody(message.statusCode || 200) - ? null - : new ReadableStream({ - start(controller) { - message.on('data', (chunk) => controller.enqueue(chunk)) - message.on('end', () => controller.close()) - - /** - * @todo Should also listen to the "error" on the message - * and forward it to the controller. Otherwise the stream - * will pend indefinitely. - */ - }, - }) - - return new Response(responseBodyOrNull, { - status: message.statusCode, - statusText: message.statusMessage, - headers: createHeadersFromIncomingHttpHeaders(message.headers), - }) -} - -function createHeadersFromIncomingHttpHeaders( - httpHeaders: IncomingHttpHeaders -): Headers { - const headers = new Headers() - - for (const headerName in httpHeaders) { - const headerValues = httpHeaders[headerName] - - if (typeof headerValues === 'undefined') { - continue - } - - if (Array.isArray(headerValues)) { - headerValues.forEach((headerValue) => { - headers.append(headerName, headerValue) - }) - - continue - } - - headers.set(headerName, headerValues) - } - - return headers -} diff --git a/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts b/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts deleted file mode 100644 index 63f0bb56..00000000 --- a/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { it, expect } from 'vitest' -import { normalizeClientRequestEndArgs } from './normalizeClientRequestEndArgs' - -it('returns [null, null, cb] given only the callback', () => { - const callback = () => {} - expect(normalizeClientRequestEndArgs(callback)).toEqual([ - null, - null, - callback, - ]) -}) - -it('returns [chunk, null, null] given only the chunk', () => { - expect(normalizeClientRequestEndArgs('chunk')).toEqual(['chunk', null, null]) -}) - -it('returns [chunk, cb] given the chunk and the callback', () => { - const callback = () => {} - expect(normalizeClientRequestEndArgs('chunk', callback)).toEqual([ - 'chunk', - null, - callback, - ]) -}) - -it('returns [chunk, encoding] given the chunk with the encoding', () => { - expect(normalizeClientRequestEndArgs('chunk', 'utf8')).toEqual([ - 'chunk', - 'utf8', - null, - ]) -}) - -it('returns [chunk, encoding, cb] given all three arguments', () => { - const callback = () => {} - expect(normalizeClientRequestEndArgs('chunk', 'utf8', callback)).toEqual([ - 'chunk', - 'utf8', - callback, - ]) -}) diff --git a/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts b/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts deleted file mode 100644 index 137b15d5..00000000 --- a/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Logger } from '@open-draft/logger' - -const logger = new Logger('utils getUrlByRequestOptions') - -export type ClientRequestEndChunk = string | Buffer -export type ClientRequestEndCallback = () => void - -type HttpRequestEndArgs = - | [] - | [ClientRequestEndCallback] - | [ClientRequestEndChunk, ClientRequestEndCallback?] - | [ClientRequestEndChunk, BufferEncoding, ClientRequestEndCallback?] - -type NormalizedHttpRequestEndParams = [ - ClientRequestEndChunk | null, - BufferEncoding | null, - ClientRequestEndCallback | null -] - -/** - * Normalizes a list of arguments given to the `ClientRequest.end()` - * method to always include `chunk`, `encoding`, and `callback`. - */ -export function normalizeClientRequestEndArgs( - ...args: HttpRequestEndArgs -): NormalizedHttpRequestEndParams { - logger.info('arguments', args) - const normalizedArgs = new Array(3) - .fill(null) - .map((value, index) => args[index] || value) - - normalizedArgs.sort((a, b) => { - // If first element is a function, move it rightwards. - if (typeof a === 'function') { - return 1 - } - - // If second element is a function, move the first leftwards. - if (typeof b === 'function') { - return -1 - } - - // If both elements are strings, preserve their original index. - if (typeof a === 'string' && typeof b === 'string') { - return normalizedArgs.indexOf(a) - normalizedArgs.indexOf(b) - } - - return 0 - }) - - logger.info('normalized args', normalizedArgs) - return normalizedArgs as NormalizedHttpRequestEndParams -} diff --git a/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts b/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts deleted file mode 100644 index 00e7cd3d..00000000 --- a/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { it, expect } from 'vitest' -import { normalizeClientRequestWriteArgs } from './normalizeClientRequestWriteArgs' - -it('returns a triplet of null given no chunk, encoding, or callback', () => { - expect( - normalizeClientRequestWriteArgs([ - // @ts-ignore - undefined, - undefined, - undefined, - ]) - ).toEqual([undefined, undefined, undefined]) -}) - -it('returns [chunk, null, null] given only a chunk', () => { - expect(normalizeClientRequestWriteArgs(['chunk', undefined])).toEqual([ - 'chunk', - undefined, - undefined, - ]) -}) - -it('returns [chunk, encoding] given only chunk and encoding', () => { - expect(normalizeClientRequestWriteArgs(['chunk', 'utf8'])).toEqual([ - 'chunk', - 'utf8', - undefined, - ]) -}) - -it('returns [chunk, encoding, cb] given all three arguments', () => { - const callbackFn = () => {} - expect( - normalizeClientRequestWriteArgs(['chunk', 'utf8', callbackFn]) - ).toEqual(['chunk', 'utf8', callbackFn]) -}) diff --git a/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts b/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts deleted file mode 100644 index 0fee9aaa..00000000 --- a/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Logger } from '@open-draft/logger' - -const logger = new Logger('http normalizeWriteArgs') - -export type ClientRequestWriteCallback = (error?: Error | null) => void -export type ClientRequestWriteArgs = [ - chunk: string | Buffer, - encoding?: BufferEncoding | ClientRequestWriteCallback, - callback?: ClientRequestWriteCallback -] - -export type NormalizedClientRequestWriteArgs = [ - chunk: string | Buffer, - encoding?: BufferEncoding, - callback?: ClientRequestWriteCallback -] - -export function normalizeClientRequestWriteArgs( - args: ClientRequestWriteArgs -): NormalizedClientRequestWriteArgs { - logger.info('normalizing ClientRequest.write arguments...', args) - - const chunk = args[0] - const encoding = - typeof args[1] === 'string' ? (args[1] as BufferEncoding) : undefined - const callback = typeof args[1] === 'function' ? args[1] : args[2] - - const writeArgs: NormalizedClientRequestWriteArgs = [ - chunk, - encoding, - callback, - ] - logger.info( - 'successfully normalized ClientRequest.write arguments:', - writeArgs - ) - - return writeArgs -} From e32680e6620f441b20ee299da3f2e254588bb2db Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 18 Apr 2024 11:08:08 +0200 Subject: [PATCH 2/2] chore: clean up socket write normalization --- src/interceptors/Socket/MockSocket.ts | 12 +++-- .../utils/normalizeSocketWriteArgs.test.ts | 52 +++++++++++++++++++ ...iteArgs.ts => normalizeSocketWriteArgs.ts} | 8 +-- 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts rename src/interceptors/Socket/utils/{normalizeWriteArgs.ts => normalizeSocketWriteArgs.ts} (74%) diff --git a/src/interceptors/Socket/MockSocket.ts b/src/interceptors/Socket/MockSocket.ts index 4aff9f26..412961ed 100644 --- a/src/interceptors/Socket/MockSocket.ts +++ b/src/interceptors/Socket/MockSocket.ts @@ -1,9 +1,9 @@ import net from 'node:net' import { - normalizeWriteArgs, + normalizeSocketWriteArgs, type WriteArgs, type WriteCallback, -} from './utils/normalizeWriteArgs' +} from './utils/normalizeSocketWriteArgs' export interface MockSocketOptions { write: ( @@ -36,13 +36,17 @@ export class MockSocket extends net.Socket { } public write(...args: Array): boolean { - const [chunk, encoding, callback] = normalizeWriteArgs(args as WriteArgs) + const [chunk, encoding, callback] = normalizeSocketWriteArgs( + args as WriteArgs + ) this.options.write(chunk, encoding, callback) return true } public end(...args: Array) { - const [chunk, encoding, callback] = normalizeWriteArgs(args as WriteArgs) + const [chunk, encoding, callback] = normalizeSocketWriteArgs( + args as WriteArgs + ) this.options.write(chunk, encoding, callback) return super.end.apply(this, args as any) diff --git a/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts b/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts new file mode 100644 index 00000000..32f2e1d5 --- /dev/null +++ b/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment node + */ +import { it, expect } from 'vitest' +import { normalizeSocketWriteArgs } from './normalizeSocketWriteArgs' + +it('normalizes .write()', () => { + expect(normalizeSocketWriteArgs([undefined])).toEqual([ + undefined, + undefined, + undefined, + ]) + expect(normalizeSocketWriteArgs([null])).toEqual([null, undefined, undefined]) +}) + +it('normalizes .write(chunk)', () => { + expect(normalizeSocketWriteArgs([Buffer.from('hello')])).toEqual([ + Buffer.from('hello'), + undefined, + undefined, + ]) + expect(normalizeSocketWriteArgs(['hello'])).toEqual([ + 'hello', + undefined, + undefined, + ]) + expect(normalizeSocketWriteArgs([null])).toEqual([null, undefined, undefined]) +}) + +it('normalizes .write(chunk, encoding)', () => { + expect(normalizeSocketWriteArgs([Buffer.from('hello'), 'utf8'])).toEqual([ + Buffer.from('hello'), + 'utf8', + undefined, + ]) +}) + +it('normalizes .write(chunk, callback)', () => { + const callback = () => {} + expect(normalizeSocketWriteArgs([Buffer.from('hello'), callback])).toEqual([ + Buffer.from('hello'), + undefined, + callback, + ]) +}) + +it('normalizes .write(chunk, encoding, callback)', () => { + const callback = () => {} + expect( + normalizeSocketWriteArgs([Buffer.from('hello'), 'utf8', callback]) + ).toEqual([Buffer.from('hello'), 'utf8', callback]) +}) diff --git a/src/interceptors/Socket/utils/normalizeWriteArgs.ts b/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts similarity index 74% rename from src/interceptors/Socket/utils/normalizeWriteArgs.ts rename to src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts index 733060e8..03a3e9c0 100644 --- a/src/interceptors/Socket/utils/normalizeWriteArgs.ts +++ b/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts @@ -4,7 +4,7 @@ export type WriteArgs = | [chunk: unknown, callback?: WriteCallback] | [chunk: unknown, encoding: BufferEncoding, callback?: WriteCallback] -export type NormalizedWriteArgs = [ +export type NormalizedSocketWriteArgs = [ chunk: any, encoding?: BufferEncoding, callback?: WriteCallback, @@ -14,8 +14,10 @@ export type NormalizedWriteArgs = [ * Normalizes the arguments provided to the `Writable.prototype.write()` * and `Writable.prototype.end()`. */ -export function normalizeWriteArgs(args: WriteArgs): NormalizedWriteArgs { - const normalized: NormalizedWriteArgs = [args[0], undefined, undefined] +export function normalizeSocketWriteArgs( + args: WriteArgs +): NormalizedSocketWriteArgs { + const normalized: NormalizedSocketWriteArgs = [args[0], undefined, undefined] if (typeof args[1] === 'string') { normalized[1] = args[1]