@mbtest/mountebank 2.9.2-beta.9050
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/bin/mb +136 -0
- package/package.json +71 -0
- package/releases.json +52 -0
- package/src/cli/api.js +112 -0
- package/src/cli/cli.js +420 -0
- package/src/controllers/configController.js +64 -0
- package/src/controllers/feedController.js +115 -0
- package/src/controllers/homeController.js +58 -0
- package/src/controllers/imposterController.js +328 -0
- package/src/controllers/impostersController.js +215 -0
- package/src/controllers/logsController.js +52 -0
- package/src/models/behaviors.js +553 -0
- package/src/models/behaviorsValidator.js +186 -0
- package/src/models/compatibility.js +133 -0
- package/src/models/dryRunValidator.js +261 -0
- package/src/models/filesystemBackedImpostersRepository.js +908 -0
- package/src/models/http/baseHttpServer.js +207 -0
- package/src/models/http/headersMap.js +87 -0
- package/src/models/http/httpProxy.js +230 -0
- package/src/models/http/httpRequest.js +82 -0
- package/src/models/http/httpServer.js +18 -0
- package/src/models/http/index.js +18 -0
- package/src/models/https/cert/mb-cert.pem +20 -0
- package/src/models/https/cert/mb-csr.pem +16 -0
- package/src/models/https/cert/mb-key.pem +27 -0
- package/src/models/https/httpsServer.js +42 -0
- package/src/models/https/index.js +18 -0
- package/src/models/imposter.js +243 -0
- package/src/models/imposterPrinter.js +120 -0
- package/src/models/impostersRepository.js +49 -0
- package/src/models/inMemoryImpostersRepository.js +418 -0
- package/src/models/jsonpath.js +44 -0
- package/src/models/mbConnection.js +107 -0
- package/src/models/predicates.js +438 -0
- package/src/models/protocols.js +242 -0
- package/src/models/responseResolver.js +398 -0
- package/src/models/smtp/index.js +16 -0
- package/src/models/smtp/smtpRequest.js +60 -0
- package/src/models/smtp/smtpServer.js +109 -0
- package/src/models/tcp/index.js +18 -0
- package/src/models/tcp/tcpProxy.js +110 -0
- package/src/models/tcp/tcpRequest.js +23 -0
- package/src/models/tcp/tcpServer.js +156 -0
- package/src/models/tcp/tcpValidator.js +19 -0
- package/src/models/xpath.js +95 -0
- package/src/mountebank.js +245 -0
- package/src/public/images/arrow_down.png +0 -0
- package/src/public/images/arrow_up.png +0 -0
- package/src/public/images/book.jpg +0 -0
- package/src/public/images/dataflow.png +0 -0
- package/src/public/images/favicon.ico +0 -0
- package/src/public/images/forkme_right_orange_ff7600.png +0 -0
- package/src/public/images/mountebank.png +0 -0
- package/src/public/images/overview.gif +0 -0
- package/src/public/images/quote.png +0 -0
- package/src/public/images/tw-logo.png +0 -0
- package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
- package/src/public/scripts/urlHashHandler.js +31 -0
- package/src/public/stylesheets/application.css +424 -0
- package/src/public/stylesheets/ie.css +14 -0
- package/src/public/stylesheets/imposters.css +121 -0
- package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
- package/src/util/combinators.js +68 -0
- package/src/util/date.js +51 -0
- package/src/util/errors.js +55 -0
- package/src/util/helpers.js +131 -0
- package/src/util/inherit.js +28 -0
- package/src/util/ip.js +54 -0
- package/src/util/logger.js +83 -0
- package/src/util/middleware.js +256 -0
- package/src/util/scopedLogger.js +47 -0
- package/src/views/_footer.ejs +20 -0
- package/src/views/_header.ejs +113 -0
- package/src/views/_imposter.ejs +8 -0
- package/src/views/config.ejs +71 -0
- package/src/views/docs/api/behaviors/copy.ejs +427 -0
- package/src/views/docs/api/behaviors/decorate.ejs +182 -0
- package/src/views/docs/api/behaviors/lookup.ejs +220 -0
- package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
- package/src/views/docs/api/behaviors/wait.ejs +121 -0
- package/src/views/docs/api/behaviors.ejs +141 -0
- package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
- package/src/views/docs/api/contracts/addStub.ejs +10 -0
- package/src/views/docs/api/contracts/config-description.ejs +32 -0
- package/src/views/docs/api/contracts/config.ejs +23 -0
- package/src/views/docs/api/contracts/home-description.ejs +18 -0
- package/src/views/docs/api/contracts/home.ejs +13 -0
- package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
- package/src/views/docs/api/contracts/imposter.ejs +182 -0
- package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
- package/src/views/docs/api/contracts/imposters.ejs +13 -0
- package/src/views/docs/api/contracts/logs-description.ejs +3 -0
- package/src/views/docs/api/contracts/logs.ejs +14 -0
- package/src/views/docs/api/contracts/stub-description.ejs +4 -0
- package/src/views/docs/api/contracts/stub.ejs +7 -0
- package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
- package/src/views/docs/api/contracts/stubs.ejs +11 -0
- package/src/views/docs/api/contracts.ejs +133 -0
- package/src/views/docs/api/errors.ejs +64 -0
- package/src/views/docs/api/fault/connectionReset.ejs +31 -0
- package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
- package/src/views/docs/api/faults.ejs +57 -0
- package/src/views/docs/api/injection.ejs +426 -0
- package/src/views/docs/api/json.ejs +205 -0
- package/src/views/docs/api/jsonpath.ejs +210 -0
- package/src/views/docs/api/mocks.ejs +130 -0
- package/src/views/docs/api/overview.ejs +968 -0
- package/src/views/docs/api/predicates/and.ejs +62 -0
- package/src/views/docs/api/predicates/contains.ejs +64 -0
- package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
- package/src/views/docs/api/predicates/endsWith.ejs +66 -0
- package/src/views/docs/api/predicates/equals.ejs +125 -0
- package/src/views/docs/api/predicates/exists.ejs +118 -0
- package/src/views/docs/api/predicates/inject.ejs +67 -0
- package/src/views/docs/api/predicates/matches.ejs +66 -0
- package/src/views/docs/api/predicates/not.ejs +52 -0
- package/src/views/docs/api/predicates/or.ejs +79 -0
- package/src/views/docs/api/predicates/startsWith.ejs +62 -0
- package/src/views/docs/api/predicates.ejs +382 -0
- package/src/views/docs/api/proxies.ejs +191 -0
- package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
- package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
- package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
- package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
- package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
- package/src/views/docs/api/stubs.ejs +391 -0
- package/src/views/docs/api/xpath.ejs +281 -0
- package/src/views/docs/cli/configFiles.ejs +133 -0
- package/src/views/docs/cli/customFormatters.ejs +53 -0
- package/src/views/docs/cli/help.ejs +6 -0
- package/src/views/docs/cli/replay.ejs +42 -0
- package/src/views/docs/cli/restart.ejs +10 -0
- package/src/views/docs/cli/save.ejs +68 -0
- package/src/views/docs/cli/start.ejs +234 -0
- package/src/views/docs/cli/stop.ejs +32 -0
- package/src/views/docs/commandLine.ejs +93 -0
- package/src/views/docs/communityExtensions.ejs +233 -0
- package/src/views/docs/gettingStarted.ejs +146 -0
- package/src/views/docs/mentalModel.ejs +51 -0
- package/src/views/docs/protocols/custom.ejs +231 -0
- package/src/views/docs/protocols/http.ejs +238 -0
- package/src/views/docs/protocols/https.ejs +246 -0
- package/src/views/docs/protocols/smtp.ejs +142 -0
- package/src/views/docs/protocols/tcp.ejs +431 -0
- package/src/views/docs/security.ejs +38 -0
- package/src/views/faqs.ejs +65 -0
- package/src/views/feed.ejs +33 -0
- package/src/views/imposter.ejs +22 -0
- package/src/views/imposters.ejs +33 -0
- package/src/views/index.ejs +89 -0
- package/src/views/license.ejs +30 -0
- package/src/views/logs.ejs +77 -0
- package/src/views/releases/v1.1.0.ejs +55 -0
- package/src/views/releases/v1.1.36.ejs +84 -0
- package/src/views/releases/v1.1.72.ejs +92 -0
- package/src/views/releases/v1.10.0.ejs +108 -0
- package/src/views/releases/v1.11.0.ejs +109 -0
- package/src/views/releases/v1.12.0.ejs +96 -0
- package/src/views/releases/v1.13.0.ejs +118 -0
- package/src/views/releases/v1.14.0.ejs +107 -0
- package/src/views/releases/v1.14.1.ejs +94 -0
- package/src/views/releases/v1.15.0.ejs +113 -0
- package/src/views/releases/v1.16.0.ejs +104 -0
- package/src/views/releases/v1.2.0.ejs +78 -0
- package/src/views/releases/v1.2.103.ejs +86 -0
- package/src/views/releases/v1.2.122.ejs +86 -0
- package/src/views/releases/v1.2.30.ejs +84 -0
- package/src/views/releases/v1.2.45.ejs +84 -0
- package/src/views/releases/v1.2.56.ejs +79 -0
- package/src/views/releases/v1.3.0.ejs +86 -0
- package/src/views/releases/v1.3.1.ejs +100 -0
- package/src/views/releases/v1.4.0.ejs +96 -0
- package/src/views/releases/v1.4.1.ejs +103 -0
- package/src/views/releases/v1.4.2.ejs +100 -0
- package/src/views/releases/v1.4.3.ejs +113 -0
- package/src/views/releases/v1.5.0.ejs +104 -0
- package/src/views/releases/v1.5.1.ejs +91 -0
- package/src/views/releases/v1.6.0.ejs +109 -0
- package/src/views/releases/v1.7.0.ejs +113 -0
- package/src/views/releases/v1.7.1.ejs +90 -0
- package/src/views/releases/v1.7.2.ejs +96 -0
- package/src/views/releases/v1.8.0.ejs +121 -0
- package/src/views/releases/v1.9.0.ejs +111 -0
- package/src/views/releases/v2.0.0.ejs +159 -0
- package/src/views/releases/v2.1.0.ejs +121 -0
- package/src/views/releases/v2.1.1.ejs +106 -0
- package/src/views/releases/v2.1.2.ejs +84 -0
- package/src/views/releases/v2.2.0.ejs +115 -0
- package/src/views/releases/v2.2.1.ejs +102 -0
- package/src/views/releases/v2.3.0.ejs +121 -0
- package/src/views/releases/v2.3.1.ejs +100 -0
- package/src/views/releases/v2.3.2.ejs +102 -0
- package/src/views/releases/v2.3.3.ejs +97 -0
- package/src/views/releases/v2.4.0.ejs +114 -0
- package/src/views/releases/v2.5.0.ejs +51 -0
- package/src/views/releases/v2.6.0.ejs +35 -0
- package/src/views/releases/v2.7.0.ejs +32 -0
- package/src/views/releases/v2.8.0.ejs +36 -0
- package/src/views/releases/v2.8.1.ejs +7 -0
- package/src/views/releases/v2.8.2.ejs +26 -0
- package/src/views/releases/v2.9.0.ejs +32 -0
- package/src/views/releases/v2.9.1.ejs +10 -0
- package/src/views/releases.ejs +26 -0
- package/src/views/sitemap.ejs +36 -0
- package/src/views/support.ejs +14 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fsExtra = require('fs-extra'),
|
|
4
|
+
prometheus = require('prom-client'),
|
|
5
|
+
properLockFile = require('proper-lockfile'),
|
|
6
|
+
pathModule = require('path'),
|
|
7
|
+
helpers = require('../util/helpers.js'),
|
|
8
|
+
errors = require('../util/errors.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An abstraction for loading imposters from the filesystem
|
|
12
|
+
* The root of the database is provided on the command line as --datadir
|
|
13
|
+
* The file layout looks like this:
|
|
14
|
+
*
|
|
15
|
+
* /{datadir}
|
|
16
|
+
* /3000
|
|
17
|
+
* /imposter.json
|
|
18
|
+
* {
|
|
19
|
+
* "protocol": "http",
|
|
20
|
+
* "port": 3000,
|
|
21
|
+
* "stubs: [{
|
|
22
|
+
* "predicates": [{ "equals": { "path": "/" } }],
|
|
23
|
+
* "meta": {
|
|
24
|
+
* "dir": "stubs/{epoch-pid-counter}"
|
|
25
|
+
* }
|
|
26
|
+
* }]
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* /stubs
|
|
30
|
+
* /{epoch-pid-counter}
|
|
31
|
+
* /meta.json
|
|
32
|
+
* {
|
|
33
|
+
* "responseFiles": ["responses/{epoch-pid-counter}.json"],
|
|
34
|
+
* // An array of indexes into responseFiles which handle repeat behavior
|
|
35
|
+
* "orderWithRepeats": [0],
|
|
36
|
+
* // The next index into orderWithRepeats; incremented with each call to nextResponse()
|
|
37
|
+
* "nextIndex": 0
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* /responses
|
|
41
|
+
* /{epoch-pid-counter}.json
|
|
42
|
+
* {
|
|
43
|
+
* "is": { "body": "Hello, world!" }
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* /matches
|
|
47
|
+
* /{epoch-pid-counter}.json
|
|
48
|
+
* { ... }
|
|
49
|
+
*
|
|
50
|
+
* /requests
|
|
51
|
+
* /{epoch-pid-counter}.json
|
|
52
|
+
* { ... }
|
|
53
|
+
*
|
|
54
|
+
* This structure is designed to minimize the amount of file locking and to maximize parallelism and throughput.
|
|
55
|
+
*
|
|
56
|
+
* The imposters.json file needs to be locked during imposter-level activities (e.g. adding a stub).
|
|
57
|
+
* Readers do not lock; they just get the data at the time they read it. Since this file doesn't
|
|
58
|
+
* contain transient state, there's no harm from a stale read. Writes (which happen for
|
|
59
|
+
* stub changing operations, either for proxy response recording or stub API calls) grab a file lock
|
|
60
|
+
* for both the read and the write. Writes to this file should be infrequent, except perhaps during
|
|
61
|
+
* proxy recording. Newly added stubs may change the index of existing stubs in the stubs array, but
|
|
62
|
+
* will never change the stub meta.dir, so it is always safe to hang on to it for subsequent operations.
|
|
63
|
+
*
|
|
64
|
+
* The stub meta.json needs to be locked to add responses or trigger the next response, but is
|
|
65
|
+
* separated from the imposter.json so we can have responses from multiple stubs in parallel with no
|
|
66
|
+
* lock conflict. Again, readers (e.g. to generate imposter JSON) do not need a lock because the responseFiles
|
|
67
|
+
* array is mostly read-only, and even when it's not (adding responses during proxyAlways recording), there's
|
|
68
|
+
* no harm from a stale read. Writers (usually generating the next response for a stub) grab a lock for the
|
|
69
|
+
* read and the write. This should be the most common write across files, which is why the meta.json file
|
|
70
|
+
* is small.
|
|
71
|
+
*
|
|
72
|
+
* In both cases where a file needs to be locked, an exponential backoff retry strategy is used. Inconsistent
|
|
73
|
+
* reads of partially written files (which can happen by default with the system calls - fs.writeFile is not
|
|
74
|
+
* atomic) are avoided by writing first to a temp file (during which time reads can happen to the original file)
|
|
75
|
+
* and then renaming to the original file.
|
|
76
|
+
*
|
|
77
|
+
* New directories and filenames use a timestamp-based filename to allow creating them without synchronizing
|
|
78
|
+
* with a read. Since multiple files (esp. requests) can be added during the same millisecond, a pid and counter
|
|
79
|
+
* is tacked on to the filename to improve uniqueness. It doesn't provide the * ironclad guarantee a GUID
|
|
80
|
+
* does -- two different processes on two different machines could in theory have the same pid and create files
|
|
81
|
+
* during the same timestamp with the same counter, but the odds of that happening * are so small that it's not
|
|
82
|
+
* worth giving up the easy time-based sortability based on filenames alone.
|
|
83
|
+
*
|
|
84
|
+
* Keeping all imposter information under a directory (instead of having metadata outside the directory)
|
|
85
|
+
* allows us to remove the imposter by simply removing the directory.
|
|
86
|
+
*
|
|
87
|
+
* There are some extra checks on filesystem operations (atomicWriteFile) due to antivirus software, solar flares,
|
|
88
|
+
* gremlins, etc. graceful-fs solves some of these, but apparently not all.
|
|
89
|
+
*
|
|
90
|
+
* @module
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
const
|
|
94
|
+
metrics = {
|
|
95
|
+
lockAcquireDuration: new prometheus.Histogram({
|
|
96
|
+
name: 'mb_lock_acquire_duration_seconds',
|
|
97
|
+
help: 'Time it takes to acquire a file lock',
|
|
98
|
+
buckets: [0.1, 0.2, 0.5, 1, 3, 5, 10, 30],
|
|
99
|
+
labelNames: ['caller']
|
|
100
|
+
}),
|
|
101
|
+
lockHoldDuration: new prometheus.Histogram({
|
|
102
|
+
name: 'mb_lock_hold_duration_seconds',
|
|
103
|
+
help: 'Time a file lock is held',
|
|
104
|
+
buckets: [0.1, 0.2, 0.5, 1, 2],
|
|
105
|
+
labelNames: ['caller']
|
|
106
|
+
}),
|
|
107
|
+
lockErrors: new prometheus.Counter({
|
|
108
|
+
name: 'mb_lock_errors_total',
|
|
109
|
+
help: 'Number of lock errors',
|
|
110
|
+
labelNames: ['caller', 'code']
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates the repository
|
|
116
|
+
* @param {Object} config - The database configuration
|
|
117
|
+
* @param {String} config.datadir - The database directory
|
|
118
|
+
* @param {Object} logger - The logger
|
|
119
|
+
* @returns {Object}
|
|
120
|
+
*/
|
|
121
|
+
function create (config, logger) {
|
|
122
|
+
const imposterFns = {};
|
|
123
|
+
let counter = 0,
|
|
124
|
+
locks = 0;
|
|
125
|
+
|
|
126
|
+
async function ensureDir (filepath) {
|
|
127
|
+
const dir = pathModule.dirname(filepath);
|
|
128
|
+
|
|
129
|
+
await fsExtra.ensureDir(dir);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function ensureFile (filepath) {
|
|
133
|
+
fsExtra.close(await fsExtra.open(filepath, 'as'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function writeFile (filepath, obj) {
|
|
137
|
+
await ensureDir(filepath);
|
|
138
|
+
await ensureFile(filepath);
|
|
139
|
+
await fsExtra.writeFile(filepath, JSON.stringify(obj, null, 2), {
|
|
140
|
+
flag: 'rs+'
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tryParse (maybeJSON, filepath) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(maybeJSON);
|
|
147
|
+
}
|
|
148
|
+
catch (parseErr) {
|
|
149
|
+
logger.error(`Corrupted database: invalid JSON for ${filepath}`);
|
|
150
|
+
throw errors.DatabaseError(`invalid JSON in ${filepath}`, { details: parseErr.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readFile (filepath, defaultContents) {
|
|
155
|
+
try {
|
|
156
|
+
const data = await fsExtra.readFile(filepath, 'utf8');
|
|
157
|
+
return tryParse(data, filepath);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
if (err.code === 'ENOENT') {
|
|
161
|
+
if (typeof defaultContents === 'undefined') {
|
|
162
|
+
logger.error(`Corrupted database: missing file ${filepath}`);
|
|
163
|
+
throw errors.DatabaseError('file not found', { details: err.message });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
return defaultContents;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function delay (duration) {
|
|
176
|
+
return new Promise(resolve => {
|
|
177
|
+
setTimeout(resolve, duration);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function atomicWriteFile (filepath, obj, attempts = 1) {
|
|
182
|
+
const tmpfile = filepath + '.tmp';
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await writeFile(tmpfile, obj);
|
|
186
|
+
await fsExtra.rename(tmpfile, filepath);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err.code === 'ENOENT' && attempts < 15) {
|
|
190
|
+
logger.debug(`Attempt ${attempts} failed with ENOENT error on atomic write of ${filepath}. Retrying...`);
|
|
191
|
+
await delay(10);
|
|
192
|
+
await atomicWriteFile(filepath, obj, attempts + 1);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function lockFile (filepath) {
|
|
201
|
+
const options = {
|
|
202
|
+
realpath: false,
|
|
203
|
+
retries: {
|
|
204
|
+
retries: 20,
|
|
205
|
+
minTimeout: 10,
|
|
206
|
+
maxTimeout: 5000,
|
|
207
|
+
randomize: true,
|
|
208
|
+
factor: 1.5
|
|
209
|
+
},
|
|
210
|
+
stale: 30000
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// with realpath = false, the file doesn't have to exist, but the directory does
|
|
214
|
+
await ensureDir(filepath);
|
|
215
|
+
return properLockFile.lock(filepath, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readAndWriteFile (filepath, caller, transformer, defaultContents) {
|
|
219
|
+
const currentLockId = locks,
|
|
220
|
+
observeLockAcquireDuration = metrics.lockAcquireDuration.startTimer({ caller });
|
|
221
|
+
|
|
222
|
+
locks += 1;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const release = await lockFile(filepath),
|
|
226
|
+
acquireLockSeconds = observeLockAcquireDuration(),
|
|
227
|
+
observeLockHoldDuration = metrics.lockHoldDuration.startTimer({ caller });
|
|
228
|
+
|
|
229
|
+
logger.debug(`Acquired file lock on ${filepath} for ${caller}-${currentLockId} after ${acquireLockSeconds}s`);
|
|
230
|
+
|
|
231
|
+
const original = await readFile(filepath, defaultContents),
|
|
232
|
+
transformed = await transformer(original);
|
|
233
|
+
|
|
234
|
+
await atomicWriteFile(filepath, transformed);
|
|
235
|
+
await release();
|
|
236
|
+
|
|
237
|
+
const lockHeld = observeLockHoldDuration();
|
|
238
|
+
logger.debug(`Released file lock on ${filepath} for ${caller}-${currentLockId} after ${lockHeld}s`);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
// Ignore lock already released errors
|
|
242
|
+
if (err.code !== 'ERELEASED') {
|
|
243
|
+
metrics.lockErrors.inc({ caller, code: err.code });
|
|
244
|
+
properLockFile.unlock(filepath, { realpath: false }).catch(() => { /* ignore */ });
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function remove (path) {
|
|
251
|
+
await fsExtra.remove(path);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function filenameFor (timestamp) {
|
|
255
|
+
const epoch = timestamp.valueOf();
|
|
256
|
+
counter += 1;
|
|
257
|
+
return `${epoch}-${process.pid}-${counter}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function partsFrom (filename) {
|
|
261
|
+
// format {epoch}-{pid}-{counter}
|
|
262
|
+
const pattern = /^(\d+)-(\d+)-(\d+)\.json$/,
|
|
263
|
+
parts = pattern.exec(filename);
|
|
264
|
+
return {
|
|
265
|
+
epoch: Number(parts[1]),
|
|
266
|
+
pid: Number(parts[2]),
|
|
267
|
+
counter: Number(parts[3])
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function timeSorter (first, second) {
|
|
272
|
+
// format {epoch}-{pid}-{counter}
|
|
273
|
+
// sort by epoch first, then pid, then counter to guarantee determinism for
|
|
274
|
+
// files added during the same millisecond.
|
|
275
|
+
const firstParts = partsFrom(first),
|
|
276
|
+
secondParts = partsFrom(second);
|
|
277
|
+
let result = firstParts.epoch - secondParts.epoch;
|
|
278
|
+
if (result === 0) {
|
|
279
|
+
result = firstParts.pid - secondParts.pid;
|
|
280
|
+
}
|
|
281
|
+
if (result === 0) {
|
|
282
|
+
result = firstParts.counter - secondParts.counter;
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readdir (path) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
fsExtra.readdir(path, (err, files) => {
|
|
290
|
+
if (err && err.code === 'ENOENT') {
|
|
291
|
+
// Nothing saved yet
|
|
292
|
+
resolve([]);
|
|
293
|
+
}
|
|
294
|
+
else if (err) {
|
|
295
|
+
reject(err);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
resolve(files);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function loadAllInDir (path) {
|
|
305
|
+
const files = await readdir(path),
|
|
306
|
+
reads = files
|
|
307
|
+
.filter(file => file.indexOf('.json') > 0)
|
|
308
|
+
.sort(timeSorter)
|
|
309
|
+
.map(file => readFile(`${path}/${file}`));
|
|
310
|
+
|
|
311
|
+
return Promise.all(reads);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function repeatsFor (response) {
|
|
315
|
+
return response.repeat || 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function saveStubMetaAndResponses (stub, baseDir) {
|
|
319
|
+
const stubDefinition = {
|
|
320
|
+
meta: { dir: `stubs/${filenameFor(new Date())}` }
|
|
321
|
+
},
|
|
322
|
+
meta = {
|
|
323
|
+
responseFiles: [],
|
|
324
|
+
orderWithRepeats: [],
|
|
325
|
+
nextIndex: 0
|
|
326
|
+
},
|
|
327
|
+
responses = stub.responses || [],
|
|
328
|
+
writes = [];
|
|
329
|
+
|
|
330
|
+
if (stub.predicates) {
|
|
331
|
+
stubDefinition.predicates = stub.predicates;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < responses.length; i += 1) {
|
|
335
|
+
const responseFile = `responses/${filenameFor(new Date())}.json`;
|
|
336
|
+
meta.responseFiles.push(responseFile);
|
|
337
|
+
|
|
338
|
+
for (let repeats = 0; repeats < repeatsFor(responses[i]); repeats += 1) {
|
|
339
|
+
meta.orderWithRepeats.push(i);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
writes.push(writeFile(`${baseDir}/${stubDefinition.meta.dir}/${responseFile}`, responses[i]));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
writes.push(writeFile(`${baseDir}/${stubDefinition.meta.dir}/meta.json`, meta));
|
|
346
|
+
await Promise.all(writes);
|
|
347
|
+
return stubDefinition;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function stubRepository (baseDir) {
|
|
351
|
+
const imposterFile = `${baseDir}/imposter.json`;
|
|
352
|
+
|
|
353
|
+
function metaPath (stubDir) {
|
|
354
|
+
return `${baseDir}/${stubDir}/meta.json`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function responsePath (stubDir, responseFile) {
|
|
358
|
+
return `${baseDir}/${stubDir}/${responseFile}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function requestPath (request) {
|
|
362
|
+
return `${baseDir}/requests/${filenameFor(Date.parse(request.timestamp))}.json`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function matchPath (stubDir, match) {
|
|
366
|
+
return `${baseDir}/${stubDir}/matches/${filenameFor(Date.parse(match.timestamp))}.json`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readHeader () {
|
|
370
|
+
return readFile(imposterFile, { stubs: [] });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function readAndWriteHeader (caller, transformer) {
|
|
374
|
+
return readAndWriteFile(imposterFile, caller, transformer, { stubs: [] });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function wrap (stub) {
|
|
378
|
+
const cloned = helpers.clone(stub || {}),
|
|
379
|
+
stubDir = stub ? stub.meta.dir : '';
|
|
380
|
+
|
|
381
|
+
if (typeof stub === 'undefined') {
|
|
382
|
+
return {
|
|
383
|
+
addResponse: () => Promise.resolve(),
|
|
384
|
+
nextResponse: () => Promise.resolve({
|
|
385
|
+
is: {},
|
|
386
|
+
stubIndex: () => Promise.resolve(0)
|
|
387
|
+
}),
|
|
388
|
+
recordMatch: () => Promise.resolve()
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
delete cloned.meta;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Adds a response to the stub
|
|
396
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
397
|
+
* @param {Object} response - the new response
|
|
398
|
+
* @returns {Object} - the promise
|
|
399
|
+
*/
|
|
400
|
+
cloned.addResponse = async response => {
|
|
401
|
+
let responseFile;
|
|
402
|
+
await readAndWriteFile(metaPath(stubDir), 'addResponse', async meta => {
|
|
403
|
+
const responseIndex = meta.responseFiles.length;
|
|
404
|
+
responseFile = `responses/${filenameFor(new Date())}.json`;
|
|
405
|
+
|
|
406
|
+
meta.responseFiles.push(responseFile);
|
|
407
|
+
for (let repeats = 0; repeats < repeatsFor(response); repeats += 1) {
|
|
408
|
+
meta.orderWithRepeats.push(responseIndex);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await writeFile(responsePath(stubDir, responseFile), response);
|
|
412
|
+
return meta;
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
async function stubIndex () {
|
|
417
|
+
const header = await readHeader();
|
|
418
|
+
for (let i = 0; i < header.stubs.length; i += 1) {
|
|
419
|
+
if (header.stubs[i].meta.dir === stubDir) {
|
|
420
|
+
return i;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createResponse (responseConfig) {
|
|
427
|
+
const result = helpers.clone(responseConfig || { is: {} });
|
|
428
|
+
result.stubIndex = stubIndex;
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Returns the next response for the stub, taking into consideration repeat behavior and cycling back the beginning
|
|
434
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
435
|
+
* @returns {Object} - the promise
|
|
436
|
+
*/
|
|
437
|
+
cloned.nextResponse = async () => {
|
|
438
|
+
let responseFile;
|
|
439
|
+
await readAndWriteFile(metaPath(stubDir), 'nextResponse', async meta => {
|
|
440
|
+
const maxIndex = meta.orderWithRepeats.length,
|
|
441
|
+
responseIndex = meta.orderWithRepeats[meta.nextIndex % maxIndex];
|
|
442
|
+
|
|
443
|
+
responseFile = meta.responseFiles[responseIndex];
|
|
444
|
+
|
|
445
|
+
meta.nextIndex = (meta.nextIndex + 1) % maxIndex;
|
|
446
|
+
return meta;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// No need to read the response file while the lock is held
|
|
450
|
+
const responseConfig = await readFile(responsePath(stubDir, responseFile));
|
|
451
|
+
return createResponse(responseConfig);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Records a match for debugging purposes
|
|
456
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
457
|
+
* @param {Object} request - the request
|
|
458
|
+
* @param {Object} response - the response
|
|
459
|
+
* @param {Object} responseConfig - the config that generated the response
|
|
460
|
+
* @param {Number} processingTime - the time to match the predicate and generate the full response
|
|
461
|
+
* @returns {Object} - the promise
|
|
462
|
+
*/
|
|
463
|
+
cloned.recordMatch = async (request, response, responseConfig, processingTime) => {
|
|
464
|
+
const match = {
|
|
465
|
+
timestamp: new Date().toJSON(),
|
|
466
|
+
request,
|
|
467
|
+
response,
|
|
468
|
+
responseConfig,
|
|
469
|
+
processingTime
|
|
470
|
+
};
|
|
471
|
+
await writeFile(matchPath(stubDir, match), match);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return cloned;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Returns the number of stubs for the imposter
|
|
479
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
480
|
+
* @returns {Object} - the promise
|
|
481
|
+
*/
|
|
482
|
+
async function count () {
|
|
483
|
+
const imposter = await readHeader();
|
|
484
|
+
return imposter.stubs.length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Returns the first stub whose predicates matches the filter
|
|
489
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
490
|
+
* @param {Function} filter - the filter function
|
|
491
|
+
* @param {Number} startIndex - the index to to start searching
|
|
492
|
+
* @returns {Object} - the promise
|
|
493
|
+
*/
|
|
494
|
+
async function first (filter, startIndex = 0) {
|
|
495
|
+
const header = await readHeader();
|
|
496
|
+
|
|
497
|
+
for (let i = startIndex; i < header.stubs.length; i += 1) {
|
|
498
|
+
if (filter(header.stubs[i].predicates || [])) {
|
|
499
|
+
return { success: true, stub: wrap(header.stubs[i]) };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { success: false, stub: wrap() };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Adds a new stub to imposter
|
|
507
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
508
|
+
* @param {Object} stub - the stub to add
|
|
509
|
+
* @returns {Object} - the promise
|
|
510
|
+
*/
|
|
511
|
+
async function add (stub) { // eslint-disable-line no-shadow
|
|
512
|
+
const stubDefinition = await saveStubMetaAndResponses(stub, baseDir);
|
|
513
|
+
|
|
514
|
+
await readAndWriteHeader('addStub', async header => {
|
|
515
|
+
header.stubs.push(stubDefinition);
|
|
516
|
+
return header;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Inserts a new stub at the given index
|
|
522
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
523
|
+
* @param {Object} stub - the stub to add
|
|
524
|
+
* @param {Number} index - the index to insert the new stub at
|
|
525
|
+
* @returns {Object} - the promise
|
|
526
|
+
*/
|
|
527
|
+
async function insertAtIndex (stub, index) {
|
|
528
|
+
const stubDefinition = await saveStubMetaAndResponses(stub, baseDir);
|
|
529
|
+
|
|
530
|
+
await readAndWriteHeader('insertStubAtIndex', async header => {
|
|
531
|
+
header.stubs.splice(index, 0, stubDefinition);
|
|
532
|
+
return header;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Deletes the stub at the given index
|
|
538
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
539
|
+
* @param {Number} index - the index of the stub to delete
|
|
540
|
+
* @returns {Object} - the promise
|
|
541
|
+
*/
|
|
542
|
+
async function deleteAtIndex (index) {
|
|
543
|
+
let stubDir;
|
|
544
|
+
|
|
545
|
+
await readAndWriteHeader('deleteStubAtIndex', async header => {
|
|
546
|
+
if (typeof header.stubs[index] === 'undefined') {
|
|
547
|
+
throw errors.MissingResourceError(`no stub at index ${index}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
stubDir = header.stubs[index].meta.dir;
|
|
551
|
+
header.stubs.splice(index, 1);
|
|
552
|
+
return header;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await remove(`${baseDir}/${stubDir}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Overwrites all stubs with a new list
|
|
560
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
561
|
+
* @param {Object} newStubs - the new list of stubs
|
|
562
|
+
* @returns {Object} - the promise
|
|
563
|
+
*/
|
|
564
|
+
async function overwriteAll (newStubs) {
|
|
565
|
+
await readAndWriteHeader('overwriteAllStubs', async header => {
|
|
566
|
+
header.stubs = [];
|
|
567
|
+
await remove(`${baseDir}/stubs`);
|
|
568
|
+
return header;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
let addSequence = Promise.resolve();
|
|
572
|
+
newStubs.forEach(stub => {
|
|
573
|
+
addSequence = addSequence.then(() => add(stub));
|
|
574
|
+
});
|
|
575
|
+
await addSequence;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Overwrites the stub at the given index
|
|
580
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
581
|
+
* @param {Object} stub - the new stub
|
|
582
|
+
* @param {Number} index - the index of the stub to overwrite
|
|
583
|
+
* @returns {Object} - the promise
|
|
584
|
+
*/
|
|
585
|
+
async function overwriteAtIndex (stub, index) {
|
|
586
|
+
await deleteAtIndex(index);
|
|
587
|
+
await insertAtIndex(stub, index);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function loadResponses (stub) {
|
|
591
|
+
const meta = await readFile(metaPath(stub.meta.dir));
|
|
592
|
+
return Promise.all(meta.responseFiles.map(responseFile =>
|
|
593
|
+
readFile(responsePath(stub.meta.dir, responseFile))));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function loadMatches (stub) {
|
|
597
|
+
return loadAllInDir(`${baseDir}/${stub.meta.dir}/matches`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Returns a JSON-convertible representation
|
|
602
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
603
|
+
* @param {Object} options - The formatting options
|
|
604
|
+
* @param {Boolean} options.debug - If true, includes debug information
|
|
605
|
+
* @returns {Object} - the promise resolving to the JSON object
|
|
606
|
+
*/
|
|
607
|
+
async function toJSON (options = {}) {
|
|
608
|
+
const header = await readHeader(),
|
|
609
|
+
responsePromises = header.stubs.map(loadResponses),
|
|
610
|
+
stubResponses = await Promise.all(responsePromises),
|
|
611
|
+
debugPromises = options.debug ? header.stubs.map(loadMatches) : [],
|
|
612
|
+
matches = await Promise.all(debugPromises);
|
|
613
|
+
|
|
614
|
+
header.stubs.forEach((stub, index) => {
|
|
615
|
+
stub.responses = stubResponses[index];
|
|
616
|
+
if (options.debug && matches[index].length > 0) {
|
|
617
|
+
stub.matches = matches[index];
|
|
618
|
+
}
|
|
619
|
+
delete stub.meta;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
return header.stubs;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function isRecordedResponse (response) {
|
|
626
|
+
return response.is && typeof response.is._proxyResponseTime === 'number';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Removes the saved proxy responses
|
|
631
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
632
|
+
* @returns {Object} - Promise
|
|
633
|
+
*/
|
|
634
|
+
async function deleteSavedProxyResponses () {
|
|
635
|
+
const allStubs = await toJSON();
|
|
636
|
+
allStubs.forEach(stub => {
|
|
637
|
+
stub.responses = stub.responses.filter(response => !isRecordedResponse(response));
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const nonProxyStubs = allStubs.filter(stub => stub.responses.length > 0);
|
|
641
|
+
return overwriteAll(nonProxyStubs);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Adds a request for the imposter
|
|
646
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
647
|
+
* @param {Object} request - the request
|
|
648
|
+
* @returns {Object} - the promise
|
|
649
|
+
*/
|
|
650
|
+
async function addRequest (request) {
|
|
651
|
+
const recordedRequest = helpers.clone(request);
|
|
652
|
+
|
|
653
|
+
recordedRequest.timestamp = new Date().toJSON();
|
|
654
|
+
await writeFile(requestPath(recordedRequest), recordedRequest);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Returns the saved requests for the imposter
|
|
659
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
660
|
+
* @returns {Object} - the promise resolving to the array of requests
|
|
661
|
+
*/
|
|
662
|
+
async function loadRequests () {
|
|
663
|
+
return loadAllInDir(`${baseDir}/requests`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Deletes the requests directory for an imposter
|
|
668
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
669
|
+
* @returns {Object} - Promise
|
|
670
|
+
*/
|
|
671
|
+
async function deleteSavedRequests () {
|
|
672
|
+
await remove(`${baseDir}/requests`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
count,
|
|
677
|
+
first,
|
|
678
|
+
add,
|
|
679
|
+
insertAtIndex,
|
|
680
|
+
overwriteAll,
|
|
681
|
+
overwriteAtIndex,
|
|
682
|
+
deleteAtIndex,
|
|
683
|
+
toJSON,
|
|
684
|
+
deleteSavedProxyResponses,
|
|
685
|
+
addRequest,
|
|
686
|
+
loadRequests,
|
|
687
|
+
deleteSavedRequests
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function imposterDir (id) {
|
|
692
|
+
return `${config.datadir}/${id}`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function headerFile (id) {
|
|
696
|
+
return `${imposterDir(id)}/imposter.json`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Returns the stubs repository for the imposter
|
|
701
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
702
|
+
* @param {Number} id - the id of the imposter
|
|
703
|
+
* @returns {Object} - the stubs repository
|
|
704
|
+
*/
|
|
705
|
+
function stubsFor (id) {
|
|
706
|
+
return stubRepository(imposterDir(id));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Saves a reference to the imposter so that the functions
|
|
711
|
+
* (which can't be persisted) can be rehydrated to a loaded imposter.
|
|
712
|
+
* This means that any data in the function closures will be held in
|
|
713
|
+
* memory.
|
|
714
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
715
|
+
* @param {Object} imposter - the imposter
|
|
716
|
+
*/
|
|
717
|
+
function addReference (imposter) {
|
|
718
|
+
const id = String(imposter.port);
|
|
719
|
+
imposterFns[id] = {};
|
|
720
|
+
Object.keys(imposter).forEach(key => {
|
|
721
|
+
if (typeof imposter[key] === 'function') {
|
|
722
|
+
imposterFns[id][key] = imposter[key];
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function rehydrate (imposter) {
|
|
728
|
+
const id = String(imposter.port);
|
|
729
|
+
Object.keys(imposterFns[id]).forEach(key => {
|
|
730
|
+
imposter[key] = imposterFns[id][key];
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Adds a new imposter
|
|
736
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
737
|
+
* @param {Object} imposter - the imposter to add
|
|
738
|
+
* @returns {Object} - the promise
|
|
739
|
+
*/
|
|
740
|
+
async function add (imposter) {
|
|
741
|
+
const imposterConfig = imposter.creationRequest,
|
|
742
|
+
stubs = imposterConfig.stubs || [],
|
|
743
|
+
saveStubs = stubs.map(stub => saveStubMetaAndResponses(stub, imposterDir(imposter.port))),
|
|
744
|
+
stubDefinitions = await Promise.all(saveStubs);
|
|
745
|
+
|
|
746
|
+
delete imposterConfig.requests;
|
|
747
|
+
imposterConfig.port = imposter.port;
|
|
748
|
+
imposterConfig.stubs = stubDefinitions;
|
|
749
|
+
await writeFile(headerFile(imposter.port), imposterConfig);
|
|
750
|
+
|
|
751
|
+
addReference(imposter);
|
|
752
|
+
return imposter;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Gets the imposter by id
|
|
757
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
758
|
+
* @param {Number} id - the id of the imposter (e.g. the port)
|
|
759
|
+
* @returns {Object} - the promise resolving to the imposter
|
|
760
|
+
*/
|
|
761
|
+
async function get (id) {
|
|
762
|
+
const header = await readFile(headerFile(id), null);
|
|
763
|
+
if (header === null) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
header.stubs = await stubsFor(id).toJSON();
|
|
768
|
+
rehydrate(header);
|
|
769
|
+
return header;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Gets all imposters
|
|
774
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
775
|
+
* @returns {Object} - all imposters keyed by port
|
|
776
|
+
*/
|
|
777
|
+
async function all () {
|
|
778
|
+
return Promise.all(Object.keys(imposterFns).map(get));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Returns whether an imposter at the given id exists or not
|
|
783
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
784
|
+
* @param {Number} id - the id (e.g. the port)
|
|
785
|
+
* @returns {boolean}
|
|
786
|
+
*/
|
|
787
|
+
async function exists (id) {
|
|
788
|
+
return Object.keys(imposterFns).indexOf(String(id)) >= 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function shutdown (id) {
|
|
792
|
+
if (typeof imposterFns[String(id)] === 'undefined') {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const stop = imposterFns[String(id)].stop;
|
|
797
|
+
delete imposterFns[String(id)];
|
|
798
|
+
if (stop) {
|
|
799
|
+
await stop();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Deletes the imposter at the given id
|
|
805
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
806
|
+
* @param {Number} id - the id (e.g. the port)
|
|
807
|
+
* @returns {Object} - the deletion promise
|
|
808
|
+
*/
|
|
809
|
+
async function del (id) {
|
|
810
|
+
const imposter = await get(id),
|
|
811
|
+
cleanup = [shutdown(id)];
|
|
812
|
+
|
|
813
|
+
if (imposter !== null) {
|
|
814
|
+
cleanup.push(remove(imposterDir(id)));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
await Promise.all(cleanup);
|
|
818
|
+
return imposter;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Deletes all imposters; used during testing
|
|
823
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
824
|
+
*/
|
|
825
|
+
async function stopAll () {
|
|
826
|
+
const promises = Object.keys(imposterFns).map(shutdown);
|
|
827
|
+
await Promise.all(promises);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Deletes all imposters synchronously; used during shutdown
|
|
832
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
833
|
+
*/
|
|
834
|
+
function stopAllSync () {
|
|
835
|
+
Object.keys(imposterFns).forEach(shutdown);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Deletes all imposters
|
|
840
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
841
|
+
* @returns {Object} - the deletion promise
|
|
842
|
+
*/
|
|
843
|
+
async function deleteAll () {
|
|
844
|
+
const ids = Object.keys(imposterFns),
|
|
845
|
+
dirs = ids.map(imposterDir),
|
|
846
|
+
deleteImposter = ids.map(shutdown).concat(dirs.map(remove));
|
|
847
|
+
|
|
848
|
+
// Remove only the directories for imposters we have a reference to
|
|
849
|
+
await Promise.all(deleteImposter);
|
|
850
|
+
const entries = await readdir(config.datadir);
|
|
851
|
+
if (entries.length === 0) {
|
|
852
|
+
await remove(config.datadir);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function loadImposterFrom (dir, protocols) {
|
|
857
|
+
const imposterFilename = `${config.datadir}/${dir}/imposter.json`;
|
|
858
|
+
|
|
859
|
+
if (!fsExtra.existsSync(imposterFilename)) {
|
|
860
|
+
logger.warn(`Skipping ${dir} during loading; missing imposter.json`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const imposterConfig = JSON.parse(fsExtra.readFileSync(imposterFilename)),
|
|
865
|
+
protocol = protocols[imposterConfig.protocol];
|
|
866
|
+
|
|
867
|
+
if (protocol) {
|
|
868
|
+
logger.info(`Loading ${imposterConfig.protocol}:${dir} from datadir`);
|
|
869
|
+
const imposter = await protocol.createImposterFrom(imposterConfig);
|
|
870
|
+
addReference(imposter);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
logger.error(`Cannot load imposter ${dir}; no protocol loaded for ${config.protocol}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Loads all saved imposters at startup
|
|
879
|
+
* @memberOf module:models/filesystemBackedImpostersRepository#
|
|
880
|
+
* @param {Object} protocols - The protocol map, used to instantiate a new instance
|
|
881
|
+
* @returns {Object} - a promise
|
|
882
|
+
*/
|
|
883
|
+
async function loadAll (protocols) {
|
|
884
|
+
if (!config.datadir || !fsExtra.existsSync(config.datadir)) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const dirs = fsExtra.readdirSync(config.datadir),
|
|
889
|
+
promises = dirs.map(async dir => loadImposterFrom(dir, protocols));
|
|
890
|
+
await Promise.all(promises);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
add,
|
|
895
|
+
addReference,
|
|
896
|
+
get,
|
|
897
|
+
all,
|
|
898
|
+
exists,
|
|
899
|
+
del,
|
|
900
|
+
stopAll,
|
|
901
|
+
stopAllSync,
|
|
902
|
+
deleteAll,
|
|
903
|
+
stubsFor,
|
|
904
|
+
loadAll
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
module.exports = { create };
|