1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

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
This commit is contained in:
Elliot Winkler 2023-04-20 11:51:41 -06:00 committed by GitHub
parent 3776f4ad4c
commit 24eae1d3c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 3503 additions and 2395 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { PollingBlockTracker } from 'eth-block-tracker';
/**
* Acts like a PollingBlockTracker, but doesn't start the polling loop or
* make any requests.
*/
export class FakeBlockTracker extends PollingBlockTracker {
async _start() {
// do nothing
}
}

View File

@ -0,0 +1,206 @@
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);
}
}
}

View File

@ -3,6 +3,7 @@ module.exports = {
'<rootDir>/app/scripts/constants/error-utils.js',
'<rootDir>/app/scripts/controllers/network/**/*.js',
'<rootDir>/app/scripts/controllers/network/**/*.ts',
'!<rootDir>/app/scripts/controllers/network/**/test/*.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.js',
'<rootDir>/app/scripts/controllers/sign.ts',
'<rootDir>/app/scripts/flask/**/*.js',

View File

@ -408,6 +408,7 @@
"@types/gulp-dart-sass": "^1.0.1",
"@types/gulp-sourcemaps": "^0.0.35",
"@types/jest": "^29.1.2",
"@types/jest-when": "^3.5.2",
"@types/madge": "^5.0.0",
"@types/node": "^17.0.21",
"@types/pify": "^5.0.1",
@ -483,6 +484,7 @@
"jest": "^29.1.2",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^29.1.2",
"jest-when": "^3.5.2",
"js-yaml": "^4.1.0",
"jsdom": "^11.2.0",
"junit-report-merger": "^4.0.0",

View File

@ -7416,6 +7416,15 @@ __metadata:
languageName: node
linkType: hard
"@types/jest-when@npm:^3.5.2":
version: 3.5.2
resolution: "@types/jest-when@npm:3.5.2"
dependencies:
"@types/jest": "*"
checksum: 106230dd71ee266bbd7620ab339a7305054e56ba7638f0eff9f222e67a959df0ad68a7f0108156c50f7005881bae59cd5c38b3760c101d060cdb3dac9cd77ee2
languageName: node
linkType: hard
"@types/jest@npm:*, @types/jest@npm:^29.1.2":
version: 29.1.2
resolution: "@types/jest@npm:29.1.2"
@ -21886,6 +21895,15 @@ __metadata:
languageName: node
linkType: hard
"jest-when@npm:^3.5.2":
version: 3.5.2
resolution: "jest-when@npm:3.5.2"
peerDependencies:
jest: ">= 25"
checksum: 9ad95552d377ef4d517c96a14c38bd8626d6856f518e7efc8a01d76a45a39d3c52281392c98da781c302114c78cd1ea17556c7f638d49d15971fcce9d58306e8
languageName: node
linkType: hard
"jest-worker@npm:^26.5.0, jest-worker@npm:^26.6.2":
version: 26.6.2
resolution: "jest-worker@npm:26.6.2"
@ -24218,6 +24236,7 @@ __metadata:
"@types/gulp-dart-sass": ^1.0.1
"@types/gulp-sourcemaps": ^0.0.35
"@types/jest": ^29.1.2
"@types/jest-when": ^3.5.2
"@types/madge": ^5.0.0
"@types/node": ^17.0.21
"@types/pify": ^5.0.1
@ -24331,6 +24350,7 @@ __metadata:
jest-canvas-mock: ^2.3.1
jest-environment-jsdom: ^29.1.2
jest-junit: ^14.0.1
jest-when: ^3.5.2
js-yaml: ^4.1.0
jsdom: ^11.2.0
json-rpc-engine: ^6.1.0