316 lines
7.8 KiB
JavaScript
316 lines
7.8 KiB
JavaScript
'use strict'
|
|
|
|
try {
|
|
require('bare-process/global')
|
|
} catch {
|
|
/* Node.js */
|
|
}
|
|
|
|
const assert = require('./assert-compat.js')
|
|
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
|
|
}
|