UNPKG

imapflow

Version:

IMAP Client for Node

1,047 lines (913 loc) 31.7 kB
'use strict'; // End-to-end ImapFlow tests against a scriptable in-process mock IMAP server. // This exercises the connection lifecycle that pure unit tests cannot reach: // connect/greeting handling, startSession (CAPABILITY/ID/NAMESPACE/ENABLE), // authentication, the reader loop (OK/NO/BAD/untagged/continuation handling), // send/trySend/write, socket handlers, autoidle, locks, logout and close. const net = require('net'); const { ImapFlow } = require('../lib/imap-flow'); // --------------------------------------------------------------------------- // Mock IMAP server // --------------------------------------------------------------------------- // `handlers` maps an uppercase command keyword to (ctx) => void, where ctx // provides { tag, line, args, write(str), ok(text), no(text), bad(text), socket }. // Sensible defaults are provided for a full happy-path session; tests override // individual commands as needed. const createServer = (options = {}) => { const capabilities = options.capabilities || 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA'; const greeting = options.greeting || `* OK [CAPABILITY ${capabilities}] mock ready\r\n`; const defaults = { CAPABILITY(ctx) { ctx.write(`* CAPABILITY ${capabilities}\r\n`); ctx.ok('CAPABILITY completed'); }, ID(ctx) { ctx.write('* ID ("name" "mock" "version" "1.0")\r\n'); ctx.ok('ID completed'); }, NAMESPACE(ctx) { ctx.write('* NAMESPACE (("" "/")) NIL NIL\r\n'); ctx.ok('NAMESPACE completed'); }, ENABLE(ctx) { ctx.write('* ENABLED CONDSTORE\r\n'); ctx.ok('ENABLE completed'); }, LOGIN(ctx) { ctx.ok('LOGIN completed'); }, COMPRESS(ctx) { ctx.no('COMPRESS not available'); }, SELECT(ctx) { ctx.write('* 3 EXISTS\r\n'); ctx.write('* 0 RECENT\r\n'); ctx.write('* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n'); ctx.write('* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft \\*)] Limited\r\n'); ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n'); ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n'); ctx.write('* OK [HIGHESTMODSEQ 1000] Highest\r\n'); ctx.ok('[READ-WRITE] SELECT completed'); }, EXAMINE(ctx) { ctx.write('* 3 EXISTS\r\n'); ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n'); ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n'); ctx.ok('[READ-ONLY] EXAMINE completed'); }, LIST(ctx) { ctx.write('* LIST (\\HasNoChildren) "/" "INBOX"\r\n'); ctx.write('* LIST (\\HasNoChildren \\Sent) "/" "Sent"\r\n'); ctx.ok('LIST completed'); }, LSUB(ctx) { ctx.write('* LSUB (\\HasNoChildren) "/" "INBOX"\r\n'); ctx.ok('LSUB completed'); }, STATUS(ctx) { ctx.write('* STATUS "INBOX" (MESSAGES 3 UIDNEXT 100 UIDVALIDITY 12345 UNSEEN 1)\r\n'); ctx.ok('STATUS completed'); }, NOOP(ctx) { ctx.ok('NOOP completed'); }, LOGOUT(ctx) { ctx.write('* BYE Logging out\r\n'); ctx.ok('LOGOUT completed'); }, SEARCH(ctx) { ctx.write('* SEARCH 1 2 3\r\n'); ctx.ok('SEARCH completed'); }, CREATE(ctx) { ctx.ok('CREATE completed'); }, DELETE(ctx) { ctx.ok('DELETE completed'); }, RENAME(ctx) { ctx.ok('RENAME completed'); }, SUBSCRIBE(ctx) { ctx.ok('SUBSCRIBE completed'); }, UNSUBSCRIBE(ctx) { ctx.ok('UNSUBSCRIBE completed'); } }; const handlers = Object.assign({}, defaults, options.handlers || {}); const server = net.createServer(socket => { socket.setNoDelay(true); socket.on('error', () => {}); if (options.onConnect) { options.onConnect(socket); } if (greeting) { socket.write(greeting); } let buf = Buffer.alloc(0); let literalRemaining = 0; let cmdPrefix = ''; const dispatch = fullLine => { // fullLine is the first physical line of the command (tag + command + args); // literal payloads are not needed to choose a response. let parts = fullLine.split(' '); let tag = parts[0]; let command = (parts[1] || '').toUpperCase(); // IDLE is terminated by a bare, untagged "DONE" continuation line, so it // has no tag/command split — route it to the DONE handler explicitly. if (!parts[1] && (parts[0] || '').toUpperCase() === 'DONE') { command = 'DONE'; } let args = parts.slice(2).join(' '); const ctx = { tag, command, line: fullLine, args, socket, write: str => socket.write(str), ok: text => socket.write(`${tag} OK ${text || 'completed'}\r\n`), no: text => socket.write(`${tag} NO ${text || 'failed'}\r\n`), bad: text => socket.write(`${tag} BAD ${text || 'bad'}\r\n`) }; let handler = handlers[command]; if (typeof handler === 'function') { handler(ctx); } else if (handler === null) { // explicitly silent (e.g. simulate no response) } else { ctx.bad(`Unknown command ${command}`); } }; const processBuffer = () => { // Loop until we run out of complete lines / literal data. while (true) { if (literalRemaining > 0) { if (buf.length < literalRemaining) { return; } buf = buf.slice(literalRemaining); literalRemaining = 0; // Fall through to read the continuation line (post-literal text + CRLF) } let idx = buf.indexOf('\r\n'); if (idx < 0) { return; } let line = buf.slice(0, idx).toString('binary'); buf = buf.slice(idx + 2); let combined = cmdPrefix + line; // Synchronizing / non-synchronizing literal at end of line? let m = combined.match(/\{(\d+)(\+)?\}$/); if (m) { literalRemaining = Number(m[1]); // Keep the prefix (sans literal marker) for command keyword extraction cmdPrefix = combined.replace(/\{(\d+)(\+)?\}$/, '<literal> '); if (!m[2]) { // synchronizing literal -> tell client to proceed socket.write('+ Ready for literal data\r\n'); } continue; } cmdPrefix = ''; dispatch(combined); } }; socket.on('data', chunk => { buf = Buffer.concat([buf, chunk]); processBuffer(); }); }); return server; }; const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port))); const makeClient = (port, overrides = {}) => new ImapFlow({ host: '127.0.0.1', port, secure: false, disableAutoIdle: true, disableCompression: true, logger: false, auth: { user: 'test', pass: 'secret' }, ...overrides }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- module.exports['Server: full connect + session + logout'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); test.ok(client.authenticated, 'authenticated'); test.ok(client.usable, 'usable'); test.ok(client.capabilities.has('IMAP4rev1')); test.ok(client.serverInfo || client.namespace, 'namespace set'); await client.logout(); test.equal(client.state, client.states.LOGOUT); client.close(); server.close(); test.done(); }; module.exports['Server: qresync option adds QRESYNC to ENABLE'] = async test => { let enabledArgs = null; let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE CONDSTORE QRESYNC', handlers: { ENABLE(ctx) { enabledArgs = ctx.args; ctx.write('* ENABLED CONDSTORE QRESYNC\r\n'); ctx.ok('ENABLE completed'); } } }); let port = await listen(server); let client = makeClient(port, { qresync: true }); client.on('error', () => {}); await client.connect(); test.ok(/QRESYNC/.test(enabledArgs || ''), 'QRESYNC requested in ENABLE'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: connect runs NOOP and LIST'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.noop(); let folders = await client.list(); test.ok(folders.length >= 2); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: enable compression negotiation (server declines)'] = async test => { let server = createServer(); let port = await listen(server); // disableCompression false -> client issues COMPRESS, server says NO let client = makeClient(port, { disableCompression: false }); client.on('error', () => {}); await client.connect(); test.ok(client.usable); test.ok(!client._deflate, 'compression not enabled when server declines'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: AUTHENTICATE LOGIN flow'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=LOGIN', handlers: { AUTHENTICATE(ctx) { // SASL LOGIN: server prompts for username then password ctx.write('+ VXNlcm5hbWU6\r\n'); // "Username:" ctx.socket.once('data', () => { ctx.write('+ UGFzc3dvcmQ6\r\n'); // "Password:" ctx.socket.once('data', () => { ctx.ok('AUTHENTICATE completed'); }); }); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); test.ok(client.authenticated); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: SELECT, SEARCH, STORE and EXPUNGE via run'] = async test => { let server = createServer({ handlers: { STORE(ctx) { ctx.write('* 1 FETCH (FLAGS (\\Seen))\r\n'); ctx.ok('STORE completed'); }, EXPUNGE(ctx) { ctx.write('* 1 EXPUNGE\r\n'); ctx.ok('EXPUNGE completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let mailbox = await client.mailboxOpen('INBOX'); test.equal(mailbox.exists, 3); let found = await client.search({ seen: true }, { uid: false }); test.deepEqual(found, [1, 2, 3]); let stored = await client.messageFlagsAdd('1', ['\\Seen']); test.ok(stored); let expungeEvents = []; client.on('expunge', e => expungeEvents.push(e)); await client.messageDelete('1', { uid: false }); test.ok(expungeEvents.length >= 1); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: FETCH via fetchOne'] = async test => { let server = createServer({ handlers: { FETCH(ctx) { ctx.write('* 1 FETCH (UID 11 FLAGS (\\Seen) RFC822.SIZE 42)\r\n'); ctx.ok('FETCH completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.mailboxOpen('INBOX'); let msg = await client.fetchOne('1', { uid: true, flags: true, size: true }); test.equal(msg.uid, 11); test.equal(msg.size, 42); test.ok(msg.flags.has('\\Seen')); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: APPEND with synchronizing literal'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS', handlers: { APPEND(ctx) { ctx.ok('[APPENDUID 12345 9] APPEND completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let res = await client.append('INBOX', 'Subject: hi\r\n\r\nbody', ['\\Seen']); test.ok(res); test.equal(res.uid, 9); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: AUTHENTICATE PLAIN flow'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN', handlers: { AUTHENTICATE(ctx) { ctx.write('+ \r\n'); ctx.socket.once('data', () => ctx.ok('AUTHENTICATE completed')); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); test.ok(client.authenticated); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: AUTHENTICATE XOAUTH2 with accessToken'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=XOAUTH2', handlers: { AUTHENTICATE(ctx) { ctx.ok('AUTHENTICATE completed'); } } }); let port = await listen(server); let client = makeClient(port, { auth: { user: 'test', accessToken: 'token-123' } }); client.on('error', () => {}); await client.connect(); test.ok(client.authenticated); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: backslash username forces LOGIN method'] = async test => { let loginUsed = false; let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN', handlers: { LOGIN(ctx) { loginUsed = true; ctx.ok('LOGIN completed'); } } }); let port = await listen(server); let client = makeClient(port, { auth: { user: 'domain\\user', pass: 'secret' } }); client.on('error', () => {}); await client.connect(); test.ok(client.authenticated); test.ok(loginUsed, 'used LOGIN command despite AUTH=PLAIN'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: LOGINDISABLED rejects connect'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE LOGINDISABLED' }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected when login disabled'); client.close(); server.close(); test.done(); }; module.exports['Server: missing auth config rejects connect'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port, { auth: false }); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected without auth'); client.close(); server.close(); test.done(); }; module.exports['Server: auth without password rejects connect'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port, { auth: { user: 'only-user' } }); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected without password'); client.close(); server.close(); test.done(); }; module.exports['Server: command returning NO rejects with responseStatus'] = async test => { let server = createServer({ handlers: { CREATE(ctx) { ctx.no('Mailbox already exists'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let err = null; try { await client.mailboxCreate('Existing'); } catch (e) { err = e; } test.ok(err); test.equal(err.responseStatus, 'NO'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: command returning BAD rejects'] = async test => { let server = createServer({ handlers: { CREATE(ctx) { ctx.bad('Invalid arguments'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let err = null; try { await client.mailboxCreate('Whatever'); } catch (e) { err = e; } test.ok(err); test.equal(err.responseStatus, 'BAD'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: verifyOnly connect lists mailboxes and logs out'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port, { verifyOnly: true, includeMailboxes: true }); client.on('error', () => {}); await client.connect(); test.ok(Array.isArray(client._mailboxList), 'mailbox list captured'); test.ok(client._mailboxList.length >= 1); // verifyOnly logs out at the end of startSession test.equal(client.state, client.states.LOGOUT); client.close(); server.close(); test.done(); }; module.exports['Server: ID is re-requested after login when first response is sparse'] = async test => { let idCalls = 0; let server = createServer({ handlers: { ID(ctx) { idCalls++; if (idCalls === 1) { // sparse/NIL ID before login triggers a re-request afterwards ctx.write('* ID NIL\r\n'); } else { ctx.write('* ID ("name" "mock" "version" "1")\r\n'); } ctx.ok('ID completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); test.ok(idCalls >= 2, 'ID requested again after login'); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: NAMESPACE BAD with auth message surfaces auth failure'] = async test => { let server = createServer({ handlers: { NAMESPACE(ctx) { ctx.bad('User is authenticated but not connected'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected'); test.ok(/Authentication failed/i.test(err.message) || err.authenticationFailed, 'reported as auth failure'); client.close(); server.close(); test.done(); }; module.exports['Server: getMailboxLock selects and releases'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let lock = await client.getMailboxLock('INBOX'); test.equal(client.mailbox.path, 'INBOX'); lock.release(); // second lock on same mailbox -> fast path let lock2 = await client.getMailboxLock('INBOX'); lock2.release(); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: getMailboxLock rejects for missing mailbox'] = async test => { let server = createServer({ handlers: { SELECT(ctx) { ctx.no('Mailbox does not exist'); }, LIST(ctx) { // empty LIST -> mailbox confirmed missing ctx.ok('LIST completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let err = null; try { await client.getMailboxLock('Missing'); } catch (e) { err = e; } test.ok(err); test.ok(err.mailboxMissing, 'flagged as missing mailbox'); client.close(); server.close(); test.done(); }; module.exports['Server: IDLE then break out'] = async test => { test.expect(2); let idleTag = null; let doneReceived = false; // Capabilities MUST advertise IDLE, otherwise the client falls back to NOOP polling // and never sends an IDLE command at all. let server = createServer({ capabilities: 'IMAP4rev1 IDLE ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA', handlers: { IDLE(ctx) { // Remember the IDLE command tag so the matching DONE can complete it. idleTag = ctx.tag; ctx.write('+ idling\r\n'); }, DONE(ctx) { doneReceived = true; // Complete the original IDLE command so client.idle() resolves cleanly. ctx.socket.write(`${idleTag} OK IDLE terminated\r\n`); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.mailboxOpen('INBOX'); // Enter IDLE, then break out by issuing another command. Queuing NOOP triggers // preCheck(), which sends DONE; the server's DONE handler then completes IDLE. let idlePromise = client.idle(); await client.noop(); await idlePromise; test.ok(doneReceived, 'server received the DONE continuation'); test.equal(client.idling, false, 'client left the IDLE state'); client.close(); server.close(); test.done(); }; module.exports['Server: stats counts bytes'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let stats = client.stats(); test.ok(stats.sent > 0); test.ok(stats.received > 0); let stats2 = client.stats(true); // reset test.ok(stats2.sent >= 0); let stats3 = client.stats(); test.equal(stats3.sent, 0); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: PREAUTH greeting skips login'] = async test => { let server = createServer({ greeting: '* PREAUTH [CAPABILITY IMAP4rev1 ID ENABLE NAMESPACE] already authenticated\r\n' }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); test.equal(client.state, client.states.AUTHENTICATED); test.ok(client.usable); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: unsolicited EXISTS and VANISHED reach the untagged handlers'] = async test => { let server = createServer({ capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE QRESYNC', handlers: { NOOP(ctx) { // unsolicited untagged updates piggybacked on NOOP ctx.write('* 5 EXISTS\r\n'); ctx.write('* VANISHED 1:2\r\n'); ctx.ok('NOOP completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.mailboxOpen('INBOX'); let existsEvent = null; let expungeEvents = []; client.on('exists', e => { existsEvent = e; }); client.on('expunge', e => expungeEvents.push(e)); await client.noop(); // allow the untagged handlers to run await new Promise(r => setTimeout(r, 20)); test.ok(existsEvent, 'EXISTS handler fired'); test.equal(existsEvent.count, 5); test.ok(expungeEvents.length >= 1, 'VANISHED handler fired'); test.equal(expungeEvents[0].vanished, true); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: invalid tagged response rejects with InvalidResponse'] = async test => { let server = createServer({ handlers: { CREATE(ctx) { // a tagged response whose command is neither OK/NO/BAD ctx.write(`${ctx.tag} WEIRD unexpected status\r\n`); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let err = null; try { await client.mailboxCreate('Whatever'); } catch (e) { err = e; } test.ok(err); test.equal(err.code, 'InvalidResponse'); client.close(); server.close(); test.done(); }; module.exports['Server: reader tolerates an unparseable untagged line'] = async test => { let server = createServer({ handlers: { NOOP(ctx) { // malformed untagged line that the parser cannot parse, then a valid OK ctx.write('* 1 FETCH (]\r\n'); ctx.ok('NOOP completed'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); // The malformed line is logged & skipped; NOOP still resolves await client.noop(); test.ok(true); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: reader yields to event loop on many untagged responses'] = async test => { let server = createServer({ handlers: { STORE(ctx) { // 12 untagged FETCH responses + OK in a SINGLE write. STORE's FETCH // responses go through the (non-backpressured) global handler, so the // reader drains all >10 items in one pass and hits the periodic yield. let out = ''; for (let i = 1; i <= 12; i++) { out += `* ${i} FETCH (FLAGS (\\Seen))\r\n`; } out += `${ctx.tag} OK STORE completed\r\n`; ctx.write(out); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.mailboxOpen('INBOX'); let ok = await client.messageFlagsAdd('1:12', ['\\Seen']); test.ok(ok); await client.logout(); client.close(); server.close(); test.done(); }; module.exports['Server: partial FETCH NO is treated as success'] = async test => { let server = createServer({ handlers: { FETCH(ctx) { ctx.write('* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n'); ctx.no('Some of the requested messages no longer exist'); } } }); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); await client.mailboxOpen('INBOX'); // The NO with this specific text is treated as success rather than rejecting let msg = await client.fetchOne('1', { uid: true }); test.ok(msg); test.equal(msg.uid, 1); await client.logout(); client.close(); server.close(); test.done(); }; // NB: the socket-timeout -> NOOP/IDLE recovery handler is covered deterministically // in imap-flow-coverage-test.js (the _socketTimeout tests) rather than via a flaky // real-clock socket timeout here. module.exports['Server: greeting timeout rejects connect'] = async test => { // Server accepts the socket but never sends a greeting let server = net.createServer(socket => { socket.on('error', () => {}); // intentionally silent }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); let port = server.address().port; let client = makeClient(port, { greetingTimeout: 100 }); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err); test.equal(err.code, 'GREETING_TIMEOUT'); client.close(); server.close(); test.done(); }; module.exports['Server: connection timeout rejects connect'] = async test => { // 192.0.2.0/24 (TEST-NET-1, RFC 5737) is reserved and never answers, so the TCP // connect hangs until the (very short) connection timeout fires. let client = makeClient(9, { host: '192.0.2.1', connectionTimeout: 120 }); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected'); test.equal(err.code, 'CONNECT_TIMEOUT'); client.close(); test.done(); }; module.exports['Server: proxy connection failure rejects connect'] = async test => { let client = makeClient(1, { // point at a port with nothing listening so the proxy setup fails proxy: 'socks://127.0.0.1:1' }); client.on('error', () => {}); let err = null; try { await client.connect(); } catch (e) { err = e; } test.ok(err, 'connect rejected on proxy failure'); client.close(); test.done(); }; module.exports['Server: socket close triggers close handling'] = async test => { let server = createServer(); let port = await listen(server); let client = makeClient(port); client.on('error', () => {}); await client.connect(); let closed = false; client.on('close', () => { closed = true; }); // Drop the server side server.close(); client.socket.destroy(); await new Promise(r => setTimeout(r, 60)); test.ok(closed || client.isClosed, 'connection closed'); client.close(); test.done(); };