@mysql/xdevapi
Version:
MySQL Connector/Node.js - A Node.js driver for MySQL using the X Protocol and X DevAPI.
1,133 lines (924 loc) • 74.5 kB
JavaScript
/*
* Copyright (c) 2021, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0, as
* published by the Free Software Foundation.
*
* This program is also distributed with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an
* additional permission to link the program and your derivative works
* with the separately licensed software that they have included with
* MySQL.
*
* Without limiting anything contained in the foregoing, this file,
* which is part of MySQL Connector/Node.js, is also subject to the
* Universal FOSS Exception, version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
'use strict';
/* eslint-env node, mocha */
const PassThrough = require('stream').PassThrough;
const errors = require('../../../lib/constants/errors');
const warnings = require('../../../lib/constants/warnings');
const expect = require('chai').expect;
const td = require('testdouble');
const util = require('util');
// subject under test needs to be reloaded with replacement fakes
let connection = require('../../../lib/DevAPI/Connection');
describe('X DevAPI Connection', () => {
afterEach('reset fakes', () => {
td.reset();
});
context('allowsAuthenticationWith()', () => {
it('checks if the connection supports authentication with a given mechanism', () => {
// PLAIN is always supported
/* eslint-disable no-unused-expressions */
expect(connection().allowsAuthenticationWith('PLAIN')).to.be.true;
expect(connection().allowsAuthenticationWith('foo')).to.be.false;
/* eslint-enable no-unused-expressions */
return expect(connection().addCapabilities({ 'authentication.mechanisms': 'foo' }).allowsAuthenticationWith('foo')).to.be.true;
});
});
context('allowsAuthenticationRetry()', () => {
it('allows to retry the authentication strategy fallback supported by the server if the connection does not use a custom authentication mechanism and is not secure', () => {
const con = connection({ tls: { enabled: false } });
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const hasCustomAuthenticationMechanism = td.replace(con, 'hasCustomAuthenticationMechanism');
td.when(hasCustomAuthenticationMechanism()).thenReturn(false);
td.when(allowsAuthenticationWith('SHA256_MEMORY')).thenReturn(true);
return expect(con.allowsAuthenticationRetry()).to.be.true;
});
it('does not allow to retry the authentication strategy if the connection uses a custom authentication mechanism', () => {
const con = connection();
const hasCustomAuthenticationMechanism = td.replace(con, 'hasCustomAuthenticationMechanism');
td.when(hasCustomAuthenticationMechanism()).thenReturn(true);
return expect(con.allowsAuthenticationRetry()).to.be.false;
});
it('does not allow to retry the authentication strategy if the connection is secure', () => {
/* eslint-disable no-unused-expressions */
expect(connection({ tls: { enabled: true } }).allowsAuthenticationRetry()).to.be.false;
expect(connection({ ssl: true }).allowsAuthenticationRetry()).to.be.false;
/* eslint-enable no-unused-expressions */
return expect(connection({ socket: '/path/to/file.sock' }).allowsAuthenticationRetry()).to.be.false;
});
it('does not allow to retry the authentication strategy if the fallback is not supported by the server', () => {
const con = connection();
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const hasCustomAuthenticationMechanism = td.replace(con, 'hasCustomAuthenticationMechanism');
td.when(hasCustomAuthenticationMechanism()).thenReturn(false);
td.when(allowsAuthenticationWith('SHA256_MEMORY')).thenReturn(false);
return expect(con.allowsAuthenticationRetry()).to.be.false;
});
});
context('authenticate()', () => {
it('negotiates the proper authentication mechanism for the connection user', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationWith('foo')).thenReturn(true);
td.when(authenticateWith('foo')).thenResolve('bar');
return con.authenticate()
.then(res => {
return expect(res).to.equal('bar');
});
});
it('retries to authenticate using SHA256_MEMORY if possible', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationRetry = td.replace(con, 'allowsAuthenticationRetry');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
const error = new Error();
error.info = { code: errors.ER_ACCESS_DENIED_ERROR };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationRetry()).thenReturn(true);
td.when(allowsAuthenticationWith(), { ignoreExtraArgs: true }).thenReturn(true);
td.when(authenticateWith('foo')).thenReject(error);
td.when(authenticateWith('SHA256_MEMORY')).thenResolve('bar');
return con.authenticate()
.then(res => {
return expect(res).to.equal('bar');
});
});
it('fails if the application provides a custom authentication mechanism that is not supported by the server', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationWith('foo')).thenReturn(false);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err.message).to.equal(util.format(errors.MESSAGES.ER_DEVAPI_AUTH_UNSUPPORTED_SERVER, 'foo'));
});
});
it('fails if there is an unexpected error while authenticating the user with a custom authentication mechanism', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
const unexpectedError = new Error();
unexpectedError.info = { code: -1 };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationWith('foo')).thenReturn(true);
td.when(authenticateWith('foo')).thenReject(unexpectedError);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(unexpectedError);
});
});
it('fails if the selected authentication mechanism does not work and it cannot retry using SHA256_MEMORY', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationRetry = td.replace(con, 'allowsAuthenticationRetry');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
const error = new Error();
error.info = { code: errors.ER_ACCESS_DENIED_ERROR };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationRetry()).thenReturn(false);
td.when(allowsAuthenticationWith(), { ignoreExtraArgs: true }).thenReturn(true);
td.when(authenticateWith('foo')).thenReject(error);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
it('fails if the selected authentication mechanism does not work and the server does not support SHA256_MEMORY', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationRetry = td.replace(con, 'allowsAuthenticationRetry');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
const authenticationError = new Error();
authenticationError.info = { code: errors.ER_ACCESS_DENIED_ERROR };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationRetry()).thenReturn(true);
td.when(allowsAuthenticationWith(), { ignoreExtraArgs: true }).thenReturn(false);
td.when(authenticateWith('foo')).thenReject(authenticationError);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err.message).to.equal(util.format(errors.MESSAGES.ER_DEVAPI_AUTH_UNSUPPORTED_SERVER, 'foo'));
});
});
it('fails if there is an unexpected error while authenticating the user with SHA256_MEMORY', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationRetry = td.replace(con, 'allowsAuthenticationRetry');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const authenticateWith = td.replace(con, 'authenticateWith');
const authenticationError = new Error();
authenticationError.info = { code: errors.ER_ACCESS_DENIED_ERROR };
const unexpectedError = new Error();
unexpectedError.info = { code: -1 };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationRetry()).thenReturn(true);
td.when(allowsAuthenticationWith(), { ignoreExtraArgs: true }).thenReturn(true);
td.when(authenticateWith('foo')).thenReject(authenticationError);
td.when(authenticateWith('SHA256_MEMORY')).thenReject(unexpectedError);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(unexpectedError);
});
});
it('fails if the user cannot be ultimately authenticated with SHA256_MEMORY', () => {
const con = connection();
const getAuth = td.replace(con, 'getAuth');
const allowsAuthenticationWith = td.replace(con, 'allowsAuthenticationWith');
const allowsAuthenticationRetry = td.replace(con, 'allowsAuthenticationRetry');
const authenticateWith = td.replace(con, 'authenticateWith');
const error = new Error();
error.info = { code: errors.ER_ACCESS_DENIED_ERROR };
td.when(getAuth()).thenReturn('foo');
td.when(allowsAuthenticationRetry()).thenReturn(true);
td.when(allowsAuthenticationWith(), { ignoreExtraArgs: true }).thenReturn(true);
td.when(authenticateWith(), { ignoreExtraArgs: true }).thenReject(error);
return con.authenticate()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err.message).to.equal(errors.MESSAGES.ER_DEVAPI_AUTH_MORE_INFO);
});
});
});
context('authenticateWith()', () => {
let authenticationManager;
beforeEach('create fakes', () => {
authenticationManager = td.replace('../../../lib/Authentication/AuthenticationManager');
connection = require('../../../lib/DevAPI/Connection');
});
it('authenticates the connection user with a given authentication mechanism', () => {
const client = 'foo';
const connectionId = 'bar';
const mechanism = 'baz';
const password = 'qux';
const schema = 'quux';
const user = 'quuz';
const con = connection({ password, schema }).setClient(client);
const getUser = td.replace(con, 'getUser');
const plugin = td.function();
const run = td.function();
td.when(getUser()).thenReturn(user);
td.when(authenticationManager.getPlugin(mechanism)).thenReturn(plugin);
td.when(plugin({ password, schema, user })).thenReturn({ run });
td.when(run(client)).thenResolve({ connectionId });
return con.authenticateWith(mechanism)
.then(con => {
expect(con.getAuth()).to.equal(mechanism);
return expect(con.getServerId()).to.equal(connectionId);
});
});
// TODO(Rui): Remove after the deprecation period.
it('authenticates the connection user using the deprecated password property', () => {
const client = 'foo';
const connectionId = 'bar';
const mechanism = 'baz';
const user = 'qux';
const con = connection({ dbPassword: 'quux' }).setClient(client);
const getUser = td.replace(con, 'getUser');
const plugin = td.function();
const run = td.function();
td.when(getUser()).thenReturn(user);
td.when(authenticationManager.getPlugin(mechanism)).thenReturn(plugin);
td.when(plugin({ password: 'quux', schema: undefined, user })).thenReturn({ run });
td.when(run(client)).thenResolve({ connectionId });
return con.authenticateWith(mechanism)
.then(con => {
expect(con.getAuth()).to.equal(mechanism);
return expect(con.getServerId()).to.equal(connectionId);
});
});
it('authenticates the connection with a passwordless user', () => {
const client = 'foo';
const connectionId = 'bar';
const mechanism = 'baz';
const user = 'qux';
const con = connection().setClient(client);
const getUser = td.replace(con, 'getUser');
const plugin = td.function();
const run = td.function();
td.when(getUser()).thenReturn(user);
td.when(authenticationManager.getPlugin(mechanism)).thenReturn(plugin);
td.when(plugin({ password: '', schema: undefined, user })).thenReturn({ run });
td.when(run(client)).thenResolve({ connectionId });
return con.authenticateWith(mechanism)
.then(con => {
expect(con.getAuth()).to.equal(mechanism);
return expect(con.getServerId()).to.equal(connectionId);
});
});
it('fails if the user cannot be authenticated', () => {
const client = 'foo';
const mechanism = 'baz';
const user = 'qux';
const con = connection().setClient(client);
const getUser = td.replace(con, 'getUser');
const plugin = td.function();
const run = td.function();
const error = new Error('foobar');
td.when(getUser()).thenReturn(user);
td.when(authenticationManager.getPlugin(mechanism)).thenReturn(plugin);
td.when(plugin({ password: '', schema: undefined, user })).thenReturn({ run });
td.when(run(client)).thenReject(error);
return con.authenticateWith(mechanism)
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
});
context('capabilitiesGet()', () => {
let Client;
beforeEach('create fakes', () => {
Client = td.replace('../../../lib/Protocol/Client');
connection = require('../../../lib/DevAPI/Connection');
});
it('returns the set of connection capabilities accepted by the server', () => {
const client = new Client();
td.when(Client.prototype.capabilitiesGet()).thenResolve('foo');
return connection().setClient(client).capabilitiesGet()
.then(res => {
return expect(res).to.equal('foo');
});
});
it('fails if the X Protocol client instance reports an error', () => {
const client = new Client();
const error = new Error('foobar');
td.when(Client.prototype.capabilitiesGet()).thenReject(error);
return connection().setClient(client).capabilitiesGet()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
});
context('capabilitiesSet()', () => {
let Client;
beforeEach('create fakes', () => {
Client = td.replace('../../../lib/Protocol/Client');
connection = require('../../../lib/DevAPI/Connection');
});
it('enables TLS support in the server for connections using TLS', () => {
const con = connection({ endpoints: [{ host: 'foo' }], tls: { enabled: true } }).setClient(new Client());
td.when(Client.prototype.capabilitiesSet(td.matchers.contains({ tls: true }))).thenResolve();
return con.capabilitiesSet()
.then(res => {
return expect(res).to.deep.include({ tls: true });
});
});
it('does not enable TLS support in the server for connections using a local Unix socket', () => {
const capabilities = { tls: true };
const con = connection({ endpoints: [{ socket: 'foo' }] }).setClient(new Client());
td.when(Client.prototype.capabilitiesSet(td.matchers.argThat(capabilities => Object.keys(capabilities).indexOf('tls') === -1))).thenResolve();
return con.capabilitiesSet()
.then(res => {
return expect(res).to.not.deep.include(capabilities);
});
});
it('sends the default set of client session attributes when the application does not provide any', () => {
const clientAttributes = connection.CLIENT_SESSION_ATTRIBUTES;
const capabilities = { session_connect_attrs: clientAttributes };
const con = connection().setClient(new Client());
td.when(Client.prototype.capabilitiesSet(td.matchers.contains(capabilities))).thenResolve();
return con.capabilitiesSet()
.then(res => {
return expect(res).to.deep.include(capabilities);
});
});
it('sends the stringified version of any custom attribute provided by the application', () => {
const applicationAttributes = { foo: 'bar', baz: 10, qux: ['quux', 'quuz'], corge: { grault: 'garply' } };
const clientAttributes = connection.CLIENT_SESSION_ATTRIBUTES;
const capabilities = { session_connect_attrs: Object.assign({}, clientAttributes, { foo: 'bar', baz: '10', qux: '["quux","quuz"]', corge: '{"grault":"garply"}' }) };
const con = connection({ connectionAttributes: applicationAttributes }).setClient(new Client());
td.when(Client.prototype.capabilitiesSet(td.matchers.contains(capabilities))).thenResolve();
return con.capabilitiesSet()
.then(res => {
return expect(res).to.deep.include(capabilities);
});
});
it('does not send session attributes when they are explicitely disabled by the application', () => {
const capability = 'session_connect_attrs';
const con = connection({ connectionAttributes: false }).setClient(new Client());
td.when(Client.prototype.capabilitiesSet(td.matchers.argThat(capabilities => Object.keys(capabilities).indexOf(capability) === -1))).thenResolve();
return con.capabilitiesSet()
.then(res => {
return expect(res).to.not.deep.include.keys(capability);
});
});
it('destroys and re-creates the connection without capabilities the server does not know', () => {
const capability = 'foo';
const destroy = td.function();
const con = connection({ connectionAttributes: false }).setClient(new Client());
const unknownCapabilityError = new Error(`Capability '${capability}' doesn't exist`);
unknownCapabilityError.info = { code: errors.ER_X_CAPABILITY_NOT_FOUND };
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true }).thenReject(unknownCapabilityError);
td.when(Client.prototype.getConnection()).thenReturn({ destroy });
return con.capabilitiesSet()
.then(() => {
return expect.fail();
})
.catch(() => {
expect(td.explain(destroy).callCount).to.equal(1);
expect(td.explain(destroy).calls[0].args).to.deep.equal([unknownCapabilityError]);
// eslint-disable-next-line no-unused-expressions
expect(con.isReconnecting()).to.be.true;
return expect(con.getUnknownCapabilities()).to.deep.equal(['foo']);
});
});
it('fails when the X Plugin does not allow to enable TLS', () => {
const tlsError = new Error("Capability prepare failed for 'tls'");
tlsError.info = { code: errors.ER_X_CAPABILITIES_PREPARE_FAILED };
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true }).thenReject(tlsError);
return connection().setClient(new Client()).capabilitiesSet()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err.message).to.equal(errors.MESSAGES.ER_DEVAPI_NO_SERVER_TLS);
});
});
it('fails when the X Plugin fails to prepare a capability not related to TLS', () => {
const message = "Capability prepare failed for 'foo'";
const capabilityPrepareError = new Error(message);
capabilityPrepareError.info = { code: errors.ER_X_CAPABILITIES_PREPARE_FAILED };
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true }).thenReject(capabilityPrepareError);
return connection().setClient(new Client()).capabilitiesSet()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err.message).to.equal(message);
});
});
it('fails if the X Protocol client instance reports an unexpected error', () => {
const unexpectedError = new Error('foobar');
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true }).thenReject(unexpectedError);
return connection().setClient(new Client()).capabilitiesSet()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(unexpectedError);
});
});
});
context('close()', () => {
it('closes the X Protocol connection and the underlying connection socket', () => {
const con = connection();
const destroy = td.replace(con, 'destroy');
td.when(destroy()).thenResolve('foo');
return con.close()
.then(res => {
return expect(res).to.equal('foo');
});
});
});
context('connect()', () => {
let Client, socket, net;
beforeEach('create fakes', () => {
socket = new PassThrough();
// Faking the timeout management makes the tests a lot more simple.
socket.setTimeout = td.function();
Client = td.replace('../../../lib/Protocol/Client');
net = td.replace('net');
connection = require('../../../lib/DevAPI/Connection');
});
it('connects to the default endpoint when none is specified and it is available', () => {
const con = connection();
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'localhost', port: 33060, path: undefined })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve('foo');
return con.connect()
.then(res => {
return expect(res).to.equal('foo');
});
});
it('creates a X Protocol client instance when the socket is established', () => {
const con = connection();
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'localhost', port: 33060, path: undefined })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve('foo');
return con.connect()
.then(() => {
expect(td.explain(Client).callCount).to.equal(1);
return expect(td.explain(Client).calls[0].args).to.deep.equal([socket]);
});
});
it('manages a the default connection timeout until the socket is not open', () => {
const con = connection();
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'localhost', port: 33060, path: undefined })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve();
return con.connect()
.then(() => {
expect(td.explain(socket.setTimeout).callCount).to.equal(2);
expect(td.explain(socket.setTimeout).calls[0].args).to.deep.equal([10000]);
expect(td.explain(socket.setTimeout).calls[1].args).to.deep.equal([0]);
});
});
it('manages a custom connection timeout until the socket is not open', () => {
const con = connection({ connectTimeout: 500 });
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'localhost', port: 33060, path: undefined })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve();
return con.connect()
.then(() => {
expect(td.explain(socket.setTimeout).callCount).to.equal(2);
expect(td.explain(socket.setTimeout).calls[0].args).to.deep.equal([500]);
expect(td.explain(socket.setTimeout).calls[1].args).to.deep.equal([0]);
});
});
context('when only one endpoint is provided', () => {
it('connects to that endpoint using its address if it is available', () => {
const con = connection({ host: 'foo', port: 'bar' });
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'foo', port: 'bar', path: undefined })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve('baz');
return con.connect()
.then(res => {
return expect(res).to.equal('baz');
});
});
it('connects to that endpoint using a local Unix socket if it is available', () => {
const con = connection({ host: 'foo', port: 'bar', socket: 'baz' });
const start = td.replace(con, 'start');
td.when(net.connect({ host: 'foo', port: 'bar', path: 'baz' })).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenResolve('qux');
return con.connect()
.then(res => {
return expect(res).to.equal('qux');
});
});
it('fails when the endpoint is not available', () => {
const con = connection({ host: 'foo' }).setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
const hasMultipleEndpoints = td.replace(con, 'hasMultipleEndpoints');
const hasMoreEndpointsAvailable = td.replace(con, 'hasMoreEndpointsAvailable');
const error = new Error('foobar');
td.when(net.connect(), { ignoreExtraArgs: true, times: 1 }).thenDo(() => {
setTimeout(() => {
socket.emit('error', error);
socket.emit('close', true);
});
return socket;
});
// The connection using the first socket should not be
// effectively open.
td.when(isOpen()).thenReturn(false);
// The connection is not configured with multiple endpoints.
td.when(hasMultipleEndpoints()).thenReturn(false);
// So, there are no other endpoints available.
td.when(hasMoreEndpointsAvailable()).thenReturn(false);
return con.connect()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
it('fails with a custom error when the connection timeout is exceeded', () => {
const connectTimeout = 500;
const con = connection({ connectTimeout });
const hasMultipleEndpoints = td.replace(con, 'hasMultipleEndpoints');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => socket.emit('timeout'));
return socket;
});
td.when(hasMultipleEndpoints()).thenReturn(false);
return con.connect()
.catch(err => {
return expect(err.message).to.equal(util.format(errors.MESSAGES.ER_DEVAPI_CONNECTION_TIMEOUT, connectTimeout));
});
});
});
context('when multiple endpoints are provided', () => {
it('refurbishes a socket for connecting to the next endpoint if the connection failed to the first', () => {
const refurbishedSocket = new PassThrough();
// Although we do not need it, we should fake the "setTimeout"
// method to avoid errors.
refurbishedSocket.setTimeout = td.function();
const con = connection({ endpoints: [{ host: 'foo', port: 'bar' }, { host: 'baz', port: 'qux' }] }).setClient(new Client());
const start = td.replace(con, 'start');
const isOpen = td.replace(con, 'isOpen');
const hasMoreEndpointsAvailable = td.replace(con, 'hasMoreEndpointsAvailable');
// The connection using the refurbished socket should be
// effectively open.
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
td.when(isOpen()).thenReturn(true);
setTimeout(() => refurbishedSocket.emit('ready'));
return refurbishedSocket;
});
// The connection using the first socket should not be
// effectively open.
td.when(net.connect(), { ignoreExtraArgs: true, times: 1 }).thenDo(() => {
td.when(isOpen()).thenReturn(false);
setTimeout(() => socket.emit('close', true));
return socket;
});
td.when(start()).thenResolve();
// The connection will be re-created to the next endpoint, so it
// should be available.
td.when(hasMoreEndpointsAvailable()).thenReturn(true);
return con.connect()
.then(() => {
expect(td.explain(net.connect).callCount).to.equal(2);
expect(td.explain(net.connect).calls[0].args).to.deep.equal([{ host: 'foo', port: 'bar', path: undefined }]);
return expect(td.explain(net.connect).calls[1].args).to.deep.equal([{ host: 'baz', port: 'qux', path: undefined }]);
});
});
it('fails when all endpoints are unavailable', () => {
const refurbishedSocket = new PassThrough();
// Although we do not need it, we should fake the "setTimeout"
// method to avoid errors.
refurbishedSocket.setTimeout = td.function();
const con = connection({ endpoints: [{ host: 'foo', port: 'bar' }, { host: 'baz', port: 'qux' }] }).setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
const hasMultipleEndpoints = td.replace(con, 'hasMultipleEndpoints');
const hasMoreEndpointsAvailable = td.replace(con, 'hasMoreEndpointsAvailable');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
// The connection using the refurbished socket should be
// effectively open.
td.when(isOpen()).thenReturn(true);
// No more endpoints are available.
td.when(hasMoreEndpointsAvailable()).thenReturn(false);
setTimeout(() => refurbishedSocket.emit('close', true));
return refurbishedSocket;
});
td.when(net.connect(), { ignoreExtraArgs: true, times: 1 }).thenDo(() => {
// The connection using the first socket should not be
// effectively open.
td.when(isOpen()).thenReturn(false);
// There is another endpoint available.
td.when(hasMoreEndpointsAvailable()).thenReturn(true);
setTimeout(() => socket.emit('close', true));
return socket;
});
// The connection is using multi-host.
td.when(hasMultipleEndpoints()).thenReturn(true);
return con.connect()
.then(() => {
return expect.fail();
})
.catch(err => {
expect(err.message).to.equal(errors.MESSAGES.ER_DEVAPI_MULTI_HOST_CONNECTION_FAILED);
});
});
it('fails with a custom error when the connection timeout is exceeded', () => {
const connectTimeout = 500;
const con = connection({ connectTimeout });
const hasMultipleEndpoints = td.replace(con, 'hasMultipleEndpoints');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => socket.emit('timeout'));
return socket;
});
td.when(hasMultipleEndpoints()).thenReturn(true);
return con.connect()
.catch(err => {
return expect(err.message).to.equal(util.format(errors.MESSAGES.ER_DEVAPI_MULTI_HOST_CONNECTION_TIMEOUT, connectTimeout));
});
});
});
it('fails when there is an error while creating the network socket', () => {
const error = new Error('foobar');
const con = connection();
const start = td.replace(con, 'start');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => {
socket.emit('ready');
socket.emit('error', error);
});
return socket;
});
td.when(start()).thenResolve();
return con.connect()
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
it('destroys the network socket and resets the state when there is a connection error', () => {
const con = connection();
const start = td.replace(con, 'start');
const reset = td.replace(con, 'reset');
const error = new Error('foobar');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => socket.emit('ready'));
return socket;
});
td.when(start()).thenReject(error);
return con.connect()
.then(() => {
return expect.fail();
})
.catch(err => {
expect(td.explain(reset).callCount).to.equal(1);
return expect(err).to.deep.equal(error);
});
});
it('delegates message handling to the X Protocol client instance', () => {
const con = connection().setClient(new Client());
const start = td.replace(con, 'start');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => {
socket.emit('ready');
socket.emit('data', 'foo');
socket.emit('data', 'bar');
});
return socket;
});
td.when(start()).thenResolve();
return con.connect()
.then(() => {
expect(td.explain(Client.prototype.handleNetworkFragment).callCount).to.equal(2);
expect(td.explain(Client.prototype.handleNetworkFragment).calls[0].args).to.deep.equal(['foo']);
expect(td.explain(Client.prototype.handleNetworkFragment).calls[1].args).to.deep.equal(['bar']);
});
});
it('notifies the X Protocol client instance if the network socket is abruptely closed by the server', () => {
const con = connection().setClient(new Client());
const isActive = td.replace(con, 'isActive');
const isOpen = td.replace(con, 'isOpen');
const start = td.replace(con, 'start');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => {
socket.emit('ready');
socket.emit('close');
});
return socket;
});
td.when(start()).thenResolve();
td.when(isOpen()).thenReturn(true);
td.when(isActive()).thenReturn(true);
return con.connect()
.then(() => {
expect(td.explain(Client.prototype.handleServerClose).callCount).to.equal(1);
});
});
it('resets the connection state when it is closed by mutual agreement', () => {
const con = connection().setClient(new Client());
const isActive = td.replace(con, 'isActive');
const isOpen = td.replace(con, 'isOpen');
const reset = td.replace(con, 'reset');
const start = td.replace(con, 'start');
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
setTimeout(() => {
socket.emit('ready');
socket.emit('close');
});
return socket;
});
td.when(start()).thenResolve();
td.when(isOpen()).thenReturn(true);
td.when(isActive()).thenReturn(false);
return con.connect()
.then(() => {
expect(td.explain(reset).callCount).to.equal(1);
});
});
it('refurbishes a socket for connecting to the same endpoint if the connection was closed by the server with a non-fatal error', () => {
const refurbishedSocket = new PassThrough();
// Although we do not need it, we should fake the "setTimeout"
// method to avoid errors.
refurbishedSocket.setTimeout = td.function();
const con = connection({ host: 'foo', port: 'bar' }).setClient(new Client());
// We want Connection.start() to be partly completed, so we can
// only fake Connection.authenticate(), which is the last stage
// in the pipeline to create a server session.
const authenticate = td.replace(con, 'authenticate');
const isOpen = td.replace(con, 'isOpen');
const hasMoreEndpointsAvailable = td.replace(con, 'hasMoreEndpointsAvailable');
// example of a non-fatal error
const nonFatalError = new Error("Capability 'foo' doesn't exist");
nonFatalError.info = { code: errors.ER_X_CAPABILITY_NOT_FOUND };
// The connection using the refurbished socket should be
// effectively open.
td.when(net.connect(), { ignoreExtraArgs: true }).thenDo(() => {
td.when(isOpen()).thenReturn(true);
setTimeout(() => refurbishedSocket.emit('ready'));
return refurbishedSocket;
});
// The connection using the first socket should not be
// effectively open.
td.when(net.connect(), { ignoreExtraArgs: true, times: 1 }).thenDo(() => {
td.when(isOpen()).thenReturn(false);
setTimeout(() => socket.emit('ready'));
return socket;
});
// The second call to capabilitiesSet() should resolve to an empty
// object in order to avoid enabling TLS.
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true }).thenResolve({});
// The first call to capabilitiesSet() should fail with a
// non-fatal error.
td.when(Client.prototype.capabilitiesSet(), { ignoreExtraArgs: true, times: 1 }).thenReject(nonFatalError);
// capabilitiesGet should always work (does not matter for this test).
td.when(Client.prototype.capabilitiesGet()).thenResolve();
// The client should have the up-to-date underlying network socket.
td.when(Client.prototype.getConnection()).thenReturn(refurbishedSocket);
td.when(Client.prototype.getConnection(), { times: 1 }).thenReturn(socket);
// The connection will be re-created to the same endpoint, so it
// should be available.
td.when(hasMoreEndpointsAvailable()).thenReturn(true);
td.when(authenticate()).thenResolve();
return con.connect()
.then(() => {
expect(td.explain(net.connect).callCount).to.equal(2);
expect(td.explain(net.connect).calls[0].args).to.deep.equal([{ host: 'foo', port: 'bar', path: undefined }]);
return expect(td.explain(net.connect).calls[1].args).to.deep.equal([{ host: 'foo', port: 'bar', path: undefined }]);
});
});
});
context('destroy()', () => {
let Client;
beforeEach('create fakes', () => {
Client = td.replace('../../../lib/Protocol/Client');
connection = require('../../../lib/DevAPI/Connection');
});
it('gracefully closes the underlying X Protocol connection', () => {
const con = connection().setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
const destroy = td.function();
td.when(isOpen()).thenReturn(true);
td.when(Client.prototype.connectionClose()).thenResolve();
td.when(Client.prototype.getConnection()).thenReturn({ destroy, destroyed: false });
return con.destroy()
.then(() => {
expect(td.explain(Client.prototype.connectionClose).callCount).to.equal(1);
expect(td.explain(destroy).callCount).to.equal(1);
// eslint-disable-next-line no-unused-expressions
expect(con.isClosing()).to.be.false;
return expect(td.explain(destroy).calls[0].args).to.be.an('array').and.be.empty;
});
});
it('does nothing if the underlying connection is closed', () => {
const con = connection().setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
td.when(isOpen()).thenReturn(false);
return con.destroy()
.then(() => {
return expect(td.explain(Client.prototype.connectionClose).callCount).to.equal(0);
});
});
it('does nothing if the network socket is destroyed', () => {
const con = connection().setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
td.when(isOpen()).thenReturn(true);
td.when(Client.prototype.getConnection()).thenReturn({ destroyed: true });
return Promise.all([con.destroy(), con.destroy()])
.then(() => {
expect(td.explain(Client.prototype.connectionClose).callCount).to.equal(0);
})
.then(() => {
return con.destroy();
})
.then(() => {
return con.destroy();
})
.then(() => {
expect(td.explain(Client.prototype.connectionClose).callCount).to.equal(0);
});
});
it('only closes the connection once', () => {
const con = connection().setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
const destroy = td.function();
td.when(isOpen()).thenReturn(true);
td.when(Client.prototype.connectionClose()).thenResolve();
td.when(Client.prototype.getConnection()).thenReturn({ destroy });
return Promise.all([con.destroy(), con.destroy()])
.then(() => {
expect(td.explain(Client.prototype.connectionClose).callCount).to.equal(1);
return expect(td.explain(destroy).callCount).to.equal(1);
});
});
it('fails if the X Protocol client instance reports an error', () => {
const con = connection().setClient(new Client());
const isOpen = td.replace(con, 'isOpen');
const error = new Error('foobar');
td.when(isOpen()).thenReturn(true);
td.when(Client.prototype.connectionClose()).thenReject(error);
td.when(Client.prototype.getConnection()).thenReturn({ destroyed: false });
return con.destroy()
.then(() => {
return expect.fail();
})
.catch(err => {
return expect(err).to.deep.equal(error);
});
});
});
context('enableTLS()', () => {
let Client, secureContext, socket, tls;
beforeEach('create fakes', () => {
secureContext = td.function();
socket = new PassThrough();