UNPKG

8.14 kBJavaScriptView Raw
1'use strict';
2
3const assert = require('assert');
4const { readFileSync } = require('fs');
5const { join } = require('path');
6const { inspect } = require('util');
7
8const Client = require('../lib/client.js');
9const Server = require('../lib/server.js');
10const { parseKey } = require('../lib/protocol/keyParser.js');
11
12const mustCallChecks = [];
13
14const DEFAULT_TEST_TIMEOUT = 30 * 1000;
15
16function noop() {}
17
18function runCallChecks(exitCode) {
19 if (exitCode !== 0) return;
20
21 const failed = mustCallChecks.filter((context) => {
22 if ('minimum' in context) {
23 context.messageSegment = `at least ${context.minimum}`;
24 return context.actual < context.minimum;
25 }
26 context.messageSegment = `exactly ${context.exact}`;
27 return context.actual !== context.exact;
28 });
29
30 failed.forEach((context) => {
31 console.error('Mismatched %s function calls. Expected %s, actual %d.',
32 context.name,
33 context.messageSegment,
34 context.actual);
35 console.error(context.stack.split('\n').slice(2).join('\n'));
36 });
37
38 if (failed.length)
39 process.exit(1);
40}
41
42function mustCall(fn, exact) {
43 return _mustCallInner(fn, exact, 'exact');
44}
45
46function mustCallAtLeast(fn, minimum) {
47 return _mustCallInner(fn, minimum, 'minimum');
48}
49
50function _mustCallInner(fn, criteria = 1, field) {
51 if (process._exiting)
52 throw new Error('Cannot use common.mustCall*() in process exit handler');
53
54 if (typeof fn === 'number') {
55 criteria = fn;
56 fn = noop;
57 } else if (fn === undefined) {
58 fn = noop;
59 }
60
61 if (typeof criteria !== 'number')
62 throw new TypeError(`Invalid ${field} value: ${criteria}`);
63
64 const context = {
65 [field]: criteria,
66 actual: 0,
67 stack: inspect(new Error()),
68 name: fn.name || '<anonymous>'
69 };
70
71 // Add the exit listener only once to avoid listener leak warnings
72 if (mustCallChecks.length === 0)
73 process.on('exit', runCallChecks);
74
75 mustCallChecks.push(context);
76
77 function wrapped(...args) {
78 ++context.actual;
79 return fn.call(this, ...args);
80 }
81 // TODO: remove origFn?
82 wrapped.origFn = fn;
83
84 return wrapped;
85}
86
87function getCallSite(top) {
88 const originalStackFormatter = Error.prepareStackTrace;
89 Error.prepareStackTrace = (err, stack) =>
90 `${stack[0].getFileName()}:${stack[0].getLineNumber()}`;
91 const err = new Error();
92 Error.captureStackTrace(err, top);
93 // With the V8 Error API, the stack is not formatted until it is accessed
94 // eslint-disable-next-line no-unused-expressions
95 err.stack;
96 Error.prepareStackTrace = originalStackFormatter;
97 return err.stack;
98}
99
100function mustNotCall(msg) {
101 const callSite = getCallSite(mustNotCall);
102 return function mustNotCall(...args) {
103 args = args.map(inspect).join(', ');
104 const argsInfo = (args.length > 0
105 ? `\ncalled with arguments: ${args}`
106 : '');
107 assert.fail(
108 `${msg || 'function should not have been called'} at ${callSite}`
109 + argsInfo);
110 };
111}
112
113function setup(title, configs) {
114 const {
115 client: clientCfg_,
116 server: serverCfg_,
117 allReady: allReady_,
118 timeout: timeout_,
119 debug,
120 noForceClientReady,
121 noForceServerReady,
122 noClientError,
123 noServerError,
124 } = configs;
125
126 // Make shallow copies of client/server configs to avoid mutating them when
127 // multiple tests share the same config object reference
128 let clientCfg;
129 if (clientCfg_)
130 clientCfg = { ...clientCfg_ };
131 let serverCfg;
132 if (serverCfg_)
133 serverCfg = { ...serverCfg_ };
134
135 let clientClose = false;
136 let clientReady = false;
137 let serverClose = false;
138 let serverReady = false;
139 const msg = (text) => {
140 return `${title}: ${text}`;
141 };
142
143 const timeout = (typeof timeout_ === 'number'
144 ? timeout_
145 : DEFAULT_TEST_TIMEOUT);
146
147 const allReady = (typeof allReady_ === 'function' ? allReady_ : undefined);
148
149 if (debug) {
150 if (clientCfg) {
151 clientCfg.debug = (...args) => {
152 console.log(`[${title}][CLIENT]`, ...args);
153 };
154 }
155 if (serverCfg) {
156 serverCfg.debug = (...args) => {
157 console.log(`[${title}][SERVER]`, ...args);
158 };
159 }
160 }
161
162 let timer;
163 let client;
164 let clientReadyFn;
165 let server;
166 let serverReadyFn;
167 if (clientCfg) {
168 client = new Client();
169 if (!noClientError)
170 client.on('error', onError);
171 clientReadyFn = (noForceClientReady ? onReady : mustCall(onReady));
172 client.on('ready', clientReadyFn)
173 .on('close', mustCall(onClose));
174 } else {
175 clientReady = clientClose = true;
176 }
177
178 if (serverCfg) {
179 server = new Server(serverCfg);
180 if (!noServerError)
181 server.on('error', onError);
182 serverReadyFn = (noForceServerReady ? onReady : mustCall(onReady));
183 server.on('connection', mustCall((conn) => {
184 if (!noServerError)
185 conn.on('error', onError);
186 conn.on('ready', serverReadyFn);
187 server.close();
188 })).on('close', mustCall(onClose));
189 } else {
190 serverReady = serverClose = true;
191 }
192
193 function onError(err) {
194 const which = (this === client ? 'client' : 'server');
195 assert(false, msg(`Unexpected ${which} error: ${err.stack}\n`));
196 }
197
198 function onReady() {
199 if (this === client) {
200 assert(!clientReady,
201 msg('Received multiple ready events for client'));
202 clientReady = true;
203 } else {
204 assert(!serverReady,
205 msg('Received multiple ready events for server'));
206 serverReady = true;
207 }
208 clientReady && serverReady && allReady && allReady();
209 }
210
211 function onClose() {
212 if (this === client) {
213 assert(!clientClose,
214 msg('Received multiple close events for client'));
215 clientClose = true;
216 } else {
217 assert(!serverClose,
218 msg('Received multiple close events for server'));
219 serverClose = true;
220 }
221 if (clientClose && serverClose)
222 clearTimeout(timer);
223 }
224
225 process.nextTick(mustCall(() => {
226 function connectClient() {
227 if (clientCfg.sock) {
228 clientCfg.sock.connect(server.address().port, 'localhost');
229 } else {
230 clientCfg.host = 'localhost';
231 clientCfg.port = server.address().port;
232 }
233 try {
234 client.connect(clientCfg);
235 } catch (ex) {
236 ex.message = msg(ex.message);
237 throw ex;
238 }
239 }
240
241 if (server) {
242 server.listen(0, 'localhost', mustCall(() => {
243 if (timeout >= 0) {
244 timer = setTimeout(() => {
245 assert(false, msg('Test timed out'));
246 }, timeout);
247 }
248 if (client)
249 connectClient();
250 }));
251 }
252 }));
253
254 return { client, server };
255}
256
257const FIXTURES_DIR = join(__dirname, 'fixtures');
258const fixture = (() => {
259 const cache = new Map();
260 return (file) => {
261 const existing = cache.get(file);
262 if (existing !== undefined)
263 return existing;
264
265 const result = readFileSync(join(FIXTURES_DIR, file));
266 cache.set(file, result);
267 return result;
268 };
269})();
270const fixtureKey = (() => {
271 const cache = new Map();
272 return (file, passphrase, bypass) => {
273 if (typeof passphrase === 'boolean') {
274 bypass = passphrase;
275 passphrase = undefined;
276 }
277 if (typeof bypass !== 'boolean' || !bypass) {
278 const existing = cache.get(file);
279 if (existing !== undefined)
280 return existing;
281 }
282 const fullPath = join(FIXTURES_DIR, file);
283 const raw = fixture(file);
284 let key = parseKey(raw, passphrase);
285 if (Array.isArray(key))
286 key = key[0];
287 const result = { key, raw, fullPath };
288 cache.set(file, result);
289 return result;
290 };
291})();
292
293function setupSimple(debug, title) {
294 const { client, server } = setup(title, {
295 client: { username: 'Password User', password: '12345' },
296 server: { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] },
297 debug,
298 });
299 server.on('connection', mustCall((conn) => {
300 conn.on('authentication', mustCall((ctx) => {
301 ctx.accept();
302 }));
303 }));
304 return { client, server };
305}
306
307module.exports = {
308 fixture,
309 fixtureKey,
310 FIXTURES_DIR,
311 mustCall,
312 mustCallAtLeast,
313 mustNotCall,
314 setup,
315 setupSimple,
316};