Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 148 additions & 19 deletions lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const {
} = require('node:util')
const { InvalidArgumentError } = require('../core/errors')

const requestAborted = Symbol('request aborted')

function matchValue (match, value) {
if (typeof match === 'string') {
return match === value
Expand Down Expand Up @@ -295,13 +297,9 @@ function mockDispatch (opts, handler) {

mockDispatch.timesInvoked++

// Here's where we resolve a callback if a callback is present for the dispatch data.
if (mockDispatch.data.callback) {
mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
}

// Parse mockDispatch data
const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
const { data: response, delay, persist } = mockDispatch
const { error } = response
const { timesInvoked, times } = mockDispatch

// If it's used up and not persistent, mark as consumed
Expand Down Expand Up @@ -346,32 +344,73 @@ function mockDispatch (opts, handler) {
}
}

let replyOpts = opts
const dispatches = this[kDispatches]

// Call onRequestStart to allow the handler to receive the controller
handler.onRequestStart?.(controller, null)
if (aborted) {
return true
}

// Handle the request with a delay if necessary
if (typeof delay === 'number' && delay > 0) {
timer = setTimeout(() => {
timer = null
handleReply(this[kDispatches])
}, delay)
} else {
handleReply(this[kDispatches])
const requestBody = dispatchRequestBody(opts.body, handler, controller, () => aborted)

if (isPromise(requestBody)) {
requestBody.then((body) => {
if (body === requestAborted) {
return
}
if (body !== opts.body) {
replyOpts = { ...opts, body }
}
sendReply()
}, (error) => controller.abort(error))
return true
}

if (requestBody === requestAborted) {
return true
}

if (requestBody !== opts.body) {
replyOpts = { ...opts, body: requestBody }
}

function handleReply (mockDispatches, _data = data) {
sendReply()

function sendReply () {
// Handle the request with a delay if necessary
if (typeof delay === 'number' && delay > 0) {
timer = setTimeout(() => {
timer = null
handleReply(dispatches)
}, delay)
} else {
handleReply(dispatches)
}
}

function handleReply (mockDispatches, _response = response) {
// Don't send response if the request was aborted
if (aborted) {
return
}

if (_response.callback) {
const { callback, ...responseDefaults } = _response
mockDispatch.data = { ...responseDefaults, ...callback(replyOpts) }
return handleReply(mockDispatches, mockDispatch.data)
}

const { statusCode, data, headers, trailers } = _response

// fetch's HeadersList is a 1D string array
const optsHeaders = Array.isArray(opts.headers)
? buildHeadersFromArray(opts.headers)
: opts.headers
const body = typeof _data === 'function'
? _data({ ...opts, headers: optsHeaders })
: _data
const body = typeof data === 'function'
? data({ ...replyOpts, headers: optsHeaders })
: data

// util.types.isPromise is likely needed for jest.
if (isPromise(body)) {
Expand All @@ -380,7 +419,7 @@ function mockDispatch (opts, handler) {
// 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.
return body.then((newData) => handleReply(mockDispatches, newData))
return body.then((newData) => handleReply(mockDispatches, { ..._response, data: newData }))
}

// Check again if aborted after async body resolution
Expand All @@ -405,6 +444,96 @@ function mockDispatch (opts, handler) {
return true
}

function dispatchRequestBody (body, handler, controller, isAborted) {
if (typeof handler.onBodySent !== 'function' && typeof handler.onRequestSent !== 'function') {
return body
}

if (body == null) {
return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
}

if (body && typeof body[Symbol.asyncIterator] === 'function') {
return dispatchAsyncIterableBody(body, handler, controller, isAborted)
}

if (isIterableBody(body)) {
const chunks = []

for (const chunk of body) {
if (isAborted()) {
return requestAborted
}
chunks.push(chunk)
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
return requestAborted
}
}

return callOnRequestSent(handler, controller, isAborted) ? chunks : requestAborted
}

if (isAborted()) {
return requestAborted
}
if (!callOnBodySent(handler, controller, body)) {
return requestAborted
}

return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
}

async function dispatchAsyncIterableBody (body, handler, controller, isAborted) {
const chunks = []

for await (const chunk of body) {
if (isAborted()) {
return requestAborted
}
chunks.push(chunk)
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
return requestAborted
}
}

if (!callOnRequestSent(handler, controller, isAborted)) {
return requestAborted
}

return {
async * [Symbol.asyncIterator] () {
yield * chunks
}
}
}

function callOnBodySent (handler, controller, chunk) {
try {
handler.onBodySent?.(chunk)
return true
} catch (error) {
controller.abort(error)
return false
}
}

function callOnRequestSent (handler, controller, isAborted) {
try {
handler.onRequestSent?.()
return !isAborted()
} catch (error) {
controller.abort(error)
return false
}
}

function isIterableBody (body) {
return typeof body !== 'string' &&
!Buffer.isBuffer(body) &&
!ArrayBuffer.isView(body) &&
typeof body[Symbol.iterator] === 'function'
}

function buildMockDispatch () {
const agent = this[kMockAgent]
const origin = this[kOrigin]
Expand Down
Loading
Loading