1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/controllers/network/test/fake-provider.ts
Elliot Winkler 24eae1d3c6
Use fake provider for NetworkController unit tests (#18628)
* Use fake provider for NetworkController unit tests

In the unit tests for NetworkController, it's important to prevent
network requests from occurring. Currently we do that by using Nock.
However, the `core` version of NetworkController uses a fake provider
object. This is arguably a better approach for unit tests because it
prevents us from having to think about the behavior that a specific
middleware may have. For instance, the Infura middleware intercepts
`eth_chainId` to return a static result, and the block cache middleware
replaces the `latest` block tag with the latest block number, making an
extra call to `eth_blockNumber` in doing so. We have to account for
these kinds of behaviors when using Nock, but we do not need to do this
when using a fake provider.

This should make it easier to compare the difference between the unit
tests in this repo vs. in the `core` repo, which should ultimately help
us merge the two controllers together.

* Rename fake-provider-engine to fake-provider

* Rearrange imports

* Move fake-provider and fake-block-tracker into a directory and exclude it from coverage

* Make FakeBlockTracker inert, and fix JSDocs

* Remove generics from FakeProvider

* Call beforeCompleting (and beforeResolving) using async/await

* Fix signature of sendAsync; align other signatures within FakeProvider

* No need to check whether error is not a string

* Don't exclude the provider-api-tests directory from coverage

* Make sure to mock both net_version and eth_getBlockByNumber when testing network status

* Fix FakeProvider so that none of the methods have optional callbacks
2023-04-20 15:21:41 -02:30

207 lines
6.4 KiB
TypeScript

import { inspect, isDeepStrictEqual } from 'util';
import {
JsonRpcEngine,
JsonRpcRequest,
JsonRpcResponse,
} from 'json-rpc-engine';
import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider/dist/safe-event-emitter-provider';
// Store this in case it gets stubbed later
const originalSetTimeout = global.setTimeout;
/**
* An object that allows specifying the behavior of a specific invocation of
* `sendAsync`. The `method` always identifies the stub, but the behavior
* may be specified multiple ways: `sendAsync` can either return a promise or
* throw an error, and if it returns a promise, that promise can either be
* resolved with a response object or reject with an error.
*
* @property request - Looks for a request matching these specifications.
* @property request.method - The RPC method to which this stub will be matched.
* @property request.params - The params to which this stub will be matched.
* @property response - Instructs `sendAsync` to return a promise that resolves
* with a response object.
* @property response.result - Specifies a successful response, with this as the
* `result`.
* @property response.error - Specifies an error response, with this as the
* `error`.
* @property error - Instructs `sendAsync` to return a promise that rejects with
* this error.
* @property implementation - Allows overriding `sendAsync` entirely. Useful if
* you want it to throw an error.
* @property delay - The amount of time that will pass after the callback is
* called with the response.
* @property discardAfterMatching - Usually after the stub matches a request, it
* is discarded, but setting this to true prevents that from happening. True by
* default.
* @property beforeCompleting - Sometimes it is useful to do something after the
* request is kicked off but before it ends (or, in terms of a `fetch` promise,
* when the promise is initiated but before it is resolved). You can pass an
* (async) function for this option to do this.
*/
export type FakeProviderStub = {
request: {
method: string;
params?: any[];
};
delay?: number;
discardAfterMatching?: boolean;
beforeCompleting?: () => void | Promise<void>;
} & (
| {
response: { result: any } | { error: string };
}
| {
error: unknown;
}
| {
implementation: () => void;
}
);
/**
* The set of options that the FakeProviderEngine constructor takes.
*
* @property stubs - A set of objects that allow specifying the behavior
* of specific invocations of `sendAsync` matching a `method`.
*/
interface FakeProviderEngineOptions {
stubs?: FakeProviderStub[];
}
/**
* FakeProviderEngine is an implementation of the provider that
* NetworkController exposes, which is actually an instance of
* Web3ProviderEngine (from the `web3-provider-engine` package). Hence it
* supports the same interface as Web3ProviderEngine, except that fake responses
* for any RPC methods that are accessed can be supplied via an API that is more
* succinct than using Jest's mocking API.
*/
// NOTE: We shouldn't need to extend from the "real" provider here, but
// we'd need a `SafeEventEmitterProvider` _interface_ and that doesn't exist (at
// least not yet).
export class FakeProvider extends SafeEventEmitterProvider {
calledStubs: FakeProviderStub[];
#originalStubs: FakeProviderStub[];
#stubs: FakeProviderStub[];
/**
* Makes a new instance of the fake provider.
*
* @param options - The options.
* @param options.stubs - A set of objects that allow specifying the behavior
* of specific invocations of `sendAsync` matching a `method`.
*/
constructor({ stubs = [] }: FakeProviderEngineOptions) {
super({ engine: new JsonRpcEngine() });
this.#originalStubs = stubs;
this.#stubs = this.#originalStubs.slice();
this.calledStubs = [];
}
send = (
payload: JsonRpcRequest<any>,
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
) => {
return this.#handleSend(payload, callback);
};
sendAsync = (
payload: JsonRpcRequest<any>,
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
) => {
return this.#handleSend(payload, callback);
};
#handleSend(
payload: JsonRpcRequest<any>,
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
) {
if (Array.isArray(payload)) {
throw new Error("Arrays aren't supported");
}
const index = this.#stubs.findIndex((stub) => {
return (
stub.request.method === payload.method &&
(!('params' in stub.request) ||
isDeepStrictEqual(stub.request.params, payload.params))
);
});
if (index === -1) {
const matchingCalledStubs = this.calledStubs.filter((stub) => {
return (
stub.request.method === payload.method &&
(!('params' in stub.request) ||
isDeepStrictEqual(stub.request.params, payload.params))
);
});
let message = `Could not find any stubs matching: ${inspect(payload, {
depth: null,
})}`;
if (matchingCalledStubs.length > 0) {
message += `\n\nIt appears the following stubs were defined, but have been called already:\n\n${inspect(
matchingCalledStubs,
{ depth: null },
)}`;
}
throw new Error(message);
} else {
const stub = this.#stubs[index];
if (stub.discardAfterMatching !== false) {
this.#stubs.splice(index, 1);
}
if (stub.delay) {
originalSetTimeout(() => {
this.#handleRequest(stub, callback);
}, stub.delay);
} else {
this.#handleRequest(stub, callback);
}
this.calledStubs.push({ ...stub });
}
}
async #handleRequest(
stub: FakeProviderStub,
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
) {
if (stub.beforeCompleting) {
await stub.beforeCompleting();
}
if ('implementation' in stub) {
stub.implementation();
return;
}
if ('response' in stub) {
if ('result' in stub.response) {
callback(null, {
jsonrpc: '2.0',
id: 1,
result: stub.response.result,
});
} else if ('error' in stub.response) {
callback(null, {
jsonrpc: '2.0',
id: 1,
error: {
code: -999,
message: stub.response.error,
},
});
}
} else if ('error' in stub) {
callback(stub.error);
}
}
}