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 | const encodedQueryObj = {}
|
115 | for (const key in queryObj) {
|
116 | const formattedPair = common.formatQueryValue(
|
117 | key,
|
118 | queryObj[key],
|
119 | common.percentEncode
|
120 | )
|
121 | encodedQueryObj[formattedPair[0]] = formattedPair[1]
|
122 | }
|
123 |
|
124 | const lines = []
|
125 |
|
126 |
|
127 | lines.push('')
|
128 |
|
129 | const scope = getScope(options)
|
130 | lines.push(`nock('${scope}', {"encodedQueryParams":true})`)
|
131 |
|
132 | const methodName = getMethod(options).toLowerCase()
|
133 | if (requestBody) {
|
134 | lines.push(` .${methodName}('${path}', ${JSON.stringify(requestBody)})`)
|
135 | } else {
|
136 | lines.push(` .${methodName}('${path}')`)
|
137 | }
|
138 |
|
139 | Object.entries(reqheaders || {}).forEach(([fieldName, fieldValue]) => {
|
140 | const safeName = JSON.stringify(fieldName)
|
141 | const safeValue = JSON.stringify(fieldValue)
|
142 | lines.push(` .matchHeader(${safeName}, ${safeValue})`)
|
143 | })
|
144 |
|
145 | if (queryIndex !== -1) {
|
146 | lines.push(` .query(${JSON.stringify(encodedQueryObj)})`)
|
147 | }
|
148 |
|
149 | const statusCode = res.statusCode.toString()
|
150 | const stringifiedResponseBody = JSON.stringify(responseBody)
|
151 | const headers = inspect(res.rawHeaders)
|
152 | lines.push(` .reply(${statusCode}, ${stringifiedResponseBody}, ${headers});`)
|
153 |
|
154 | return lines.join('\n')
|
155 | }
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | let currentRecordingId = 0
|
163 |
|
164 | const defaultRecordOptions = {
|
165 | dont_print: false,
|
166 | enable_reqheaders_recording: false,
|
167 | logging: console.log,
|
168 | output_objects: false,
|
169 | use_separator: true,
|
170 | }
|
171 |
|
172 | function record(recOptions) {
|
173 |
|
174 |
|
175 |
|
176 | if (recordingInProgress) {
|
177 | throw new Error('Nock recording already in progress')
|
178 | }
|
179 |
|
180 | recordingInProgress = true
|
181 |
|
182 |
|
183 | currentRecordingId = currentRecordingId + 1
|
184 | const thisRecordingId = currentRecordingId
|
185 |
|
186 |
|
187 |
|
188 | if (typeof recOptions === 'boolean') {
|
189 | recOptions = { dont_print: recOptions }
|
190 | }
|
191 |
|
192 | recOptions = { ...defaultRecordOptions, ...recOptions }
|
193 |
|
194 | debug('start recording', thisRecordingId, recOptions)
|
195 |
|
196 | const {
|
197 | dont_print: dontPrint,
|
198 | enable_reqheaders_recording: enableReqHeadersRecording,
|
199 | logging,
|
200 | output_objects: outputObjects,
|
201 | use_separator: useSeparator,
|
202 | } = recOptions
|
203 |
|
204 | debug(thisRecordingId, 'restoring overridden requests before new overrides')
|
205 |
|
206 |
|
207 |
|
208 |
|
209 | common.restoreOverriddenRequests()
|
210 |
|
211 | restoreOverriddenClientRequest()
|
212 |
|
213 |
|
214 | common.overrideRequests(function(proto, overriddenRequest, rawArgs) {
|
215 | const { options, callback } = common.normalizeClientRequestArgs(...rawArgs)
|
216 | const bodyChunks = []
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | if (options._recording) {
|
222 | return overriddenRequest(options, callback)
|
223 | }
|
224 | options._recording = true
|
225 |
|
226 | const req = overriddenRequest(options, function(res) {
|
227 | debug(thisRecordingId, 'intercepting', proto, 'request to record')
|
228 |
|
229 |
|
230 | res.once('end', function() {
|
231 | debug(thisRecordingId, proto, 'intercepted request ended')
|
232 |
|
233 | let reqheaders
|
234 |
|
235 | if (enableReqHeadersRecording) {
|
236 |
|
237 |
|
238 | reqheaders = req.getHeaders()
|
239 | common.deleteHeadersField(reqheaders, 'user-agent')
|
240 | }
|
241 |
|
242 | const generateFn = outputObjects
|
243 | ? generateRequestAndResponseObject
|
244 | : generateRequestAndResponse
|
245 | let out = generateFn({
|
246 | req,
|
247 | bodyChunks,
|
248 | options,
|
249 | res,
|
250 | dataChunks,
|
251 | reqheaders,
|
252 | })
|
253 |
|
254 | debug('out:', out)
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | if (thisRecordingId !== currentRecordingId) {
|
265 | debug('skipping recording of an out-of-order request', out)
|
266 | return
|
267 | }
|
268 |
|
269 | outputs.push(out)
|
270 |
|
271 | if (!dontPrint) {
|
272 | if (useSeparator) {
|
273 | if (typeof out !== 'string') {
|
274 | out = JSON.stringify(out, null, 2)
|
275 | }
|
276 | logging(SEPARATOR + out + SEPARATOR)
|
277 | } else {
|
278 | logging(out)
|
279 | }
|
280 | }
|
281 | })
|
282 |
|
283 | let encoding
|
284 |
|
285 |
|
286 | const { setEncoding } = res
|
287 | res.setEncoding = function(newEncoding) {
|
288 | encoding = newEncoding
|
289 | return setEncoding.apply(this, arguments)
|
290 | }
|
291 |
|
292 | const dataChunks = []
|
293 |
|
294 | const origResPush = res.push
|
295 | res.push = function(data) {
|
296 | if (data) {
|
297 | if (encoding) {
|
298 | data = Buffer.from(data, encoding)
|
299 | }
|
300 | dataChunks.push(data)
|
301 | }
|
302 |
|
303 | return origResPush.call(res, data)
|
304 | }
|
305 |
|
306 | if (callback) {
|
307 | callback(res, options, callback)
|
308 | } else {
|
309 | res.resume()
|
310 | }
|
311 |
|
312 | debug('finished setting up intercepting')
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 | if (proto === 'https') {
|
319 | options.proto = 'https'
|
320 | }
|
321 | })
|
322 |
|
323 | const recordChunk = (chunk, encoding) => {
|
324 | debug(thisRecordingId, 'new', proto, 'body chunk')
|
325 | if (!Buffer.isBuffer(chunk)) {
|
326 | chunk = Buffer.from(chunk, encoding)
|
327 | }
|
328 | bodyChunks.push(chunk)
|
329 | }
|
330 |
|
331 | const oldWrite = req.write
|
332 | req.write = function(chunk, encoding) {
|
333 | if (typeof chunk !== 'undefined') {
|
334 | recordChunk(chunk, encoding)
|
335 | oldWrite.apply(req, arguments)
|
336 | } else {
|
337 | throw new Error('Data was undefined.')
|
338 | }
|
339 | }
|
340 |
|
341 |
|
342 |
|
343 |
|
344 | const oldEnd = req.end
|
345 | req.end = function(chunk, encoding, callback) {
|
346 | debug('req.end')
|
347 | if (typeof chunk === 'function') {
|
348 | callback = chunk
|
349 | chunk = null
|
350 | } else if (typeof encoding === 'function') {
|
351 | callback = encoding
|
352 | encoding = null
|
353 | }
|
354 |
|
355 | if (chunk) {
|
356 | recordChunk(chunk, encoding)
|
357 | }
|
358 | oldEnd.call(req, chunk, encoding, callback)
|
359 | }
|
360 |
|
361 | return req
|
362 | })
|
363 | }
|
364 |
|
365 |
|
366 | function restore() {
|
367 | debug(
|
368 | currentRecordingId,
|
369 | 'restoring all the overridden http/https properties'
|
370 | )
|
371 |
|
372 | common.restoreOverriddenRequests()
|
373 | restoreOverriddenClientRequest()
|
374 | recordingInProgress = false
|
375 | }
|
376 |
|
377 | function clear() {
|
378 | outputs = []
|
379 | }
|
380 |
|
381 | module.exports = {
|
382 | record,
|
383 | outputs: () => outputs,
|
384 | restore,
|
385 | clear,
|
386 | }
|