Files
xahau.js/test/connection.ts
Nathan Nichols 8b95ee5fab build: Initial linting setup (#1560)
* sets up linting config and runs `yarn lint --fix` once, so that all changes will show up correctly in future PRs.

* Note that there are still a lot of linter errors.
2021-10-04 14:10:10 -04:00

578 lines
18 KiB
TypeScript

import net from "net";
import assert from "assert-diff";
import _ from "lodash";
import { Client } from "xrpl-local";
import { Connection } from "xrpl-local/client";
import rippled from "./fixtures/rippled";
import setupClient from "./setupClient";
import { ignoreWebSocketDisconnect } from "./testUtils";
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(setupClient.setup);
afterEach(setupClient.teardown);
it("default options", function () {
const connection: any = new Connection("url");
assert.strictEqual(connection._url, "url");
assert(connection._config.proxy == null);
assert(connection._config.authorization == null);
});
describe("trace", function () {
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(function () {
console.log = originalConsoleLog;
});
it("as false", function () {
const messages: any[] = [];
console.log = (id, message) => messages.push([id, message]);
const connection: any = new Connection("url", { trace: false });
connection._ws = { send() {} };
connection.request(mockedRequestData);
connection._onMessage(mockedResponse);
assert.deepEqual(messages, []);
});
it("as true", function () {
const messages: any[] = [];
console.log = (id, message) => messages.push([id, message]);
const connection: any = new Connection("url", { trace: true });
connection._ws = { send() {} };
connection.request(mockedRequestData);
connection._onMessage(mockedResponse);
assert.deepEqual(messages, expectedMessages);
});
it("as a function", function () {
const messages: any[] = [];
const connection: any = new Connection("url", {
trace: (id, message) => messages.push([id, message]),
});
connection._ws = { send() {} };
connection.request(mockedRequestData);
connection._onMessage(mockedResponse);
assert.deepEqual(messages, expectedMessages);
});
});
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 Connection(this.client.connection._url, options);
connection.connect().catch((err) => {
assert(err instanceof this.client.errors.NotConnectedError);
});
}, done);
});
it("Multiply disconnect calls", function () {
this.client.disconnect();
return this.client.disconnect();
});
it("reconnect", function () {
return this.client.connection.reconnect();
});
it("NotConnectedError", function () {
const connection = new Connection("url");
return connection
.request({
command: "ledger",
ledger_index: "validated",
})
.then(() => {
assert(false, "Should throw NotConnectedError");
})
.catch((error) => {
assert(error instanceof this.client.errors.NotConnectedError);
});
});
it("should throw NotConnectedError if server not responding ", function (done) {
if (isBrowser) {
if (navigator.userAgent.includes("PhantomJS")) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done();
return;
}
}
// Address where no one listens
const connection = new Connection("ws://testripple.circleci.com:129");
connection.on("error", done);
connection.connect().catch((error) => {
assert(error instanceof this.client.errors.NotConnectedError);
done();
});
});
it("DisconnectedError", async function () {
this.mockRippled.suppressOutput = true;
this.mockRippled.on(`request_server_info`, function (request, conn) {
assert.strictEqual(request.command, "server_info");
conn.close();
});
return this.client
.request({ command: "server_info" })
.then(() => {
assert(false, "Should throw DisconnectedError");
})
.catch((error) => {
assert(error instanceof this.client.errors.DisconnectedError);
});
});
it("TimeoutError", function () {
this.client.connection._ws.send = function (message, callback) {
callback(null);
};
const request = { command: "server_info" };
return this.client.connection
.request(request, 10)
.then(() => {
assert(false, "Should throw TimeoutError");
})
.catch((error) => {
assert(error instanceof this.client.errors.TimeoutError);
});
});
it("DisconnectedError on send", function () {
this.client.connection._ws.send = function (message, callback) {
callback({ message: "not connected" });
};
return this.client
.request({ command: "server_info" })
.then(() => {
assert(false, "Should throw DisconnectedError");
})
.catch((error) => {
console.log(error);
assert(error instanceof this.client.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 client.setup hook to test this as it bypasses the case, disconnect client connection first
await this.client.disconnect();
// stub _onOpen to only run logic relevant to test case
this.client.connection._onOpen = () => {
// overload websocket send on open when _ws exists
this.client.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.client.connection.request(request);
};
try {
await this.client.connect();
} catch (error) {
assert(error instanceof this.client.errors.DisconnectedError);
assert.strictEqual(
error.message,
"WebSocket is not open: readyState 0 (CONNECTING)"
);
}
});
it("ResponseFormatError", function () {
return this.client
.request({
command: "test_command",
data: { unrecognizedResponse: true },
})
.then(() => {
assert(false, "Should throw ResponseFormatError");
})
.catch((error) => {
assert(error instanceof this.client.errors.ResponseFormatError);
});
});
it("reconnect on unexpected close", function (done) {
this.client.connection.on("connected", () => {
done();
});
setTimeout(() => {
this.client.connection._ws.close();
}, 1);
});
describe("reconnection test", function () {
it("reconnect on several unexpected close", function (done) {
if (isBrowser) {
if (navigator.userAgent.includes("PhantomJS")) {
// inside PhantomJS this one just hangs, so skip as not very relevant
done();
return;
}
}
this.timeout(70001);
const self = this;
function breakConnection() {
self.client.connection
.request({
command: "test_command",
data: { disconnectIn: 10 },
})
.catch(ignoreWebSocketDisconnect);
}
let connectsCount = 0;
let disconnectsCount = 0;
let reconnectsCount = 0;
let code = 0;
this.client.connection.on("reconnecting", () => {
reconnectsCount += 1;
});
this.client.connection.on("disconnected", (_code) => {
code = _code;
disconnectsCount += 1;
});
const num = 3;
this.client.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) {
if (navigator.userAgent.includes("PhantomJS")) {
// 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.client.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.client.connection.on("reconnect", () => done());
// Trigger a heartbeat
this.client.connection._heartbeat().catch((error) => {
/* ignore - test expects heartbeat failure */
});
});
it("heartbeat failure and reconnect failure", function (done) {
if (isBrowser) {
if (navigator.userAgent.includes("PhantomJS")) {
// 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.client.connection._config.timeout = 500;
// Drop the test runner timeout, since this should be a quick test
this.timeout(5000);
// fail on reconnect/connection
this.client.connection.reconnect = async () => {
throw new Error("error on reconnect");
};
// Hook up a listener for the reconnect error event
this.client.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.client.connection._heartbeat();
});
it("should emit disconnected event with code 1000 (CLOSE_NORMAL)", function (done) {
this.client.once("disconnected", (code) => {
assert.strictEqual(code, 1000);
done();
});
this.client.disconnect();
});
it("should emit disconnected event with code 1006 (CLOSE_ABNORMAL)", function (done) {
this.client.connection.once("error", (error) => {
done(new Error(`should not throw error, got ${String(error)}`));
});
this.client.connection.once("disconnected", (code) => {
assert.strictEqual(code, 1006);
done();
});
this.client.connection
.request({
command: "test_command",
data: { disconnectIn: 10 },
})
.catch(ignoreWebSocketDisconnect);
});
it("should emit connected event on after reconnect", function (done) {
this.client.once("connected", done);
this.client.connection._ws.close();
});
it("Multiply connect calls", function () {
return this.client.connect().then(() => {
return this.client.connect();
});
});
it("Cannot connect because no server", function () {
const connection = new Connection(undefined as unknown as string);
return connection
.connect()
.then(() => {
assert(false, "Should throw ConnectionError");
})
.catch((error) => {
assert(
error instanceof this.client.errors.ConnectionError,
"Should throw ConnectionError"
);
});
});
it("connect multiserver error", function () {
assert.throws(function () {
new Client({
servers: ["wss://server1.com", "wss://server2.com"],
} as any);
}, this.client.errors.RippleError);
});
it("connect throws error", function (done) {
this.client.once("error", (type, info) => {
assert.strictEqual(type, "type");
assert.strictEqual(info, "info");
done();
});
this.client.connection.emit("error", "type", "info");
});
it("emit stream messages", function (done) {
let transactionCount = 0;
let pathFindCount = 0;
this.client.connection.on("transaction", () => {
transactionCount++;
});
this.client.connection.on("path_find", () => {
pathFindCount++;
});
this.client.connection.on("response", (message) => {
assert.strictEqual(message.id, 1);
assert.strictEqual(transactionCount, 1);
assert.strictEqual(pathFindCount, 1);
done();
});
this.client.connection._onMessage(
JSON.stringify({
type: "transaction",
})
);
this.client.connection._onMessage(
JSON.stringify({
type: "path_find",
})
);
this.client.connection._onMessage(
JSON.stringify({
type: "response",
id: 1,
})
);
});
it("invalid message id", function (done) {
this.client.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.client.connection._onMessage(
JSON.stringify({
type: "response",
id: "must be integer",
})
);
});
it("propagates error message", function (done) {
this.client.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.client.connection._onMessage(
JSON.stringify({
error: "slowDown",
error_message: "slow down",
})
);
});
it("propagates RippledError data", function (done) {
const request = { command: "subscribe", streams: "validations" };
this.mockRippled.addResponse(request, rippled.subscribe.error);
this.client.request(request).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",
});
done();
});
});
it("unrecognized message type", function (done) {
// This enables us to automatically support any
// new messages added by rippled in the future.
this.client.connection.on("unknown", (event) => {
assert.deepEqual(event, { type: "unknown" });
done();
});
this.client.connection._onMessage(JSON.stringify({ type: "unknown" }));
});
// it('should clean up websocket connection if error after websocket is opened', async function () {
// await this.client.disconnect()
// // fail on connection
// this.client.connection._subscribeToLedger = async () => {
// throw new Error('error on _subscribeToLedger')
// }
// try {
// await this.client.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.client.connection._subscribeToLedger = async () => {}
// await this.client.connection.reconnect()
// }
// })
it("should try to reconnect on empty subscribe response on reconnect", function (done) {
this.timeout(23000);
this.client.on("error", (error) => {
done(error || new Error("Should not emit error."));
});
let disconnectedCount = 0;
this.client.on("connected", () => {
done(
disconnectedCount !== 1
? new Error("Wrong number of disconnects")
: undefined
);
});
this.client.on("disconnected", () => {
disconnectedCount++;
});
this.client.connection.request({
command: "test_command",
data: { disconnectIn: 5 },
});
});
it("should not crash on error", async function (done) {
this.mockRippled.suppressOutput = true;
this.client.connection
.request({
command: "test_garbage",
})
.then(() => new Error("Should not have succeeded"))
.catch(done());
});
});