206 lines
6.5 KiB
JavaScript
206 lines
6.5 KiB
JavaScript
'use strict'
|
|
|
|
const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
|
|
const {
|
|
kDispatches,
|
|
kDispatchKey,
|
|
kDefaultHeaders,
|
|
kDefaultTrailers,
|
|
kContentLength,
|
|
kMockDispatch
|
|
} = require('./mock-symbols')
|
|
const { InvalidArgumentError } = require('../core/errors')
|
|
const { buildURL } = require('../core/util')
|
|
|
|
/**
|
|
* Defines the scope API for an interceptor reply
|
|
*/
|
|
class MockScope {
|
|
constructor (mockDispatch) {
|
|
this[kMockDispatch] = mockDispatch
|
|
}
|
|
|
|
/**
|
|
* Delay a reply by a set amount in ms.
|
|
*/
|
|
delay (waitInMs) {
|
|
if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
|
|
throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
|
|
}
|
|
|
|
this[kMockDispatch].delay = waitInMs
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* For a defined reply, never mark as consumed.
|
|
*/
|
|
persist () {
|
|
this[kMockDispatch].persist = true
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Allow one to define a reply for a set amount of matching requests.
|
|
*/
|
|
times (repeatTimes) {
|
|
if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
|
|
throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
|
|
}
|
|
|
|
this[kMockDispatch].times = repeatTimes
|
|
return this
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Defines an interceptor for a Mock
|
|
*/
|
|
class MockInterceptor {
|
|
constructor (opts, mockDispatches) {
|
|
if (typeof opts !== 'object') {
|
|
throw new InvalidArgumentError('opts must be an object')
|
|
}
|
|
if (typeof opts.path === 'undefined') {
|
|
throw new InvalidArgumentError('opts.path must be defined')
|
|
}
|
|
if (typeof opts.method === 'undefined') {
|
|
opts.method = 'GET'
|
|
}
|
|
// See https://github.com/nodejs/undici/issues/1245
|
|
// As per RFC 3986, clients are not supposed to send URI
|
|
// fragments to servers when they retrieve a document,
|
|
if (typeof opts.path === 'string') {
|
|
if (opts.query) {
|
|
opts.path = buildURL(opts.path, opts.query)
|
|
} else {
|
|
// Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
|
|
const parsedURL = new URL(opts.path, 'data://')
|
|
opts.path = parsedURL.pathname + parsedURL.search
|
|
}
|
|
}
|
|
if (typeof opts.method === 'string') {
|
|
opts.method = opts.method.toUpperCase()
|
|
}
|
|
|
|
this[kDispatchKey] = buildKey(opts)
|
|
this[kDispatches] = mockDispatches
|
|
this[kDefaultHeaders] = {}
|
|
this[kDefaultTrailers] = {}
|
|
this[kContentLength] = false
|
|
}
|
|
|
|
createMockScopeDispatchData (statusCode, data, responseOptions = {}) {
|
|
const responseData = getResponseData(data)
|
|
const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
|
|
const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
|
|
const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
|
|
|
|
return { statusCode, data, headers, trailers }
|
|
}
|
|
|
|
validateReplyParameters (statusCode, data, responseOptions) {
|
|
if (typeof statusCode === 'undefined') {
|
|
throw new InvalidArgumentError('statusCode must be defined')
|
|
}
|
|
if (typeof data === 'undefined') {
|
|
throw new InvalidArgumentError('data must be defined')
|
|
}
|
|
if (typeof responseOptions !== 'object') {
|
|
throw new InvalidArgumentError('responseOptions must be an object')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock an undici request with a defined reply.
|
|
*/
|
|
reply (replyData) {
|
|
// Values of reply aren't available right now as they
|
|
// can only be available when the reply callback is invoked.
|
|
if (typeof replyData === 'function') {
|
|
// We'll first wrap the provided callback in another function,
|
|
// this function will properly resolve the data from the callback
|
|
// when invoked.
|
|
const wrappedDefaultsCallback = (opts) => {
|
|
// Our reply options callback contains the parameter for statusCode, data and options.
|
|
const resolvedData = replyData(opts)
|
|
|
|
// Check if it is in the right format
|
|
if (typeof resolvedData !== 'object') {
|
|
throw new InvalidArgumentError('reply options callback must return an object')
|
|
}
|
|
|
|
const { statusCode, data = '', responseOptions = {} } = resolvedData
|
|
this.validateReplyParameters(statusCode, data, responseOptions)
|
|
// Since the values can be obtained immediately we return them
|
|
// from this higher order function that will be resolved later.
|
|
return {
|
|
...this.createMockScopeDispatchData(statusCode, data, responseOptions)
|
|
}
|
|
}
|
|
|
|
// Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
|
|
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback)
|
|
return new MockScope(newMockDispatch)
|
|
}
|
|
|
|
// We can have either one or three parameters, if we get here,
|
|
// we should have 1-3 parameters. So we spread the arguments of
|
|
// this function to obtain the parameters, since replyData will always
|
|
// just be the statusCode.
|
|
const [statusCode, data = '', responseOptions = {}] = [...arguments]
|
|
this.validateReplyParameters(statusCode, data, responseOptions)
|
|
|
|
// Send in-already provided data like usual
|
|
const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions)
|
|
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
|
|
return new MockScope(newMockDispatch)
|
|
}
|
|
|
|
/**
|
|
* Mock an undici request with a defined error.
|
|
*/
|
|
replyWithError (error) {
|
|
if (typeof error === 'undefined') {
|
|
throw new InvalidArgumentError('error must be defined')
|
|
}
|
|
|
|
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error })
|
|
return new MockScope(newMockDispatch)
|
|
}
|
|
|
|
/**
|
|
* Set default reply headers on the interceptor for subsequent replies
|
|
*/
|
|
defaultReplyHeaders (headers) {
|
|
if (typeof headers === 'undefined') {
|
|
throw new InvalidArgumentError('headers must be defined')
|
|
}
|
|
|
|
this[kDefaultHeaders] = headers
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Set default reply trailers on the interceptor for subsequent replies
|
|
*/
|
|
defaultReplyTrailers (trailers) {
|
|
if (typeof trailers === 'undefined') {
|
|
throw new InvalidArgumentError('trailers must be defined')
|
|
}
|
|
|
|
this[kDefaultTrailers] = trailers
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Set reply content length header for replies on the interceptor
|
|
*/
|
|
replyContentLength () {
|
|
this[kContentLength] = true
|
|
return this
|
|
}
|
|
}
|
|
|
|
module.exports.MockInterceptor = MockInterceptor
|
|
module.exports.MockScope = MockScope
|