Files
xahau.js/test/connection-test.ts
2020-04-27 12:06:18 -07:00

637 lines
19 KiB
TypeScript

import _ from 'lodash'
import net from 'net'
import assert from 'assert-diff'
import setupAPI from './setup-api'
import {RippleAPI} from 'ripple-api'
import ledgerClose from './fixtures/rippled/ledger-close.json'
import {ignoreWebSocketDisconnect} from './utils'
const utils = RippleAPI._PRIVATE.ledgerUtils
const TIMEOUT = 200000 // how long before each test case times out
const isBrowser = (process as any).browser
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: any = new utils.common.Connection('url')
assert.strictEqual(connection._url, 'url')
assert(_.isUndefined(connection._config.proxy))
assert(_.isUndefined(connection._config.authorization))
})
describe('trace', () => {
const mockedRequestData = {mocked: 'request'}
const mockedResponse = JSON.stringify({mocked: 'response', id: 0})
const expectedMessages = [
// We add the ID here, since it's not a part of the user-provided request.
['send', JSON.stringify({...mockedRequestData, id: 0})],
['receive', mockedResponse]
]
const originalConsoleLog = console.log
afterEach(() => {
console.log = originalConsoleLog
})
it('as false', function() {
const messages = []
console.log = (id, message) => messages.push([id, message])
const connection: any = new utils.common.Connection('url', {trace: false})
connection._ws = {send: function() {}}
connection.request(mockedRequestData)
connection._onMessage(mockedResponse)
assert.deepEqual(messages, [])
})
it('as true', function() {
const messages = []
console.log = (id, message) => messages.push([id, message])
const connection: any = new utils.common.Connection('url', {trace: true})
connection._ws = {send: function() {}}
connection.request(mockedRequestData)
connection._onMessage(mockedResponse)
assert.deepEqual(messages, expectedMessages)
})
it('as a function', function() {
const messages = []
const connection: any = new utils.common.Connection('url', {
trace: (id, message) => messages.push([id, message])
})
connection._ws = {send: function() {}}
connection.request(mockedRequestData)
connection._onMessage(mockedResponse)
assert.deepEqual(messages, expectedMessages)
})
})
it('ledger methods work as expected', async function() {
assert.strictEqual(await this.api.connection.getLedgerVersion(), 8819951)
assert.strictEqual(
await this.api.connection.hasLedgerVersion(8819951),
true
)
assert.strictEqual(
await this.api.connection.hasLedgerVersions(8819951, undefined),
true
)
// It would be nice to test a better range, but the mocked ledger only supports this single number
assert.strictEqual(
await this.api.connection.hasLedgerVersions(8819951, 8819951),
true
)
assert.strictEqual(await this.api.connection.getFeeBase(), 10)
assert.strictEqual(await this.api.connection.getFeeRef(), 10)
assert.strictEqual(await this.api.connection.getReserveBase(), 20000000) // 20 XRP
})
it('with proxy', function(done) {
if (isBrowser) {
done()
return
}
createServer().then((server: any) => {
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()
connection.disconnect()
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(err => {
assert(err instanceof this.api.errors.NotConnectedError)
})
}, 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 (isBrowser) {
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', async function() {
await this.api.connection.request({
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._ws.send = function(message, options, callback) {
callback(null)
}
const request = {command: 'server_info'}
return this.api.connection
.request(request, 10)
.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) {
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('DisconnectedError on initial _onOpen send', async function() {
// _onOpen previously could throw PromiseRejectionHandledWarning: Promise rejection was handled asynchronously
// do not rely on the api.setup hook to test this as it bypasses the case, disconnect api connection first
await this.api.disconnect()
// stub _onOpen to only run logic relevant to test case
this.api.connection._onOpen = () => {
// overload websocket send on open when _ws exists
this.api.connection._ws.send = function(data, options, cb) {
// recent ws throws this error instead of calling back
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)')
}
const request = {command: 'subscribe', streams: ['ledger']}
return this.api.connection.request(request)
}
try {
await this.api.connect()
} catch (error) {
assert(error instanceof this.api.errors.DisconnectedError)
assert.strictEqual(
error.message,
'WebSocket is not open: readyState 0 (CONNECTING)'
)
}
})
it('ResponseFormatError', function() {
return this.api
.request('test_command', {data: {unrecognizedResponse: true}})
.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() {
it('reconnect on several unexpected close', function(done) {
if (isBrowser) {
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
function breakConnection() {
self.api.connection
.request({
command: 'test_command',
data: {disconnectIn: 10}
})
.catch(ignoreWebSocketDisconnect)
}
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) {
done(
new Error(
'reconnectsCount must be equal to ' +
num +
' (got ' +
reconnectsCount +
' instead)'
)
)
} else if (code !== 1006) {
done(
new Error(
'disconnect must send code 1006 (got ' + code + ' instead)'
)
)
} else {
done()
}
}
})
breakConnection()
})
})
it('reconnect event on heartbeat failure', function(done) {
if (isBrowser) {
const phantomTest = /PhantomJS/
if (phantomTest.test(navigator.userAgent)) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done()
return
}
}
// Set the heartbeat to less than the 1 second ping response
this.api.connection._config.timeout = 500
// Drop the test runner timeout, since this should be a quick test
this.timeout(5000)
// Hook up a listener for the reconnect event
this.api.connection.on('reconnect', () => done())
// Trigger a heartbeat
this.api.connection._heartbeat().catch(error => {
/* ignore - test expects heartbeat failure */
})
})
it('heartbeat failure and reconnect failure', function(done) {
if (isBrowser) {
const phantomTest = /PhantomJS/
if (phantomTest.test(navigator.userAgent)) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done()
return
}
}
// Set the heartbeat to less than the 1 second ping response
this.api.connection._config.timeout = 500
// Drop the test runner timeout, since this should be a quick test
this.timeout(5000)
// fail on reconnect/connection
this.api.connection.reconnect = async () => {
throw new Error('error on reconnect')
}
// Hook up a listener for the reconnect error event
this.api.on('error', (error, message) => {
if (error === 'reconnect' && message === 'error on reconnect') {
return done()
}
return done(new Error('Expected error on reconnect'))
})
// Trigger a heartbeat
this.api.connection._heartbeat()
})
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.connection.once('error', error => {
done(new Error('should not throw error, got ' + String(error)))
})
this.api.connection.once('disconnected', code => {
assert.strictEqual(code, 1006)
done()
})
this.api.connection
.request({
command: 'test_command',
data: {disconnectIn: 10}
})
.catch(ignoreWebSocketDisconnect)
})
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(undefined as string)
return connection
.connect()
.then(() => {
assert(false, 'Should throw ConnectionError')
})
.catch(error => {
assert(
error instanceof this.api.errors.ConnectionError,
'Should throw ConnectionError'
)
})
})
it('connect multiserver error', function() {
assert.throws(function() {
new RippleAPI({
servers: ['wss://server1.com', 'wss://server2.com']
} as any)
}, 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('response', message => {
assert.strictEqual(message.id, 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',
async function() {
this.timeout(3000)
await this.api.connection.request({
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 clean up websocket connection if error after websocket is opened', async function() {
await this.api.disconnect()
// fail on connection
this.api.connection._subscribeToLedger = async () => {
throw new Error('error on _subscribeToLedger')
}
try {
await this.api.connect()
throw new Error('expected connect() to reject, but it resolved')
} catch (err) {
assert(err.message === 'error on _subscribeToLedger')
// _ws.close event listener should have cleaned up the socket when disconnect _ws.close is run on connection error
// do not fail on connection anymore
this.api.connection._subscribeToLedger = async () => {}
await this.api.connection.reconnect()
}
})
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.request({
command: 'test_command',
data: {disconnectIn: 5}
})
})
})