UNPKG

postchain-client

Version:

Client library for accessing a Postchain node through REST.

438 lines 22.9 kB
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