Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
coverageDirectory: 'coverage',

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/', '/src/ppom.ts'],
coveragePathIgnorePatterns: ['/node_modules/', 'src/ppom.ts', 'src/index.ts'],

// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'babel',
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"test": "jest && jest-it-up",
"test:watch": "jest --watch"
},
"dependencies": {
"@metamask/base-controller": "^3.0.0",
"@metamask/controller-utils": "^4.0.0",
"await-semaphore": "^0.1.3"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^2.3.1",
"@lavamoat/preinstall-always-fail": "^1.0.0",
Expand Down Expand Up @@ -70,7 +75,9 @@
},
"lavamoat": {
"allowScripts": {
"@lavamoat/preinstall-always-fail": false
"@lavamoat/preinstall-always-fail": false,
"@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>keccak": false,
"@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>secp256k1": false
}
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ppom-controller';

export type { StorageBackend, StorageKey } from './ppom-storage';
281 changes: 281 additions & 0 deletions src/ppom-controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { ControllerMessenger } from '@metamask/base-controller';

import { VERSION_INFO, storageBackendReturningData } from '../test/test-utils';
import { PPOM } from './ppom';
import { PPOMController, DAY_IN_MILLISECONDS } from './ppom-controller';

Object.defineProperty(globalThis, 'fetch', {
writable: true,
value: () => undefined,
});

jest.mock('./ppom.ts', () => ({
PPOM: class PPOMClass {
#jsonRpcRequest;

constructor(jsonRpcRequest: any) {
this.#jsonRpcRequest = jsonRpcRequest;
}

validateJsonRpc = async () => {
return Promise.resolve();
};

free = () => undefined;

testJsonRPCRequest = async () => await this.#jsonRpcRequest();
},
ppomInit: () => undefined,
}));

const PPOM_VERSION_PATH =
'https://storage.googleapis.com/ppom-cdn/ppom_version.json';

const buildFetchSpy = (
versionData: any = {
status: 200,
json: () => VERSION_INFO,
},
blobData: any = {
status: 200,
arrayBuffer: () => new ArrayBuffer(123),
},
) => {
return jest
.spyOn(globalThis, 'fetch' as any)
.mockImplementation((url: any) => {
if (url === PPOM_VERSION_PATH) {
return versionData;
}
return blobData;
});
};

const buildPPOMController = (args?: any) => {
const controllerMessenger = new ControllerMessenger();
const ppomController = new PPOMController({
storageBackend: storageBackendReturningData,
provider: () => undefined,
chainId: '0x1',
onNetworkChange: () => undefined,
messenger: controllerMessenger.getRestricted({
name: 'PPOMController',
}),
...args,
});
return ppomController;
};

describe('PPOMController', () => {
describe('usePPOM', () => {
it('should provide instance of ppom to the passed ballback', async () => {
const ppomController = buildPPOMController();
buildFetchSpy();

await ppomController.usePPOM(async (ppom: PPOM) => {
expect(ppom).toBeDefined();
return Promise.resolve();
});
});

it('should return the value returned by callback', async () => {
const ppomController = buildPPOMController();
buildFetchSpy();

const result = await ppomController.usePPOM(async (ppom: PPOM) => {
expect(ppom).toBeDefined();
return Promise.resolve('DUMMY_VALUE');
});
expect(result).toBe('DUMMY_VALUE');
});

it('should refresh data if network is changed', async () => {
const spy = buildFetchSpy({
status: 200,
json: () => [
...VERSION_INFO,
{
name: 'data',
chainId: '0x2',
version: '1.0.3',
checksum:
'409a7f83ac6b31dc8c77e3ec18038f209bd2f545e0f4177c2e2381aa4e067b49',
filePath: 'data',
},
],
});

let callBack: any;
const ppomController = buildPPOMController({
onNetworkChange: (func: any) => {
callBack = func;
},
});
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);

callBack('0x2');
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(4);
callBack('0x1');
});

it('should pass instance of provider to ppom to enable it to send JSON RPC request on it', async () => {
buildFetchSpy();

const ppomController = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2(undefined, 'DUMMY_VALUE');
},
},
});

await ppomController.usePPOM(async (ppom: PPOM) => {
const result = await (ppom as any).testJsonRPCRequest({});
expect(result).toBe('DUMMY_VALUE');
});
});

it('should propogate to ppom if JSON RPC request on provider fails', async () => {
const ppomController = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2('DUMMY_ERROR');
},
},
});
buildFetchSpy();
await ppomController.usePPOM(async (ppom: PPOM) => {
(ppom as any).testJsonRPCRequest({}).catch((exp: any) => {
// eslint-disable-next-line jest/no-conditional-expect
expect(exp).toBe('DUMMY_ERROR');
});
});
});
});

describe('updatePPOM', () => {
it('should not fetch file if chainId of the file is different from current chainId', async () => {
const spy = buildFetchSpy({
status: 200,
json: () => [
...VERSION_INFO,
{
name: 'data',
chainId: '0x2',
version: '1.0.3',
checksum:
'409a7f83ac6b31dc8c77e3ec18038f209bd2f545e0f4177c2e2381aa4e067b49',
filePath: 'data',
},
],
});

const ppomController = buildPPOMController();
await ppomController.usePPOM(async () => {
return Promise.resolve();
});

expect(spy).toHaveBeenCalledTimes(3);
});

it('should not fetch file if it already exists', async () => {
const spy = buildFetchSpy();

const ppomController = buildPPOMController();
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
});

it('should throw error if fetch for version info return 500', async () => {
buildFetchSpy({
status: 500,
});

const ppomController = buildPPOMController();
await expect(async () => {
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
}).rejects.toThrow('Failed to fetch version info');
});

it('should throw error if fetch for blob return 500', async () => {
buildFetchSpy(undefined, {
status: 500,
});

const ppomController = buildPPOMController();
await expect(async () => {
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
}).rejects.toThrow(
'Failed to fetch file with url https://storage.googleapis.com/ppom-cdn/blob',
);
});
});

describe('setRefreshInterval', () => {
it('should update refresh interval', async () => {
const ppomController = buildPPOMController();
const spy = buildFetchSpy();

// controller fetches new data files is difference from last updated time
// is greater than refresh interval.
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
ppomController.setRefreshInterval(0);
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(4);

ppomController.setRefreshInterval(1000);
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(4);
ppomController.setRefreshInterval(DAY_IN_MILLISECONDS);
});
});

describe('clear', () => {
it('should clear controller state', async () => {
const ppomController = buildPPOMController();
const spy = buildFetchSpy();

// controller fetches new data files when state is cleared
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);
ppomController.clear();
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(6);
});
Comment thread
jpuri marked this conversation as resolved.
});
});
Loading