317 lines
7.9 KiB
JavaScript
317 lines
7.9 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const { readFileSync } = require('fs');
|
|
const { join } = require('path');
|
|
const { inspect } = require('util');
|
|
|
|
const Client = require('../lib/client.js');
|
|
const Server = require('../lib/server.js');
|
|
const { parseKey } = require('../lib/protocol/keyParser.js');
|
|
|
|
const mustCallChecks = [];
|
|
|
|
const DEFAULT_TEST_TIMEOUT = 30 * 1000;
|
|
|
|
function noop() {}
|
|
|
|
function runCallChecks(exitCode) {
|
|
if (exitCode !== 0) return;
|
|
|
|
const failed = mustCallChecks.filter((context) => {
|
|
if ('minimum' in context) {
|
|
context.messageSegment = `at least ${context.minimum}`;
|
|
return context.actual < context.minimum;
|
|
}
|
|
context.messageSegment = `exactly ${context.exact}`;
|
|
return context.actual !== context.exact;
|
|
});
|
|
|
|
failed.forEach((context) => {
|
|
console.error('Mismatched %s function calls. Expected %s, actual %d.',
|
|
context.name,
|
|
context.messageSegment,
|
|
context.actual);
|
|
console.error(context.stack.split('\n').slice(2).join('\n'));
|
|
});
|
|
|
|
if (failed.length)
|
|
process.exit(1);
|
|
}
|
|
|
|
function mustCall(fn, exact) {
|
|
return _mustCallInner(fn, exact, 'exact');
|
|
}
|
|
|
|
function mustCallAtLeast(fn, minimum) {
|
|
return _mustCallInner(fn, minimum, 'minimum');
|
|
}
|
|
|
|
function _mustCallInner(fn, criteria = 1, field) {
|
|
if (process._exiting)
|
|
throw new Error('Cannot use common.mustCall*() in process exit handler');
|
|
|
|
if (typeof fn === 'number') {
|
|
criteria = fn;
|
|
fn = noop;
|
|
} else if (fn === undefined) {
|
|
fn = noop;
|
|
}
|
|
|
|
if (typeof criteria !== 'number')
|
|
throw new TypeError(`Invalid ${field} value: ${criteria}`);
|
|
|
|
const context = {
|
|
[field]: criteria,
|
|
actual: 0,
|
|
stack: inspect(new Error()),
|
|
name: fn.name || '<anonymous>'
|
|
};
|
|
|
|
// Add the exit listener only once to avoid listener leak warnings
|
|
if (mustCallChecks.length === 0)
|
|
process.on('exit', runCallChecks);
|
|
|
|
mustCallChecks.push(context);
|
|
|
|
function wrapped(...args) {
|
|
++context.actual;
|
|
return fn.call(this, ...args);
|
|
}
|
|
// TODO: remove origFn?
|
|
wrapped.origFn = fn;
|
|
|
|
return wrapped;
|
|
}
|
|
|
|
function getCallSite(top) {
|
|
const originalStackFormatter = Error.prepareStackTrace;
|
|
Error.prepareStackTrace = (err, stack) =>
|
|
`${stack[0].getFileName()}:${stack[0].getLineNumber()}`;
|
|
const err = new Error();
|
|
Error.captureStackTrace(err, top);
|
|
// With the V8 Error API, the stack is not formatted until it is accessed
|
|
// eslint-disable-next-line no-unused-expressions
|
|
err.stack;
|
|
Error.prepareStackTrace = originalStackFormatter;
|
|
return err.stack;
|
|
}
|
|
|
|
function mustNotCall(msg) {
|
|
const callSite = getCallSite(mustNotCall);
|
|
return function mustNotCall(...args) {
|
|
args = args.map(inspect).join(', ');
|
|
const argsInfo = (args.length > 0
|
|
? `\ncalled with arguments: ${args}`
|
|
: '');
|
|
assert.fail(
|
|
`${msg || 'function should not have been called'} at ${callSite}`
|
|
+ argsInfo);
|
|
};
|
|
}
|
|
|
|
function setup(title, configs) {
|
|
const {
|
|
client: clientCfg_,
|
|
server: serverCfg_,
|
|
allReady: allReady_,
|
|
timeout: timeout_,
|
|
debug,
|
|
noForceClientReady,
|
|
noForceServerReady,
|
|
noClientError,
|
|
noServerError,
|
|
} = configs;
|
|
|
|
// Make shallow copies of client/server configs to avoid mutating them when
|
|
// multiple tests share the same config object reference
|
|
let clientCfg;
|
|
if (clientCfg_)
|
|
clientCfg = { ...clientCfg_ };
|
|
let serverCfg;
|
|
if (serverCfg_)
|
|
serverCfg = { ...serverCfg_ };
|
|
|
|
let clientClose = false;
|
|
let clientReady = false;
|
|
let serverClose = false;
|
|
let serverReady = false;
|
|
const msg = (text) => {
|
|
return `${title}: ${text}`;
|
|
};
|
|
|
|
const timeout = (typeof timeout_ === 'number'
|
|
? timeout_
|
|
: DEFAULT_TEST_TIMEOUT);
|
|
|
|
const allReady = (typeof allReady_ === 'function' ? allReady_ : undefined);
|
|
|
|
if (debug) {
|
|
if (clientCfg) {
|
|
clientCfg.debug = (...args) => {
|
|
console.log(`[${title}][CLIENT]`, ...args);
|
|
};
|
|
}
|
|
if (serverCfg) {
|
|
serverCfg.debug = (...args) => {
|
|
console.log(`[${title}][SERVER]`, ...args);
|
|
};
|
|
}
|
|
}
|
|
|
|
let timer;
|
|
let client;
|
|
let clientReadyFn;
|
|
let server;
|
|
let serverReadyFn;
|
|
if (clientCfg) {
|
|
client = new Client();
|
|
if (!noClientError)
|
|
client.on('error', onError);
|
|
clientReadyFn = (noForceClientReady ? onReady : mustCall(onReady));
|
|
client.on('ready', clientReadyFn)
|
|
.on('close', mustCall(onClose));
|
|
} else {
|
|
clientReady = clientClose = true;
|
|
}
|
|
|
|
if (serverCfg) {
|
|
server = new Server(serverCfg);
|
|
if (!noServerError)
|
|
server.on('error', onError);
|
|
serverReadyFn = (noForceServerReady ? onReady : mustCall(onReady));
|
|
server.on('connection', mustCall((conn) => {
|
|
if (!noServerError)
|
|
conn.on('error', onError);
|
|
conn.on('ready', serverReadyFn);
|
|
server.close();
|
|
})).on('close', mustCall(onClose));
|
|
} else {
|
|
serverReady = serverClose = true;
|
|
}
|
|
|
|
function onError(err) {
|
|
const which = (this === client ? 'client' : 'server');
|
|
assert(false, msg(`Unexpected ${which} error: ${err.stack}\n`));
|
|
}
|
|
|
|
function onReady() {
|
|
if (this === client) {
|
|
assert(!clientReady,
|
|
msg('Received multiple ready events for client'));
|
|
clientReady = true;
|
|
} else {
|
|
assert(!serverReady,
|
|
msg('Received multiple ready events for server'));
|
|
serverReady = true;
|
|
}
|
|
clientReady && serverReady && allReady && allReady();
|
|
}
|
|
|
|
function onClose() {
|
|
if (this === client) {
|
|
assert(!clientClose,
|
|
msg('Received multiple close events for client'));
|
|
clientClose = true;
|
|
} else {
|
|
assert(!serverClose,
|
|
msg('Received multiple close events for server'));
|
|
serverClose = true;
|
|
}
|
|
if (clientClose && serverClose)
|
|
clearTimeout(timer);
|
|
}
|
|
|
|
process.nextTick(mustCall(() => {
|
|
function connectClient() {
|
|
if (clientCfg.sock) {
|
|
clientCfg.sock.connect(server.address().port, 'localhost');
|
|
} else {
|
|
clientCfg.host = 'localhost';
|
|
clientCfg.port = server.address().port;
|
|
}
|
|
try {
|
|
client.connect(clientCfg);
|
|
} catch (ex) {
|
|
ex.message = msg(ex.message);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
if (server) {
|
|
server.listen(0, 'localhost', mustCall(() => {
|
|
if (timeout >= 0) {
|
|
timer = setTimeout(() => {
|
|
assert(false, msg('Test timed out'));
|
|
}, timeout);
|
|
}
|
|
if (client)
|
|
connectClient();
|
|
}));
|
|
}
|
|
}));
|
|
|
|
return { client, server };
|
|
}
|
|
|
|
const FIXTURES_DIR = join(__dirname, 'fixtures');
|
|
const fixture = (() => {
|
|
const cache = new Map();
|
|
return (file) => {
|
|
const existing = cache.get(file);
|
|
if (existing !== undefined)
|
|
return existing;
|
|
|
|
const result = readFileSync(join(FIXTURES_DIR, file));
|
|
cache.set(file, result);
|
|
return result;
|
|
};
|
|
})();
|
|
const fixtureKey = (() => {
|
|
const cache = new Map();
|
|
return (file, passphrase, bypass) => {
|
|
if (typeof passphrase === 'boolean') {
|
|
bypass = passphrase;
|
|
passphrase = undefined;
|
|
}
|
|
if (typeof bypass !== 'boolean' || !bypass) {
|
|
const existing = cache.get(file);
|
|
if (existing !== undefined)
|
|
return existing;
|
|
}
|
|
const fullPath = join(FIXTURES_DIR, file);
|
|
const raw = fixture(file);
|
|
let key = parseKey(raw, passphrase);
|
|
if (Array.isArray(key))
|
|
key = key[0];
|
|
const result = { key, raw, fullPath };
|
|
cache.set(file, result);
|
|
return result;
|
|
};
|
|
})();
|
|
|
|
function setupSimple(debug, title) {
|
|
const { client, server } = setup(title, {
|
|
client: { username: 'Password User', password: '12345' },
|
|
server: { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] },
|
|
debug,
|
|
});
|
|
server.on('connection', mustCall((conn) => {
|
|
conn.on('authentication', mustCall((ctx) => {
|
|
ctx.accept();
|
|
}));
|
|
}));
|
|
return { client, server };
|
|
}
|
|
|
|
module.exports = {
|
|
fixture,
|
|
fixtureKey,
|
|
FIXTURES_DIR,
|
|
mustCall,
|
|
mustCallAtLeast,
|
|
mustNotCall,
|
|
setup,
|
|
setupSimple,
|
|
};
|