1 | 'use strict'
|
---|
2 |
|
---|
3 | const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
|
---|
4 | const {
|
---|
5 | kDispatches,
|
---|
6 | kDispatchKey,
|
---|
7 | kDefaultHeaders,
|
---|
8 | kDefaultTrailers,
|
---|
9 | kContentLength,
|
---|
10 | kMockDispatch
|
---|
11 | } = require('./mock-symbols')
|
---|
12 | const { InvalidArgumentError } = require('../core/errors')
|
---|
13 | const { buildURL } = require('../core/util')
|
---|
14 |
|
---|
15 | /**
|
---|
16 | * Defines the scope API for an interceptor reply
|
---|
17 | */
|
---|
18 | class MockScope {
|
---|
19 | constructor (mockDispatch) {
|
---|
20 | this[kMockDispatch] = mockDispatch
|
---|
21 | }
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * Delay a reply by a set amount in ms.
|
---|
25 | */
|
---|
26 | delay (waitInMs) {
|
---|
27 | if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
|
---|
28 | throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
|
---|
29 | }
|
---|
30 |
|
---|
31 | this[kMockDispatch].delay = waitInMs
|
---|
32 | return this
|
---|
33 | }
|
---|
34 |
|
---|
35 | /**
|
---|
36 | * For a defined reply, never mark as consumed.
|
---|
37 | */
|
---|
38 | persist () {
|
---|
39 | this[kMockDispatch].persist = true
|
---|
40 | return this
|
---|
41 | }
|
---|
42 |
|
---|
43 | /**
|
---|
44 | * Allow one to define a reply for a set amount of matching requests.
|
---|
45 | */
|
---|
46 | times (repeatTimes) {
|
---|
47 | if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
|
---|
48 | throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
|
---|
49 | }
|
---|
50 |
|
---|
51 | this[kMockDispatch].times = repeatTimes
|
---|
52 | return this
|
---|
53 | }
|
---|
54 | }
|
---|
55 |
|
---|
56 | /**
|
---|
57 | * Defines an interceptor for a Mock
|
---|
58 | */
|
---|
59 | class MockInterceptor {
|
---|
60 | constructor (opts, mockDispatches) {
|
---|
61 | if (typeof opts !== 'object') {
|
---|
62 | throw new InvalidArgumentError('opts must be an object')
|
---|
63 | }
|
---|
64 | if (typeof opts.path === 'undefined') {
|
---|
65 | throw new InvalidArgumentError('opts.path must be defined')
|
---|
66 | }
|
---|
67 | if (typeof opts.method === 'undefined') {
|
---|
68 | opts.method = 'GET'
|
---|
69 | }
|
---|
70 | // See https://github.com/nodejs/undici/issues/1245
|
---|
71 | // As per RFC 3986, clients are not supposed to send URI
|
---|
72 | // fragments to servers when they retrieve a document,
|
---|
73 | if (typeof opts.path === 'string') {
|
---|
74 | if (opts.query) {
|
---|
75 | opts.path = buildURL(opts.path, opts.query)
|
---|
76 | } else {
|
---|
77 | // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
|
---|
78 | const parsedURL = new URL(opts.path, 'data://')
|
---|
79 | opts.path = parsedURL.pathname + parsedURL.search
|
---|
80 | }
|
---|
81 | }
|
---|
82 | if (typeof opts.method === 'string') {
|
---|
83 | opts.method = opts.method.toUpperCase()
|
---|
84 | }
|
---|
85 |
|
---|
86 | this[kDispatchKey] = buildKey(opts)
|
---|
87 | this[kDispatches] = mockDispatches
|
---|
88 | this[kDefaultHeaders] = {}
|
---|
89 | this[kDefaultTrailers] = {}
|
---|
90 | this[kContentLength] = false
|
---|
91 | }
|
---|
92 |
|
---|
93 | createMockScopeDispatchData (statusCode, data, responseOptions = {}) {
|
---|
94 | const responseData = getResponseData(data)
|
---|
95 | const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
|
---|
96 | const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
|
---|
97 | const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
|
---|
98 |
|
---|
99 | return { statusCode, data, headers, trailers }
|
---|
100 | }
|
---|
101 |
|
---|
102 | validateReplyParameters (statusCode, data, responseOptions) {
|
---|
103 | if (typeof statusCode === 'undefined') {
|
---|
104 | throw new InvalidArgumentError('statusCode must be defined')
|
---|
105 | }
|
---|
106 | if (typeof data === 'undefined') {
|
---|
107 | throw new InvalidArgumentError('data must be defined')
|
---|
108 | }
|
---|
109 | if (typeof responseOptions !== 'object') {
|
---|
110 | throw new InvalidArgumentError('responseOptions must be an object')
|
---|
111 | }
|
---|
112 | }
|
---|
113 |
|
---|
114 | /**
|
---|
115 | * Mock an undici request with a defined reply.
|
---|
116 | */
|
---|
117 | reply (replyData) {
|
---|
118 | // Values of reply aren't available right now as they
|
---|
119 | // can only be available when the reply callback is invoked.
|
---|
120 | if (typeof replyData === 'function') {
|
---|
121 | // We'll first wrap the provided callback in another function,
|
---|
122 | // this function will properly resolve the data from the callback
|
---|
123 | // when invoked.
|
---|
124 | const wrappedDefaultsCallback = (opts) => {
|
---|
125 | // Our reply options callback contains the parameter for statusCode, data and options.
|
---|
126 | const resolvedData = replyData(opts)
|
---|
127 |
|
---|
128 | // Check if it is in the right format
|
---|
129 | if (typeof resolvedData !== 'object') {
|
---|
130 | throw new InvalidArgumentError('reply options callback must return an object')
|
---|
131 | }
|
---|
132 |
|
---|
133 | const { statusCode, data = '', responseOptions = {} } = resolvedData
|
---|
134 | this.validateReplyParameters(statusCode, data, responseOptions)
|
---|
135 | // Since the values can be obtained immediately we return them
|
---|
136 | // from this higher order function that will be resolved later.
|
---|
137 | return {
|
---|
138 | ...this.createMockScopeDispatchData(statusCode, data, responseOptions)
|
---|
139 | }
|
---|
140 | }
|
---|
141 |
|
---|
142 | // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
|
---|
143 | const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback)
|
---|
144 | return new MockScope(newMockDispatch)
|
---|
145 | }
|
---|
146 |
|
---|
147 | // We can have either one or three parameters, if we get here,
|
---|
148 | // we should have 1-3 parameters. So we spread the arguments of
|
---|
149 | // this function to obtain the parameters, since replyData will always
|
---|
150 | // just be the statusCode.
|
---|
151 | const [statusCode, data = '', responseOptions = {}] = [...arguments]
|
---|
152 | this.validateReplyParameters(statusCode, data, responseOptions)
|
---|
153 |
|
---|
154 | // Send in-already provided data like usual
|
---|
155 | const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions)
|
---|
156 | const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
|
---|
157 | return new MockScope(newMockDispatch)
|
---|
158 | }
|
---|
159 |
|
---|
160 | /**
|
---|
161 | * Mock an undici request with a defined error.
|
---|
162 | */
|
---|
163 | replyWithError (error) {
|
---|
164 | if (typeof error === 'undefined') {
|
---|
165 | throw new InvalidArgumentError('error must be defined')
|
---|
166 | }
|
---|
167 |
|
---|
168 | const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error })
|
---|
169 | return new MockScope(newMockDispatch)
|
---|
170 | }
|
---|
171 |
|
---|
172 | /**
|
---|
173 | * Set default reply headers on the interceptor for subsequent replies
|
---|
174 | */
|
---|
175 | defaultReplyHeaders (headers) {
|
---|
176 | if (typeof headers === 'undefined') {
|
---|
177 | throw new InvalidArgumentError('headers must be defined')
|
---|
178 | }
|
---|
179 |
|
---|
180 | this[kDefaultHeaders] = headers
|
---|
181 | return this
|
---|
182 | }
|
---|
183 |
|
---|
184 | /**
|
---|
185 | * Set default reply trailers on the interceptor for subsequent replies
|
---|
186 | */
|
---|
187 | defaultReplyTrailers (trailers) {
|
---|
188 | if (typeof trailers === 'undefined') {
|
---|
189 | throw new InvalidArgumentError('trailers must be defined')
|
---|
190 | }
|
---|
191 |
|
---|
192 | this[kDefaultTrailers] = trailers
|
---|
193 | return this
|
---|
194 | }
|
---|
195 |
|
---|
196 | /**
|
---|
197 | * Set reply content length header for replies on the interceptor
|
---|
198 | */
|
---|
199 | replyContentLength () {
|
---|
200 | this[kContentLength] = true
|
---|
201 | return this
|
---|
202 | }
|
---|
203 | }
|
---|
204 |
|
---|
205 | module.exports.MockInterceptor = MockInterceptor
|
---|
206 | module.exports.MockScope = MockScope
|
---|