Files
xahau.js/test/connection-test.js
Elliot Lee d82703f41b prepareTransaction should not overwrite Sequence (#990)
* Cleans up some code and fixes some type errors

* Clarify how null settings work

* Document updated RippledError

* Updates per review by @mDuo13
2019-03-18 15:55:42 -07:00

476 lines
14 KiB
JavaScript

'use strict'; // eslint-disable-line
/* eslint-disable max-nested-callbacks */
const _ = require('lodash');
const net = require('net');
const assert = require('assert-diff');
const setupAPI = require('./setup-api');
const RippleAPI = require('ripple-api').RippleAPI;
const utils = RippleAPI._PRIVATE.ledgerUtils;
const ledgerClose = require('./fixtures/rippled/ledger-close.json');
const TIMEOUT = 200000; // how long before each test case times out
function unused() {
}
function createServer() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.on('listening', function() {
resolve(server);
});
server.on('error', function(error) {
reject(error);
});
server.listen(0, '0.0.0.0');
});
}
describe('Connection', function() {
this.timeout(TIMEOUT);
beforeEach(setupAPI.setup);
afterEach(setupAPI.teardown);
it('default options', function() {
const connection = new utils.common.Connection('url');
assert.strictEqual(connection._url, 'url');
assert(_.isUndefined(connection._proxyURL));
assert(_.isUndefined(connection._authorization));
});
it('trace', function() {
const connection = new utils.common.Connection('url', {trace: true});
const message1 = '{"type": "transaction"}';
const message2 = '{"type": "path_find"}';
const messages = [];
connection._console = {
log: function(message) {
messages.push(message);
}
};
connection._ws = {
send: function() {}
};
connection._onMessage(message1);
connection._send(message2);
assert.deepEqual(messages, [message1, message2]);
});
it('with proxy', function(done) {
if (process.browser) {
done();
return;
}
createServer().then(server => {
const port = server.address().port;
const expect = 'CONNECT localhost';
server.on('connection', socket => {
socket.on('data', data => {
const got = data.toString('ascii', 0, expect.length);
assert.strictEqual(got, expect);
server.close();
done();
});
});
const options = {
proxy: 'ws://localhost:' + port,
authorization: 'authorization',
trustedCertificates: ['path/to/pem']
};
const connection =
new utils.common.Connection(this.api.connection._url, options);
connection.connect().catch(done);
connection.connect().catch(done);
}, done);
});
it('Multiply disconnect calls', function() {
this.api.disconnect();
return this.api.disconnect();
});
it('reconnect', function() {
return this.api.connection.reconnect();
});
it('NotConnectedError', function() {
const connection = new utils.common.Connection('url');
return connection.getLedgerVersion().then(() => {
assert(false, 'Should throw NotConnectedError');
}).catch(error => {
assert(error instanceof this.api.errors.NotConnectedError);
});
});
it('should throw NotConnectedError if server not responding ', function(
done
) {
if (process.browser) {
const phantomTest = /PhantomJS/;
if (phantomTest.test(navigator.userAgent)) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done();
return;
}
}
// Address where no one listens
const connection =
new utils.common.Connection('ws://testripple.circleci.com:129');
connection.on('error', done);
connection.connect().catch(error => {
assert(error instanceof this.api.errors.NotConnectedError);
done();
});
});
it('DisconnectedError', function() {
this.api.connection._send(JSON.stringify({
command: 'config',
data: {disconnectOnServerInfo: true}
}));
return this.api.getServerInfo().then(() => {
assert(false, 'Should throw DisconnectedError');
}).catch(error => {
assert(error instanceof this.api.errors.DisconnectedError);
});
});
it('TimeoutError', function() {
this.api.connection._send = function() {
return Promise.resolve({});
};
const request = {command: 'server_info'};
return this.api.connection.request(request, 1).then(() => {
assert(false, 'Should throw TimeoutError');
}).catch(error => {
assert(error instanceof this.api.errors.TimeoutError);
});
});
it('DisconnectedError on send', function() {
this.api.connection._ws.send = function(message, options, callback) {
unused(message, options);
callback({message: 'not connected'});
};
return this.api.getServerInfo().then(() => {
assert(false, 'Should throw DisconnectedError');
}).catch(error => {
assert(error instanceof this.api.errors.DisconnectedError);
assert.strictEqual(error.message, 'not connected');
});
});
it('ResponseFormatError', function() {
this.api.connection._send = function(message) {
const parsed = JSON.parse(message);
setTimeout(() => {
this._ws.emit('message', JSON.stringify({
id: parsed.id,
type: 'response',
status: 'unrecognized'
}));
}, 2);
return new Promise(() => {});
};
return this.api.getServerInfo().then(() => {
assert(false, 'Should throw ResponseFormatError');
}).catch(error => {
assert(error instanceof this.api.errors.ResponseFormatError);
});
});
it('reconnect on unexpected close ', function(done) {
this.api.connection.on('connected', () => {
done();
});
setTimeout(() => {
this.api.connection._ws.close();
}, 1);
});
describe('reconnection test', function() {
beforeEach(function() {
this.api.connection.__workingUrl = this.api.connection._url;
this.api.connection.__doReturnBad = function() {
this._url = this.__badUrl;
const self = this;
function onReconnect(num) {
if (num >= 2) {
self._url = self.__workingUrl;
self.removeListener('reconnecting', onReconnect);
}
}
this.on('reconnecting', onReconnect);
};
});
afterEach(function() {
});
it('reconnect on several unexpected close', function(done) {
if (process.browser) {
const phantomTest = /PhantomJS/;
if (phantomTest.test(navigator.userAgent)) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done();
return;
}
}
this.timeout(70001);
const self = this;
self.api.connection.__badUrl = 'ws://testripple.circleci.com:129';
function breakConnection() {
self.api.connection.__doReturnBad();
self.api.connection._send(JSON.stringify({
command: 'test_command',
data: {disconnectIn: 10}
}));
}
let connectsCount = 0;
let disconnectsCount = 0;
let reconnectsCount = 0;
let code = 0;
this.api.connection.on('reconnecting', () => {
reconnectsCount += 1;
});
this.api.connection.on('disconnected', _code => {
code = _code;
disconnectsCount += 1;
});
const num = 3;
this.api.connection.on('connected', () => {
connectsCount += 1;
if (connectsCount < num) {
breakConnection();
}
if (connectsCount === num) {
if (disconnectsCount !== num) {
done(new Error('disconnectsCount must be equal to ' + num +
'(got ' + disconnectsCount + ' instead)'));
} else if (reconnectsCount !== num * 2) {
done(new Error('reconnectsCount must be equal to ' + num * 2 +
' (got ' + reconnectsCount + ' instead)'));
} else if (code !== 1006) {
done(new Error('disconnect must send code 1006 (got ' + code +
' instead)'));
} else {
done();
}
}
});
breakConnection();
});
});
it('should emit disconnected event with code 1000 (CLOSE_NORMAL)',
function(done
) {
this.api.once('disconnected', code => {
assert.strictEqual(code, 1000);
done();
});
this.api.disconnect();
});
it('should emit disconnected event with code 1006 (CLOSE_ABNORMAL)',
function(done
) {
this.api.once('error', error => {
done(new Error('should not throw error, got ' + String(error)));
});
this.api.once('disconnected', code => {
assert.strictEqual(code, 1006);
done();
});
this.api.connection._send(JSON.stringify({
command: 'test_command',
data: {disconnectIn: 10}
}));
});
it('should emit connected event on after reconnect', function(done) {
this.api.once('connected', done);
this.api.connection._ws.close();
});
it('Multiply connect calls', function() {
return this.api.connect().then(() => {
return this.api.connect();
});
});
it('hasLedgerVersion', function() {
return this.api.connection.hasLedgerVersion(8819951).then(result => {
assert(result);
});
});
it('Cannot connect because no server', function() {
const connection = new utils.common.Connection();
return connection.connect().then(() => {
assert(false, 'Should throw ConnectionError');
}).catch(error => {
assert(error instanceof this.api.errors.ConnectionError);
});
});
it('connect multiserver error', function() {
const options = {
servers: ['wss://server1.com', 'wss://server2.com']
};
assert.throws(function() {
const api = new RippleAPI(options);
unused(api);
}, this.api.errors.RippleError);
});
it('connect throws error', function(done) {
this.api.once('error', (type, info) => {
assert.strictEqual(type, 'type');
assert.strictEqual(info, 'info');
done();
});
this.api.connection.emit('error', 'type', 'info');
});
it('emit stream messages', function(done) {
let transactionCount = 0;
let pathFindCount = 0;
this.api.connection.on('transaction', () => {
transactionCount++;
});
this.api.connection.on('path_find', () => {
pathFindCount++;
});
this.api.connection.on('1', () => {
assert.strictEqual(transactionCount, 1);
assert.strictEqual(pathFindCount, 1);
done();
});
this.api.connection._onMessage(JSON.stringify({
type: 'transaction'
}));
this.api.connection._onMessage(JSON.stringify({
type: 'path_find'
}));
this.api.connection._onMessage(JSON.stringify({
type: 'response', id: 1
}));
});
it('invalid message id', function(done) {
this.api.on('error', (errorCode, errorMessage, message) => {
assert.strictEqual(errorCode, 'badMessage');
assert.strictEqual(errorMessage, 'valid id not found in response');
assert.strictEqual(message,
'{"type":"response","id":"must be integer"}');
done();
});
this.api.connection._onMessage(JSON.stringify({
type: 'response', id: 'must be integer'
}));
});
it('propagates error message', function(done) {
this.api.on('error', (errorCode, errorMessage, data) => {
assert.strictEqual(errorCode, 'slowDown');
assert.strictEqual(errorMessage, 'slow down');
assert.deepEqual(data, {error: 'slowDown', error_message: 'slow down'});
done();
});
this.api.connection._onMessage(JSON.stringify({
error: 'slowDown', error_message: 'slow down'
}));
});
it('propagates RippledError data', function(done) {
this.api.request('subscribe', {streams: 'validations'}).catch(error => {
assert.strictEqual(error.name, 'RippledError')
assert.strictEqual(error.data.error, 'invalidParams')
assert.strictEqual(error.message, 'Invalid parameters.')
assert.strictEqual(error.data.error_code, 31)
assert.strictEqual(error.data.error_message, 'Invalid parameters.')
assert.deepEqual(error.data.request, { command: 'subscribe', id: 0, streams: 'validations' })
assert.strictEqual(error.data.status, 'error')
assert.strictEqual(error.data.type, 'response')
done()
})
});
it('unrecognized message type', function(done) {
// This enables us to automatically support any
// new messages added by rippled in the future.
this.api.connection.on('unknown', (event) => {
assert.deepEqual(event, {type: 'unknown'})
done();
});
this.api.connection._onMessage(JSON.stringify({type: 'unknown'}));
});
it('ledger close without validated_ledgers', function(done) {
const message = _.omit(ledgerClose, 'validated_ledgers');
this.api.on('ledger', function(ledger) {
assert.strictEqual(ledger.ledgerVersion, 8819951);
done();
});
this.api.connection._ws.emit('message', JSON.stringify(message));
});
it('should throw RippledNotInitializedError if server does not have ' +
'validated ledgers', function() {
this.timeout(3000);
this.api.connection._send(JSON.stringify({
command: 'global_config',
data: {returnEmptySubscribeRequest: 1}
}));
const api = new RippleAPI({server: this.api.connection._url});
return api.connect().then(() => {
assert(false, 'Must have thrown!');
}, error => {
assert(error instanceof this.api.errors.RippledNotInitializedError,
'Must throw RippledNotInitializedError, got instead ' + String(error));
});
});
it('should try to reconnect on empty subscribe response on reconnect',
function(done) {
this.timeout(23000);
this.api.on('error', error => {
done(error || new Error('Should not emit error.'));
});
let disconnectedCount = 0;
this.api.on('connected', () => {
done(disconnectedCount !== 1 ?
new Error('Wrong number of disconnects') : undefined);
});
this.api.on('disconnected', () => {
disconnectedCount++;
});
this.api.connection._send(JSON.stringify({
command: 'global_config',
data: {returnEmptySubscribeRequest: 3}
}));
this.api.connection._send(JSON.stringify({
command: 'test_command',
data: {disconnectIn: 10}
}));
});
});