/* eslint-disable jest/require-top-level-describe, jest/no-export */ import { withMockedCommunications, withNetworkClient } from './helpers'; /** * Defines tests which exercise the behavior exhibited by an RPC method that * use `blockHash` in the response data to determine whether the response is * cacheable. * * @param method - The name of the RPC method under test. * @param additionalArgs - Additional arguments. * @param additionalArgs.numberOfParameters - The number of parameters supported * by the method under test. * @param additionalArgs.providerType - The type of provider being tested; * either `infura` or `custom` (default: "infura"). */ export function testsForRpcMethodsThatCheckForBlockHashInResponse( method, { numberOfParameters, providerType }, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( `providerType must be either "infura" or "custom", was "${providerType}" instead`, ); } it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => { const requests = [{ method }, { method }]; const mockResult = { blockHash: '0x1' }; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResult }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResult, mockResult]); }); }); it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { const requests = [{ method }, { method }]; const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; await withMockedCommunications({ providerType }, async (comms) => { // Note that we have to mock these requests in a specific order. The // first block tracker request occurs because of the first RPC // request. The second block tracker request, however, does not occur // because of the second RPC request, but rather because we call // `clock.runAll()` below. comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, async (client) => { const firstResult = await client.makeRpcCall(requests[0]); // Proceed to the next iteration of the block tracker so that a new // block is fetched and the current block is updated. client.clock.runAll(); const secondResult = await client.makeRpcCall(requests[1]); return [firstResult, secondResult]; }, ); expect(results).toStrictEqual(mockResults); }); }); it('does not reuse the result of a previous request if result.blockHash was null', async () => { const requests = [{ method }, { method }]; const mockResults = [ { blockHash: null, extra: 'some value' }, { blockHash: '0x100', extra: 'some other value' }, ]; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); }); }); it('does not reuse the result of a previous request if result.blockHash was undefined', async () => { const requests = [{ method }, { method }]; const mockResults = [ { extra: 'some value' }, { blockHash: '0x100', extra: 'some other value' }, ]; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); }); }); it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => { const requests = [{ method }, { method }]; const mockResults = [ { blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', extra: 'some value', }, { blockHash: '0x100', extra: 'some other value' }, ]; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); }); }); for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method }; const mockResult = emptyValue; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request, response: { result: mockResult }, }); const result = await withNetworkClient( { providerType }, ({ makeRpcCall }) => makeRpcCall(request), ); expect(result).toStrictEqual(mockResult); }); }); it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { const requests = [{ method }, { method }]; const mockResults = [emptyValue, { blockHash: '0x100' }]; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); }); }); } for (const paramIndex of [...Array(numberOfParameters).keys()]) { it(`does not reuse the result of a previous request with a valid blockHash if parameter at index "${paramIndex}" differs`, async () => { const firstMockParams = [ ...new Array(numberOfParameters).fill('some value'), ]; const secondMockParams = firstMockParams.slice(); secondMockParams[paramIndex] = 'another value'; const requests = [ { method, params: firstMockParams, }, { method, params: secondMockParams }, ]; const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); const results = await withNetworkClient( { providerType }, ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResults[0], mockResults[1]]); }); }); } }