1 | 'use strict'
|
2 |
|
3 | const debug = require('debug')('nock.recorder')
|
4 | const querystring = require('querystring')
|
5 | const { inspect } = require('util')
|
6 |
|
7 | const common = require('./common')
|
8 | const { restoreOverriddenClientRequest } = require('./intercept')
|
9 |
|
10 | const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n'
|
11 | let recordingInProgress = false
|
12 | let outputs = []
|
13 |
|
14 | function getScope(options) {
|
15 | const { proto, host, port } = common.normalizeRequestOptions(options)
|
16 | return common.normalizeOrigin(proto, host, port)
|
17 | }
|
18 |
|
19 | function getMethod(options) {
|
20 | return options.method || 'GET'
|
21 | }
|
22 |
|
23 | function getBodyFromChunks(chunks, headers) {
|
24 |
|
25 |
|
26 |
|
27 | if (headers && common.isContentEncoded(headers)) {
|
28 | return {
|
29 | body: chunks.map(chunk => chunk.toString('hex')),
|
30 | }
|
31 | }
|
32 |
|
33 | const mergedBuffer = Buffer.concat(chunks)
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | const isUtf8Representable = common.isUtf8Representable(mergedBuffer)
|
40 | if (isUtf8Representable) {
|
41 | const maybeStringifiedJson = mergedBuffer.toString('utf8')
|
42 | try {
|
43 | return {
|
44 | isUtf8Representable,
|
45 | body: JSON.parse(maybeStringifiedJson),
|
46 | }
|
47 | } catch (err) {
|
48 | return {
|
49 | isUtf8Representable,
|
50 | body: maybeStringifiedJson,
|
51 | }
|
52 | }
|
53 | } else {
|
54 | return {
|
55 | isUtf8Representable,
|
56 | body: mergedBuffer.toString('hex'),
|
57 | }
|
58 | }
|
59 | }
|
60 |
|
61 | function generateRequestAndResponseObject({
|
62 | req,
|
63 | bodyChunks,
|
64 | options,
|
65 | res,
|
66 | dataChunks,
|
67 | reqheaders,
|
68 | }) {
|
69 | const { body, isUtf8Representable } = getBodyFromChunks(
|
70 | dataChunks,
|
71 | res.headers
|
72 | )
|
73 | options.path = req.path
|
74 |
|
75 | return {
|
76 | scope: getScope(options),
|
77 | method: getMethod(options),
|
78 | path: options.path,
|
79 |
|
80 | body: getBodyFromChunks(bodyChunks).body,
|
81 | status: res.statusCode,
|
82 | response: body,
|
83 | rawHeaders: res.rawHeaders,
|
84 | reqheaders: reqheaders || undefined,
|
85 |
|
86 |
|
87 | responseIsBinary: isUtf8Representable === false,
|
88 | }
|
89 | }
|
90 |
|
91 | function generateRequestAndResponse({
|
92 | req,
|
93 | bodyChunks,
|
94 | options,
|
95 | res,
|
96 | dataChunks,
|
97 | reqheaders,
|
98 | }) {
|
99 | const requestBody = getBodyFromChunks(bodyChunks).body
|
100 | const responseBody = getBodyFromChunks(dataChunks, res.headers).body
|
101 |
|
102 |
|
103 | let { path } = options
|
104 | const queryIndex = req.path.indexOf('?')
|
105 | let queryObj = {}
|
106 | if (queryIndex !== -1) {
|
107 |
|
108 | path = path.substring(0, queryIndex)
|
109 |
|
110 | const queryStr = req.path.slice(queryIndex + 1)
|
111 | queryObj = querystring.parse(queryStr)
|
112 | }
|
113 |
|
114 |
|
115 | path = path.replace(/'/g, `\\'`)
|
116 |
|
117 |
|
118 | const encodedQueryObj = {}
|
119 | for (const key in queryObj) {
|
120 | const formattedPair = common.formatQueryValue(
|
121 | key,
|
122 | queryObj[key],
|
123 | common.percentEncode
|
124 | )
|
125 | encodedQueryObj[formattedPair[0]] = formattedPair[1]
|
126 | }
|
127 |
|
128 | const lines = []
|
129 |
|
130 |
|
131 | lines.push('')
|
132 |
|
133 | const scope = getScope(options)
|
134 | lines.push(`nock('${scope}', {"encodedQueryParams":true})`)
|
135 |
|
136 | const methodName = getMethod(options).toLowerCase()
|
137 | if (requestBody) {
|
138 | lines.push(` .${methodName}('${path}', ${JSON.stringify(requestBody)})`)
|
139 | } else {
|
140 | lines.push(` .${methodName}('${path}')`)
|
141 | }
|
142 |
|
143 | Object.entries(reqheaders || {}).forEach(([fieldName, fieldValue]) => {
|
144 | const safeName = JSON.stringify(fieldName)
|
145 | const safeValue = JSON.stringify(fieldValue)
|
146 | lines.push(` .matchHeader(${safeName}, ${safeValue})`)
|
147 | })
|
148 |
|
149 | if (queryIndex !== -1) {
|
150 | lines.push(` .query(${JSON.stringify(encodedQueryObj)})`)
|
151 | }
|
152 |
|
153 | const statusCode = res.statusCode.toString()
|
154 | const stringifiedResponseBody = JSON.stringify(responseBody)
|
155 | const headers = inspect(res.rawHeaders)
|
156 | lines.push(` .reply(${statusCode}, ${stringifiedResponseBody}, ${headers});`)
|
157 |
|
158 | return lines.join('\n')
|
159 | }
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | let currentRecordingId = 0
|
167 |
|
168 | const defaultRecordOptions = {
|
169 | dont_print: false,
|
170 | enable_reqheaders_recording: false,
|
171 | logging: console.log,
|
172 | output_objects: false,
|
173 | use_separator: true,
|
174 | }
|
175 |
|
176 | function record(recOptions) {
|
177 |
|
178 |
|
179 |
|
180 | if (recordingInProgress) {
|
181 | throw new Error('Nock recording already in progress')
|
182 | }
|
183 |
|
184 | recordingInProgress = true
|
185 |
|
186 |
|
187 | currentRecordingId = currentRecordingId + 1
|
188 | const thisRecordingId = currentRecordingId
|
189 |
|
190 |
|
191 |
|
192 | if (typeof recOptions === 'boolean') {
|
193 | recOptions = { dont_print: recOptions }
|
194 | }
|
195 |
|
196 | recOptions = { ...defaultRecordOptions, ...recOptions }
|
197 |
|
198 | debug('start recording', thisRecordingId, recOptions)
|
199 |
|
200 | const {
|
201 | dont_print: dontPrint,
|
202 | enable_reqheaders_recording: enableReqHeadersRecording,
|
203 | logging,
|
204 | output_objects: outputObjects,
|
205 | use_separator: useSeparator,
|
206 | } = recOptions
|
207 |
|
208 | debug(thisRecordingId, 'restoring overridden requests before new overrides')
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | common.restoreOverriddenRequests()
|
214 |
|
215 | restoreOverriddenClientRequest()
|
216 |
|
217 |
|
218 | common.overrideRequests(function (proto, overriddenRequest, rawArgs) {
|
219 | const { options, callback } = common.normalizeClientRequestArgs(...rawArgs)
|
220 | const bodyChunks = []
|
221 |
|
222 |
|
223 |
|
224 |
|
225 | if (options._recording) {
|
226 | return overriddenRequest(options, callback)
|
227 | }
|
228 | options._recording = true
|
229 |
|
230 | const req = overriddenRequest(options, function (res) {
|
231 | debug(thisRecordingId, 'intercepting', proto, 'request to record')
|
232 |
|
233 |
|
234 | res.once('end', function () {
|
235 | debug(thisRecordingId, proto, 'intercepted request ended')
|
236 |
|
237 | let reqheaders
|
238 |
|
239 | if (enableReqHeadersRecording) {
|
240 |
|
241 |
|
242 | reqheaders = req.getHeaders()
|
243 | common.deleteHeadersField(reqheaders, 'user-agent')
|
244 | }
|
245 |
|
246 | const generateFn = outputObjects
|
247 | ? generateRequestAndResponseObject
|
248 | : generateRequestAndResponse
|
249 | let out = generateFn({
|
250 | req,
|
251 | bodyChunks,
|
252 | options,
|
253 | res,
|
254 | dataChunks,
|
255 | reqheaders,
|
256 | })
|
257 |
|
258 | debug('out:', out)
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | if (thisRecordingId !== currentRecordingId) {
|
269 | debug('skipping recording of an out-of-order request', out)
|
270 | return
|
271 | }
|
272 |
|
273 | outputs.push(out)
|
274 |
|
275 | if (!dontPrint) {
|
276 | if (useSeparator) {
|
277 | if (typeof out !== 'string') {
|
278 | out = JSON.stringify(out, null, 2)
|
279 | }
|
280 | logging(SEPARATOR + out + SEPARATOR)
|
281 | } else {
|
282 | logging(out)
|
283 | }
|
284 | }
|
285 | })
|
286 |
|
287 | let encoding
|
288 |
|
289 |
|
290 | const { setEncoding } = res
|
291 | res.setEncoding = function (newEncoding) {
|
292 | encoding = newEncoding
|
293 | return setEncoding.apply(this, arguments)
|
294 | }
|
295 |
|
296 | const dataChunks = []
|
297 |
|
298 | const origResPush = res.push
|
299 | res.push = function (data) {
|
300 | if (data) {
|
301 | if (encoding) {
|
302 | data = Buffer.from(data, encoding)
|
303 | }
|
304 | dataChunks.push(data)
|
305 | }
|
306 |
|
307 | return origResPush.call(res, data)
|
308 | }
|
309 |
|
310 | if (callback) {
|
311 | callback(res, options, callback)
|
312 | } else {
|
313 | res.resume()
|
314 | }
|
315 |
|
316 | debug('finished setting up intercepting')
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | if (proto === 'https') {
|
323 | options.proto = 'https'
|
324 | }
|
325 | })
|
326 |
|
327 | const recordChunk = (chunk, encoding) => {
|
328 | debug(thisRecordingId, 'new', proto, 'body chunk')
|
329 | if (!Buffer.isBuffer(chunk)) {
|
330 | chunk = Buffer.from(chunk, encoding)
|
331 | }
|
332 | bodyChunks.push(chunk)
|
333 | }
|
334 |
|
335 | const oldWrite = req.write
|
336 | req.write = function (chunk, encoding) {
|
337 | if (typeof chunk !== 'undefined') {
|
338 | recordChunk(chunk, encoding)
|
339 | oldWrite.apply(req, arguments)
|
340 | } else {
|
341 | throw new Error('Data was undefined.')
|
342 | }
|
343 | }
|
344 |
|
345 |
|
346 |
|
347 |
|
348 | const oldEnd = req.end
|
349 | req.end = function (chunk, encoding, callback) {
|
350 | debug('req.end')
|
351 | if (typeof chunk === 'function') {
|
352 | callback = chunk
|
353 | chunk = null
|
354 | } else if (typeof encoding === 'function') {
|
355 | callback = encoding
|
356 | encoding = null
|
357 | }
|
358 |
|
359 | if (chunk) {
|
360 | recordChunk(chunk, encoding)
|
361 | }
|
362 | oldEnd.call(req, chunk, encoding, callback)
|
363 | }
|
364 |
|
365 | return req
|
366 | })
|
367 | }
|
368 |
|
369 |
|
370 | function restore() {
|
371 | debug(
|
372 | currentRecordingId,
|
373 | 'restoring all the overridden http/https properties'
|
374 | )
|
375 |
|
376 | common.restoreOverriddenRequests()
|
377 | restoreOverriddenClientRequest()
|
378 | recordingInProgress = false
|
379 | }
|
380 |
|
381 | function clear() {
|
382 | outputs = []
|
383 | }
|
384 |
|
385 | module.exports = {
|
386 | record,
|
387 | outputs: () => outputs,
|
388 | restore,
|
389 | clear,
|
390 | }
|