UNPKG

6.8 kBJavaScriptView Raw
1'use strict'
2
3const assert = require('assert')
4const recorder = require('./recorder')
5const {
6 activate,
7 disableNetConnect,
8 enableNetConnect,
9 removeAll: cleanAll,
10} = require('./intercept')
11const { loadDefs, define } = require('./scope')
12
13const { format } = require('util')
14const path = require('path')
15const debug = require('debug')('nock.back')
16
17let _mode = null
18
19let fs
20
21try {
22 fs = require('fs')
23} catch (err) {
24 // do nothing, probably in browser
25}
26
27/**
28 * nock the current function with the fixture given
29 *
30 * @param {string} fixtureName - the name of the fixture, e.x. 'foo.json'
31 * @param {object} options - [optional] extra options for nock with, e.x. `{ assert: true }`
32 * @param {function} nockedFn - [optional] callback function to be executed with the given fixture being loaded;
33 * if defined the function will be called with context `{ scopes: loaded_nocks || [] }`
34 * set as `this` and `nockDone` callback function as first and only parameter;
35 * if not defined a promise resolving to `{nockDone, context}` where `context` is
36 * aforementioned `{ scopes: loaded_nocks || [] }`
37 *
38 * List of options:
39 *
40 * @param {function} before - a preprocessing function, gets called before nock.define
41 * @param {function} after - a postprocessing function, gets called after nock.define
42 * @param {function} afterRecord - a postprocessing function, gets called after recording. Is passed the array
43 * of scopes recorded and should return the array scopes to save to the fixture
44 * @param {function} recorder - custom options to pass to the recorder
45 *
46 */
47function Back(fixtureName, options, nockedFn) {
48 if (!Back.fixtures) {
49 throw new Error(
50 'Back requires nock.back.fixtures to be set\n' +
51 'Ex:\n' +
52 "\trequire(nock).back.fixtures = '/path/to/fixtures/'"
53 )
54 }
55
56 if (typeof fixtureName !== 'string') {
57 throw new Error('Parameter fixtureName must be a string')
58 }
59
60 if (arguments.length === 1) {
61 options = {}
62 } else if (arguments.length === 2) {
63 // If 2nd parameter is a function then `options` has been omitted
64 // otherwise `options` haven't been omitted but `nockedFn` was.
65 if (typeof options === 'function') {
66 nockedFn = options
67 options = {}
68 }
69 }
70
71 _mode.setup()
72
73 const fixture = path.join(Back.fixtures, fixtureName)
74 const context = _mode.start(fixture, options)
75
76 const nockDone = function () {
77 _mode.finish(fixture, options, context)
78 }
79
80 debug('context:', context)
81
82 // If nockedFn is a function then invoke it, otherwise return a promise resolving to nockDone.
83 if (typeof nockedFn === 'function') {
84 nockedFn.call(context, nockDone)
85 } else {
86 return Promise.resolve({ nockDone, context })
87 }
88}
89
90/*******************************************************************************
91 * Modes *
92 *******************************************************************************/
93
94const wild = {
95 setup: function () {
96 cleanAll()
97 recorder.restore()
98 activate()
99 enableNetConnect()
100 },
101
102 start: function () {
103 return load() // don't load anything but get correct context
104 },
105
106 finish: function () {
107 // nothing to do
108 },
109}
110
111const dryrun = {
112 setup: function () {
113 recorder.restore()
114 cleanAll()
115 activate()
116 // We have to explicitly enable net connectivity as by default it's off.
117 enableNetConnect()
118 },
119
120 start: function (fixture, options) {
121 const contexts = load(fixture, options)
122
123 enableNetConnect()
124 return contexts
125 },
126
127 finish: function () {
128 // nothing to do
129 },
130}
131
132const record = {
133 setup: function () {
134 recorder.restore()
135 recorder.clear()
136 cleanAll()
137 activate()
138 disableNetConnect()
139 },
140
141 start: function (fixture, options) {
142 if (!fs) {
143 throw new Error('no fs')
144 }
145 const context = load(fixture, options)
146
147 if (!context.isLoaded) {
148 recorder.record({
149 dont_print: true,
150 output_objects: true,
151 ...options.recorder,
152 })
153
154 context.isRecording = true
155 }
156
157 return context
158 },
159
160 finish: function (fixture, options, context) {
161 if (context.isRecording) {
162 let outputs = recorder.outputs()
163
164 if (typeof options.afterRecord === 'function') {
165 outputs = options.afterRecord(outputs)
166 }
167
168 outputs =
169 typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4)
170 debug('recorder outputs:', outputs)
171
172 fs.mkdirSync(path.dirname(fixture), { recursive: true })
173 fs.writeFileSync(fixture, outputs)
174 }
175 },
176}
177
178const lockdown = {
179 setup: function () {
180 recorder.restore()
181 recorder.clear()
182 cleanAll()
183 activate()
184 disableNetConnect()
185 },
186
187 start: function (fixture, options) {
188 return load(fixture, options)
189 },
190
191 finish: function () {
192 // nothing to do
193 },
194}
195
196function load(fixture, options) {
197 const context = {
198 scopes: [],
199 assertScopesFinished: function () {
200 assertScopes(this.scopes, fixture)
201 },
202 }
203
204 if (fixture && fixtureExists(fixture)) {
205 let scopes = loadDefs(fixture)
206 applyHook(scopes, options.before)
207
208 scopes = define(scopes)
209 applyHook(scopes, options.after)
210
211 context.scopes = scopes
212 context.isLoaded = true
213 }
214
215 return context
216}
217
218function applyHook(scopes, fn) {
219 if (!fn) {
220 return
221 }
222
223 if (typeof fn !== 'function') {
224 throw new Error('processing hooks must be a function')
225 }
226
227 scopes.forEach(fn)
228}
229
230function fixtureExists(fixture) {
231 if (!fs) {
232 throw new Error('no fs')
233 }
234
235 return fs.existsSync(fixture)
236}
237
238function assertScopes(scopes, fixture) {
239 const pending = scopes
240 .filter(scope => !scope.isDone())
241 .map(scope => scope.pendingMocks())
242
243 if (pending.length) {
244 assert.fail(
245 format(
246 '%j was not used, consider removing %s to rerecord fixture',
247 [].concat(...pending),
248 fixture
249 )
250 )
251 }
252}
253
254const Modes = {
255 wild, // all requests go out to the internet, dont replay anything, doesnt record anything
256
257 dryrun, // use recorded nocks, allow http calls, doesnt record anything, useful for writing new tests (default)
258
259 record, // use recorded nocks, record new nocks
260
261 lockdown, // use recorded nocks, disables all http calls even when not nocked, doesnt record
262}
263
264Back.setMode = function (mode) {
265 if (!(mode in Modes)) {
266 throw new Error(`Unknown mode: ${mode}`)
267 }
268
269 Back.currentMode = mode
270 debug('New nock back mode:', Back.currentMode)
271
272 _mode = Modes[mode]
273 _mode.setup()
274}
275
276Back.fixtures = null
277Back.currentMode = null
278
279module.exports = Back