diff --git a/src/transports/HTTPTransport.test.ts b/src/transports/HTTPTransport.test.ts index 014ce8b..e60cd68 100644 --- a/src/transports/HTTPTransport.test.ts +++ b/src/transports/HTTPTransport.test.ts @@ -1,6 +1,6 @@ -import {HTTPTransport, HTTPTransportOptions} from "./HTTPTransport"; +import { HTTPTransport, HTTPTransportOptions } from "./HTTPTransport"; import * as reqMocks from "../__mocks__/requestData"; -import _fetchMock from "isomorphic-fetch" +import _fetchMock from "isomorphic-fetch"; const fetchMock = _fetchMock as jest.Mock>; describe("HTTPTransport", () => { @@ -15,75 +15,123 @@ describe("HTTPTransport", () => { }); it("can send and retrieve request data", async () => { - const httpTransport = new HTTPTransport("http://localhost:8545/rpc-request"); + const httpTransport = new HTTPTransport( + "http://localhost:8545/rpc-request" + ); const data = reqMocks.generateMockRequest(1, "foo", ["bar"]); - const result = await httpTransport.sendData({ request: data, internalID: 1 }); + const result = await httpTransport.sendData({ + request: data, + internalID: 1, + }); expect(result.method).toEqual("foo"); expect(result.params).toEqual(["bar"]); }); it("can send notification data", async () => { - const httpTransport = new HTTPTransport("http://localhost:8545/rpc-notification"); + const httpTransport = new HTTPTransport( + "http://localhost:8545/rpc-notification" + ); const data = reqMocks.generateMockNotificationRequest("foo", ["bar"]); - const result = await httpTransport.sendData({ request: data, internalID: 1 }); + const result = await httpTransport.sendData({ + request: data, + internalID: 1, + }); expect(result).toEqual(undefined); }); it("should throw error on error response", async () => { const httpTransport = new HTTPTransport("http://localhost:8545/rpc-error"); const data = reqMocks.generateMockRequest(9, "foo", ["bar"]); - await expect(httpTransport.sendData({ request: data, internalID: 9 })).rejects.toThrowError("Error message"); + await expect( + httpTransport.sendData({ request: data, internalID: 9 }) + ).rejects.toThrowError("Error message"); }); it("should throw error on bad data response", async () => { - const httpTransport = new HTTPTransport("http://localhost:8545/rpc-garbage"); - const data = { request: reqMocks.generateMockRequest(9, "foo", ["bar"]), internalID: 9 }; - await expect(httpTransport.sendData(data)).rejects.toThrowError("Bad response format"); + const httpTransport = new HTTPTransport( + "http://localhost:8545/rpc-garbage" + ); + const data = { + request: reqMocks.generateMockRequest(9, "foo", ["bar"]), + internalID: 9, + }; + await expect(httpTransport.sendData(data)).rejects.toThrowError( + "Bad response format" + ); }); it("should throw error on bad data response from a batch", async (done) => { - const httpTransport = new HTTPTransport("http://localhost:8545/rpc-garbage"); + const httpTransport = new HTTPTransport( + "http://localhost:8545/rpc-garbage" + ); const data = { resolve: (d: any) => ({}), reject: (e: Error) => { expect(e.message).toContain("Bad response format"); done(); }, - request: { request: reqMocks.generateMockRequest(9, "foo", ["bar"]), internalID: 9 }, + request: { + request: reqMocks.generateMockRequest(9, "foo", ["bar"]), + internalID: 9, + }, }; - await expect(httpTransport.sendData([data])).rejects.toThrow("Bad response format"); + await expect(httpTransport.sendData([data])).rejects.toThrow( + "Bad response format" + ); }); it("should throw error if unknown server crash", async () => { const httpTransport = new HTTPTransport("http://localhost:8545/crash"); - const data = { request: reqMocks.generateMockRequest(9, "foo", ["bar"]), internalID: 9 }; - await expect(httpTransport.sendData(data)).rejects.toThrowError("Random Segfault that crashes fetch"); + const data = { + request: reqMocks.generateMockRequest(9, "foo", ["bar"]), + internalID: 9, + }; + await expect(httpTransport.sendData(data)).rejects.toThrowError( + "Random Segfault that crashes fetch" + ); }); async function callFetch(options?: HTTPTransportOptions): Promise { const httpTransport = new HTTPTransport("http://localhost:8545", options); const data = reqMocks.generateMockRequest(1, "foo", ["bar"]); - await httpTransport.sendData({request: data, internalID: 1}); + await httpTransport.sendData({ request: data, internalID: 1 }); } it("sets content type to application/json", async () => { - await callFetch({headers: {"Content-Type": "image/png"}}) - const headers = fetchMock.mock.calls[0][1].headers - expect(headers.get("Content-Type")).toEqual("application/json") + await callFetch({ headers: { "Content-Type": "image/png" } }); + const headers = fetchMock.mock.calls[0][1].headers; + expect(headers.get("Content-Type")).toEqual("application/json"); }); it("sets header passed from options", async () => { - const headerName = "Authorization" - const headerValue = "Basic credentials" - await callFetch({headers: {[headerName]: headerValue}}) - const headers = fetchMock.mock.calls[0][1].headers - expect(headers.get(headerName)).toEqual(headerValue) + const headerName = "Authorization"; + const headerValue = "Basic credentials"; + await callFetch({ headers: { [headerName]: headerValue } }); + const headers = fetchMock.mock.calls[0][1].headers; + expect(headers.get(headerName)).toEqual(headerValue); }); it("sets credentials argument passed from options", async () => { - const credentials = "include" - await callFetch({credentials}) - expect(fetchMock.mock.calls[0][1].credentials).toEqual(credentials) + const credentials = "include"; + await callFetch({ credentials }); + expect(fetchMock.mock.calls[0][1].credentials).toEqual(credentials); }); -}); + it("accepts an injected fetcher", async () => { + const injectedFetchMock = _fetchMock as jest.Mock>; + + const httpTransport = new HTTPTransport( + "http://localhost:8545/rpc-notification", + { + fetcher: injectedFetchMock, + } + ); + const data = reqMocks.generateMockNotificationRequest("foo", ["bar"]); + const result = await httpTransport.sendData({ + request: data, + internalID: 1, + }); + expect(injectedFetchMock.mock.calls.length).toEqual(1); + expect(result).toEqual(undefined); + }); +}); diff --git a/src/transports/HTTPTransport.ts b/src/transports/HTTPTransport.ts index 3532bd3..06b8f9c 100644 --- a/src/transports/HTTPTransport.ts +++ b/src/transports/HTTPTransport.ts @@ -1,35 +1,46 @@ import fetch from "isomorphic-fetch"; import { Transport } from "./Transport"; -import { JSONRPCRequestData, getNotifications, getBatchRequests } from "../Request"; +import { + JSONRPCRequestData, + getNotifications, + getBatchRequests, +} from "../Request"; import { ERR_UNKNOWN, JSONRPCError } from "../Error"; -type CredentialsOption = "omit" | "same-origin" | "include" +type CredentialsOption = "omit" | "same-origin" | "include"; interface HTTPTransportOptions { - credentials?: CredentialsOption - headers?: Record + credentials?: CredentialsOption; + headers?: Record; + fetcher?: typeof fetch; } class HTTPTransport extends Transport { public uri: string; private readonly credentials?: CredentialsOption; - private readonly headers: Headers + private readonly headers: Headers; + private readonly injectedFetcher?: typeof fetch; constructor(uri: string, options?: HTTPTransportOptions) { super(); this.uri = uri; this.credentials = options && options.credentials; - this.headers = HTTPTransport.setupHeaders(options && options.headers) + this.headers = HTTPTransport.setupHeaders(options && options.headers); + this.injectedFetcher = options?.fetcher; } public connect(): Promise { return Promise.resolve(); } - public async sendData(data: JSONRPCRequestData, timeout: number | null = null): Promise { + public async sendData( + data: JSONRPCRequestData, + timeout: number | null = null + ): Promise { const prom = this.transportRequestManager.addRequest(data, timeout); const notifications = getNotifications(data); const batch = getBatchRequests(data); + const fetcher = this.injectedFetcher || fetch; try { - const result = await fetch(this.uri, { + const result = await fetcher(this.uri, { method: "POST", headers: this.headers, body: JSON.stringify(this.parseData(data)), @@ -50,30 +61,40 @@ class HTTPTransport extends Transport { } } catch (e) { const responseErr = new JSONRPCError(e.message, ERR_UNKNOWN, e); - this.transportRequestManager.settlePendingRequest(notifications, responseErr); - this.transportRequestManager.settlePendingRequest(getBatchRequests(data), responseErr); + this.transportRequestManager.settlePendingRequest( + notifications, + responseErr + ); + this.transportRequestManager.settlePendingRequest( + getBatchRequests(data), + responseErr + ); return Promise.reject(responseErr); } return prom; } // tslint:disable-next-line:no-empty - public close(): void { } + public close(): void {} private onlyNotifications = (data: JSONRPCRequestData) => { if (data instanceof Array) { - return data.every((datum) => datum.request.request.id === null || datum.request.request.id === undefined); + return data.every( + (datum) => + datum.request.request.id === null || + datum.request.request.id === undefined + ); } - return (data.request.id === null || data.request.id === undefined); - } + return data.request.id === null || data.request.id === undefined; + }; private static setupHeaders(headerOptions?: Record): Headers { - const headers = new Headers(headerOptions) + const headers = new Headers(headerOptions); // Overwrite header options to ensure correct content type. - headers.set("Content-Type", "application/json") - return headers + headers.set("Content-Type", "application/json"); + return headers; } } export default HTTPTransport; -export {HTTPTransport, HTTPTransportOptions, CredentialsOption} +export { HTTPTransport, HTTPTransportOptions, CredentialsOption };