From 542083ed9eb3d11f6fbce542991183501cdc30b0 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 6 Nov 2022 17:07:42 -0500 Subject: [PATCH 1/2] feat(MockInterceptor): allow async reply callbacks --- lib/mock/mock-utils.js | 26 ++++++++++-- test/jest/issue-1757.test.js | 61 +++++++++++++++++++++++++++++ test/mock-agent.js | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 test/jest/issue-1757.test.js diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 7d5ca5071d9..9dc414d0a35 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -10,6 +10,11 @@ const { } = require('./mock-symbols') const { buildURL, nop } = require('../core/util') const { STATUS_CODES } = require('http') +const { + types: { + isPromise + } +} = require('util') function matchValue (match, value) { if (typeof match === 'string') { @@ -241,14 +246,27 @@ function mockDispatch (opts, handler) { handleReply(this[kDispatches]) } - function handleReply (mockDispatches) { + function handleReply (mockDispatches, _data = data) { // fetch's HeadersList is a 1D string array const optsHeaders = Array.isArray(opts.headers) ? buildHeadersFromArray(opts.headers) : opts.headers - const responseData = getResponseData( - typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data - ) + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + body.then((newData) => handleReply(mockDispatches, newData)) + return + } + + const responseData = getResponseData(body) const responseHeaders = generateKeyValues(headers) const responseTrailers = generateKeyValues(trailers) diff --git a/test/jest/issue-1757.test.js b/test/jest/issue-1757.test.js new file mode 100644 index 00000000000..b6519d9da14 --- /dev/null +++ b/test/jest/issue-1757.test.js @@ -0,0 +1,61 @@ +'use strict' + +const { Dispatcher, setGlobalDispatcher, MockAgent } = require('../..') + +/* global expect, it */ + +class MiniflareDispatcher extends Dispatcher { + constructor (inner, options) { + super(options) + this.inner = inner + } + + dispatch (options, handler) { + return this.inner.dispatch(options, handler) + } + + close (...args) { + return this.inner.close(...args) + } + + destroy (...args) { + return this.inner.destroy(...args) + } +} + +const runIf = (condition) => condition ? it : it.skip +const nodeMajor = Number(process.versions.node.split('.', 1)[0]) + +runIf(nodeMajor >= 16)('https://github.com/nodejs/undici/issues/1757', async () => { + // fetch isn't exported in <16.8 + const { fetch } = require('../..') + + const mockAgent = new MockAgent() + const mockClient = mockAgent.get('http://localhost:3000') + mockAgent.disableNetConnect() + setGlobalDispatcher(new MiniflareDispatcher(mockAgent)) + + mockClient.intercept({ + path: () => true, + method: () => true + }).reply(200, async (opts) => { + if (opts.body?.[Symbol.asyncIterator]) { + const chunks = [] + for await (const chunk of opts.body) { + chunks.push(chunk) + } + + return Buffer.concat(chunks) + } + + return opts.body + }) + + const response = await fetch('http://localhost:3000', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }) + }) + + expect(response.json()).resolves.toMatchObject({ foo: 'bar' }) + expect(response.status).toBe(200) +}) diff --git a/test/mock-agent.js b/test/mock-agent.js index a6d6192bda4..73ba635bf2c 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2495,6 +2495,82 @@ test('MockAgent - headers in mock dispatcher intercept should be case-insensitiv t.end() }) +// https://github.com/nodejs/undici/issues/1757 +test('MockAgent - reply callback can be asynchronous', { skip: nodeMajor < 16 }, async (t) => { + const { fetch } = require('..') + const ReadableStream = globalThis.ReadableStream ?? require('stream/web').ReadableStream + + class MiniflareDispatcher extends Dispatcher { + constructor (inner, options) { + super(options) + this.inner = inner + } + + dispatch (options, handler) { + return this.inner.dispatch(options, handler) + } + + close (...args) { + return this.inner.close(...args) + } + + destroy (...args) { + return this.inner.destroy(...args) + } + } + + const mockAgent = new MockAgent() + const mockClient = mockAgent.get('http://localhost:3000') + mockAgent.disableNetConnect() + setGlobalDispatcher(new MiniflareDispatcher(mockAgent)) + + t.teardown(mockAgent.close.bind(mockAgent)) + + mockClient.intercept({ + path: () => true, + method: () => true + }).reply(200, async (opts) => { + if (opts.body?.[Symbol.asyncIterator]) { + const chunks = [] + for await (const chunk of opts.body) { + chunks.push(chunk) + } + + return Buffer.concat(chunks) + } + + return opts.body + }).persist() + + { + const response = await fetch('http://localhost:3000', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }) + }) + + t.same(await response.json(), { foo: 'bar' }) + } + + { + const response = await fetch('http://localhost:3000', { + method: 'POST', + body: new ReadableStream({ + start (controller) { + controller.enqueue(new TextEncoder().encode('{"foo":')) + + setTimeout(() => { + controller.enqueue(new TextEncoder().encode('"bar"}')) + controller.close() + }, 100) + } + }), + duplex: 'half' + }) + + t.same(await response.json(), { foo: 'bar' }) + } +}) + test('MockAgent - headers should be array of strings', async (t) => { const mockAgent = new MockAgent() mockAgent.disableNetConnect() From 9ec3104882070a2310e8e1f7230931f1bb3f9db6 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 6 Nov 2022 17:20:52 -0500 Subject: [PATCH 2/2] fix: v12 syntax --- test/mock-agent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mock-agent.js b/test/mock-agent.js index 73ba635bf2c..f83f9406be9 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2498,7 +2498,7 @@ test('MockAgent - headers in mock dispatcher intercept should be case-insensitiv // https://github.com/nodejs/undici/issues/1757 test('MockAgent - reply callback can be asynchronous', { skip: nodeMajor < 16 }, async (t) => { const { fetch } = require('..') - const ReadableStream = globalThis.ReadableStream ?? require('stream/web').ReadableStream + const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream class MiniflareDispatcher extends Dispatcher { constructor (inner, options) { @@ -2530,7 +2530,7 @@ test('MockAgent - reply callback can be asynchronous', { skip: nodeMajor < 16 }, path: () => true, method: () => true }).reply(200, async (opts) => { - if (opts.body?.[Symbol.asyncIterator]) { + if (opts.body && opts.body[Symbol.asyncIterator]) { const chunks = [] for await (const chunk of opts.body) { chunks.push(chunk)