mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
24eae1d3c6
* 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
207 lines
6.4 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|