postchain-client
Version:
Client library for accessing a Postchain node through REST.
438 lines • 22.9 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { abortOnError, tryNextOnError, queryMajority, singleEndpoint, retryRequest, errorMessages, } from "../../src/blockchainClient/failoverStrategies";
import { FailoverStrategy, Method } from "../../src/blockchainClient/enums";
import { createNodeManager } from "../../src/blockchainClient/nodeManager";
import { mockStringDirectoryChainRid } from "../common/mocks";
import { setStatusPolling } from "../../src/blockchainClient/utils";
import * as httpUtils from "../../src/blockchainClient/httpUtil";
import { MERKLE_HASH_VERSIONS } from "../../src/utils/constants";
describe("Failover strategies", () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});
function testAbortOnError({ error, statusCode, numberOfRetries, config }) {
return __awaiter(this, void 0, void 0, function* () {
const handleRequestSpy = jest
.spyOn(httpUtils, "handleRequest")
.mockImplementation((method, path) => {
if (path === "/path/abort-on-error-test") {
return Promise.resolve({
error,
statusCode,
rspBody: "",
});
}
return Promise.resolve({
error: null,
statusCode: 200,
rspBody: "default response",
});
});
yield abortOnError({
method: Method.GET,
path: "/path/abort-on-error-test",
config,
});
const expectedCalls = handleRequestSpy.mock.calls.filter(call => {
const [, path, endpoint] = call;
return (path === "/path/abort-on-error-test" && config.endpointPool.some(ep => ep.url === endpoint));
});
expect(expectedCalls.length).toBe(numberOfRetries);
});
}
function testTryNextOnError({ error, statusCode, numberOfRetries, config }) {
return __awaiter(this, void 0, void 0, function* () {
const handleRequestSpy = jest.spyOn(httpUtils, "handleRequest").mockResolvedValue({
error,
statusCode,
rspBody: "",
});
yield tryNextOnError({
method: Method.GET,
path: "/path",
config,
});
expect(handleRequestSpy).toHaveBeenCalledTimes(numberOfRetries);
});
}
function testSingleEndpoint({ error, statusCode, numberOfRetries, config }) {
return __awaiter(this, void 0, void 0, function* () {
const handleRequestSpy = jest.spyOn(httpUtils, "handleRequest").mockResolvedValue({
error,
statusCode,
rspBody: "",
});
yield singleEndpoint({
method: Method.GET,
path: "/path",
config,
});
expect(handleRequestSpy).toHaveBeenCalledTimes(numberOfRetries);
});
}
describe("AbortOnError", () => {
const config = {
merkleHashVersion: MERKLE_HASH_VERSIONS.ONE,
endpointPool: [
{ url: "url1", whenAvailable: 0 },
{ url: "url2", whenAvailable: 0 },
],
blockchainRid: "blockchainRid",
dappStatusPolling: setStatusPolling({ interval: 100, count: 1 }),
clusterAnchoringStatusPolling: setStatusPolling(),
systemAnchoringStatusPolling: setStatusPolling(),
failoverStrategy: FailoverStrategy.AbortOnError,
attemptsPerEndpoint: 3,
attemptInterval: 100,
unreachableDuration: 100,
directoryChainRid: mockStringDirectoryChainRid,
};
it("abortOnError retries when server error 500", () => __awaiter(void 0, void 0, void 0, function* () {
yield testAbortOnError({
numberOfRetries: 6,
statusCode: 500,
error: null,
config: Object.assign(Object.assign({}, config), { nodeManager: createNodeManager({ nodeUrls: ["url1", "url2"] }) }),
});
}));
it("abortOnError retries when server error 503", () => __awaiter(void 0, void 0, void 0, function* () {
yield testAbortOnError({
numberOfRetries: 6,
statusCode: 503,
error: null,
config: Object.assign(Object.assign({}, config), { nodeManager: createNodeManager({ nodeUrls: ["url1", "url2"] }) }),
});
}));
it("abortOnError does not retry when client error 400", () => __awaiter(void 0, void 0, void 0, function* () {
yield testAbortOnError({
numberOfRetries: 1,
statusCode: 400,
error: null,
config: Object.assign(Object.assign({}, config), { nodeManager: createNodeManager({ nodeUrls: ["url1", "url2"] }) }),
});
}));
it("abortOnError does not retry when statuscode is 200", () => __awaiter(void 0, void 0, void 0, function* () {
yield testAbortOnError({
numberOfRetries: 1,
statusCode: 200,
error: null,
config: Object.assign(Object.assign({}, config), { nodeManager: createNodeManager({ nodeUrls: ["url1", "url2"] }) }),
});
}));
it("abortOnError retries when error is returned", () => __awaiter(void 0, void 0, void 0, function* () {
yield testAbortOnError({
numberOfRetries: 6,
statusCode: null,
error: new Error(),
config: Object.assign(Object.assign({}, config), { nodeManager: createNodeManager({ nodeUrls: ["url1", "url2"] }) }),
});
}));
});
describe("TryNextOnError", () => {
const config = {
endpointPool: [
{ url: "url1", whenAvailable: 0 },
{ url: "url2", whenAvailable: 0 },
],
blockchainRid: "blockchainRid",
statusPollInterval: 100,
statusPollCount: 1,
failoverStrategy: FailoverStrategy.TryNextOnError,
attemptsPerEndpoint: 3,
attemptInterval: 100,
unreachableDuration: 100,
directoryChainRid: mockStringDirectoryChainRid,
};
it("tryNextOnError retries when client error 400", () => __awaiter(void 0, void 0, void 0, function* () {
yield testTryNextOnError({
numberOfRetries: 6,
statusCode: 400,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("tryNextOnError retries when server error 500", () => __awaiter(void 0, void 0, void 0, function* () {
yield testTryNextOnError({
numberOfRetries: 6,
statusCode: 500,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("tryNextOnError does not retry when statuscode is 200", () => __awaiter(void 0, void 0, void 0, function* () {
yield testTryNextOnError({
numberOfRetries: 1,
statusCode: 200,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("tryNextOnError reties when error is returned", () => __awaiter(void 0, void 0, void 0, function* () {
yield testTryNextOnError({
numberOfRetries: 6,
statusCode: null,
error: new Error(),
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
});
describe("SingleEndpoint", () => {
const config = {
endpointPool: [
{ url: "url1", whenAvailable: 0 },
{ url: "url2", whenAvailable: 0 },
],
blockchainRid: "blockchainRid",
statusPollInterval: 100,
statusPollCount: 1,
failoverStrategy: FailoverStrategy.TryNextOnError,
attemptsPerEndpoint: 3,
attemptInterval: 100,
unreachableDuration: 100,
directoryChainRid: mockStringDirectoryChainRid,
};
it("singleEndpoint does not retry when statuscode is 200", () => __awaiter(void 0, void 0, void 0, function* () {
yield testSingleEndpoint({
numberOfRetries: 1,
statusCode: 200,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("singleEndpoint retry when server error 400", () => __awaiter(void 0, void 0, void 0, function* () {
yield testSingleEndpoint({
numberOfRetries: 3,
statusCode: 400,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("singleEndpoint retry when server error 500", () => __awaiter(void 0, void 0, void 0, function* () {
yield testSingleEndpoint({
numberOfRetries: 3,
statusCode: 500,
error: null,
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("singleEndpoint reties when error is returned", () => __awaiter(void 0, void 0, void 0, function* () {
yield testSingleEndpoint({
numberOfRetries: 3,
statusCode: null,
error: new Error(),
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: ["url1", "url2"],
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}));
it("singleEndpoint throws an error when no endpoint is found", () => __awaiter(void 0, void 0, void 0, function* () {
const nodeManager = createNodeManager({
nodeUrls: ["url1", "url2"],
});
const nodeManagerSpy = jest.spyOn(nodeManager, "getNode").mockReturnValue(null);
yield expect(singleEndpoint({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { nodeManager: nodeManager }),
})).rejects.toThrow("Cannot get endpoint. Node not found!");
expect(nodeManagerSpy).toHaveBeenCalledTimes(1);
}));
});
describe("QueryMajority", () => {
const config = {
endpointPool: [
{ url: "url1", whenAvailable: 0 },
{ url: "url2", whenAvailable: 0 },
],
blockchainRid: "blockchainRid",
statusPollInterval: 100,
statusPollCount: 1,
failoverStrategy: FailoverStrategy.QueryMajority,
attemptsPerEndpoint: 3,
attemptInterval: 100,
unreachableDuration: 100,
directoryChainRid: mockStringDirectoryChainRid,
};
const resolveRequestsAs = (responses) => {
const handleRequestSpy = jest.spyOn(httpUtils, "handleRequest");
responses.forEach(response => {
if (response.error) {
handleRequestSpy.mockImplementationOnce(() => __awaiter(void 0, void 0, void 0, function* () {
throw response.error;
}));
}
else {
handleRequestSpy.mockImplementationOnce(() => __awaiter(void 0, void 0, void 0, function* () {
return ({
error: null,
statusCode: response.statusCode,
rspBody: response.rspBody,
});
}));
}
});
return handleRequestSpy;
};
it("Should return a succesfull response as soon as it gets an BFT-majority of successful responses", () => __awaiter(void 0, void 0, void 0, function* () {
const mockRequests = [
{ error: null, statusCode: 200, rspBody: "response2" },
{ error: null, statusCode: 200, rspBody: "response1" },
{ error: null, statusCode: 200, rspBody: "response1" },
{ error: null, statusCode: 200, rspBody: "response3" },
{ error: null, statusCode: 200, rspBody: "response1" },
];
const handleRequestSpy = resolveRequestsAs(mockRequests);
const urls = ["url1", "url2", "url3", "url4", "url5"];
const result = yield queryMajority({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: urls,
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
expect(result.rspBody).toEqual("response1");
expect(handleRequestSpy).toHaveBeenCalledTimes(urls.length);
}));
it("should when the failureThreshold is reached return the first failure if there are any.", () => __awaiter(void 0, void 0, void 0, function* () {
resolveRequestsAs([
{ error: null, statusCode: 200, rspBody: "success - response 1" },
{ error: null, statusCode: 200, rspBody: "success - response 2" },
{ error: null, statusCode: 500, rspBody: "fail - response 1" },
{ error: null, statusCode: 500, rspBody: "fail - response 2" },
]);
const urls = ["url1", "url2", "url3", "url4"];
const result = yield queryMajority({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, nodeManager: createNodeManager({
nodeUrls: urls,
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
expect(result.rspBody).toEqual("fail - response 1");
}));
it("should when the failureThreshold is reached throw the first error, if there are no failures", () => __awaiter(void 0, void 0, void 0, function* () {
const clientErrorMessage = "client error";
resolveRequestsAs([
{ error: null, statusCode: 200, rspBody: "success - response 1" },
{ error: null, statusCode: 200, rspBody: "success - response 2" },
{ error: new Error(clientErrorMessage), statusCode: 500, rspBody: "" },
{ error: new Error(clientErrorMessage), statusCode: 500, rspBody: "" },
]);
const urls = ["url1", "url2", "url3", "url4"];
yield expect(queryMajority({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, endpointPool: urls.map(url => ({ url, whenAvailable: 0 })), nodeManager: createNodeManager({
nodeUrls: urls,
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
})).rejects.toThrow(clientErrorMessage);
}));
it("should throw an error if no consensus could be reached.", () => __awaiter(void 0, void 0, void 0, function* () {
resolveRequestsAs([
{ error: null, statusCode: 200, rspBody: "response1" },
{ error: null, statusCode: 200, rspBody: "response2" },
{ error: null, statusCode: 200, rspBody: "response3" },
{ error: null, statusCode: 200, rspBody: "response4" },
]);
const urls = ["url1", "url2", "url3", "url4"];
let requestError;
try {
yield queryMajority({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, endpointPool: urls.map(url => ({ url, whenAvailable: 0 })), nodeManager: createNodeManager({
nodeUrls: urls,
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}
catch (error) {
requestError = error;
}
expect(requestError).not.toBe(null);
expect(requestError.message).toEqual(errorMessages.NO_CONSENSUS);
}));
it("throws timeout error if takes longer than 15000 ms min for first requsts to resolve", () => __awaiter(void 0, void 0, void 0, function* () {
const urls = ["url1", "url2", "url3", "url4"];
urls.forEach((url, i) => {
jest.spyOn(httpUtils, "handleRequest").mockImplementationOnce(() => new Promise(resolve => {
setTimeout(() => {
resolve({
error: null,
statusCode: 200,
rspBody: `response${i + 1}`,
});
}, 16000);
}));
});
let requestError;
try {
yield queryMajority({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { merkleHashVersion: MERKLE_HASH_VERSIONS.ONE, endpointPool: urls.map(url => ({ url, whenAvailable: 0 })), nodeManager: createNodeManager({
nodeUrls: urls,
}), dappStatusPolling: setStatusPolling(), clusterAnchoringStatusPolling: setStatusPolling(), systemAnchoringStatusPolling: setStatusPolling() }),
});
}
catch (error) {
requestError = error;
}
expect(requestError).not.toBe("Timeout exceeded");
}), 20000);
});
describe("RetryRequest", () => {
const config = {
endpointPool: [{ url: "url1", whenAvailable: 0 }],
blockchainRid: "brid",
statusPollInterval: 100,
statusPollCount: 10,
failoverStrategy: FailoverStrategy.AbortOnError,
attemptsPerEndpoint: 1,
attemptInterval: 100,
unreachableDuration: 1000,
};
it("update the endpointpool if a endpoint is not responding", () => __awaiter(void 0, void 0, void 0, function* () {
jest.spyOn(httpUtils, "handleRequest").mockResolvedValue({
error: null,
statusCode: 500,
rspBody: "",
});
const nodeManager = createNodeManager({
nodeUrls: ["url1"],
});
yield retryRequest({
method: Method.GET,
path: "/path",
config: Object.assign(Object.assign({}, config), { nodeManager }),
validateStatusCode: statusCode => ![500, 503].includes(statusCode),
});
expect(nodeManager.nodes[0].whenAvailable).not.toBe(0);
}));
});
});
//# sourceMappingURL=failoverStrategies.test.js.map