Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: SnapController.installSnaps Refactor & Rollback Functionality Added #1023

Merged
merged 39 commits into from Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6fdb952
Refactor wallet_enable
FrederikBolding Oct 26, 2022
14caddc
Merge branch 'fb/wallet-enable-refactor' into hm/update-rollback-mech…
hmalik88 Nov 8, 2022
b597ce5
add initial rollback code
hmalik88 Nov 28, 2022
b9c68cc
more updates
hmalik88 Nov 29, 2022
4657818
Merge branch 'main' into hm/update-rollback-mechanism
hmalik88 Nov 29, 2022
a2ed496
add test
hmalik88 Nov 30, 2022
ee1f5bf
refactors
hmalik88 Dec 1, 2022
4e8ff8d
fixed some more errors
hmalik88 Dec 1, 2022
012d751
fixed another failing test
hmalik88 Dec 1, 2022
e54aec8
Fix test
FrederikBolding Dec 1, 2022
9e18b17
updated test
hmalik88 Dec 1, 2022
9c13101
reverted some inadvertent changes
hmalik88 Dec 1, 2022
020c1c9
removed unnecessary immer usage
hmalik88 Dec 1, 2022
26d48fc
Merge branch 'main' into hm/update-rollback-mechanism
hmalik88 Dec 1, 2022
5b88672
address PR comments
hmalik88 Dec 6, 2022
76a9474
add semver util
hmalik88 Dec 6, 2022
c358a1f
Refactor wallet_enable
FrederikBolding Oct 26, 2022
f2a54e1
add initial rollback code
hmalik88 Nov 28, 2022
bdfb081
more updates
hmalik88 Nov 29, 2022
4b9902c
add test
hmalik88 Nov 30, 2022
221aef4
refactors
hmalik88 Dec 1, 2022
43dfe1b
fixed some more errors
hmalik88 Dec 1, 2022
8d8502b
Fix test
FrederikBolding Dec 1, 2022
5722caf
updated test
hmalik88 Dec 1, 2022
ba3830c
reverted some inadvertent changes
hmalik88 Dec 1, 2022
531d530
removed unnecessary immer usage
hmalik88 Dec 1, 2022
df30e50
address PR comments
hmalik88 Dec 6, 2022
c35c39f
add semver util
hmalik88 Dec 6, 2022
35934be
Stop running snap installs in parallel
FrederikBolding Dec 6, 2022
ad502f7
Fix tests after rebase
FrederikBolding Dec 6, 2022
bac4e86
Merge branch 'hm/update-rollback-mechanism' of github.com:MetaMask/sn…
hmalik88 Dec 6, 2022
d77296c
address more PR comments
hmalik88 Dec 6, 2022
185d8ac
fixed some tests
hmalik88 Dec 6, 2022
6a931da
Merge branch 'main' into hm/update-rollback-mechanism
hmalik88 Dec 6, 2022
3aad81d
removed casting and added gtRange test
hmalik88 Dec 6, 2022
7399f57
jest config update
hmalik88 Dec 6, 2022
54cbb63
update threshold
hmalik88 Dec 6, 2022
4b71e5e
update grammar
hmalik88 Dec 6, 2022
ce8d114
Merge branch 'main' into hm/update-rollback-mechanism
FrederikBolding Dec 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/snaps-controllers/jest.config.js
Expand Up @@ -5,7 +5,7 @@ const baseConfig = require('../../jest.config.base');
module.exports = deepmerge(baseConfig, {
coverageThreshold: {
global: {
branches: 90.85,
branches: 90.81,
functions: 97.45,
lines: 97.45,
statements: 97.45,
Expand Down
213 changes: 162 additions & 51 deletions packages/snaps-controllers/src/snaps/SnapController.test.ts
Expand Up @@ -29,7 +29,7 @@ import {
} from '@metamask/snaps-utils/test-utils';
import { AssertionError } from '@metamask/utils';
import { Crypto } from '@peculiar/webcrypto';
import { EthereumRpcError, ethErrors, serializeError } from 'eth-rpc-errors';
import { ethErrors } from 'eth-rpc-errors';
import fetchMock from 'jest-fetch-mock';
import { createAsyncMiddleware, JsonRpcEngine } from 'json-rpc-engine';
import { createEngineStream } from 'json-rpc-middleware-stream';
Expand Down Expand Up @@ -622,13 +622,13 @@ describe('SnapController', () => {
it('throws an error on invalid semver range during installSnaps', async () => {
const controller = getSnapController();

const result = await controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: 'foo' },
});

expect(result).toMatchObject({
[MOCK_SNAP_ID]: { error: expect.any(EthereumRpcError) },
});
await expect(
controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: 'foo' },
}),
).rejects.toThrow(
'The "version" field must be a valid SemVer version range if specified. Received: "foo"',
hmalik88 marked this conversation as resolved.
Show resolved Hide resolved
);
});

it('reuses an already installed Snap if it satisfies the requested SemVer range', async () => {
Expand Down Expand Up @@ -682,17 +682,12 @@ describe('SnapController', () => {
return true;
});

const result = await controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: {},
});

const { code, message } = serializeError(
ethErrors.provider.userRejectedRequest(),
);
await expect(
controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: {},
}),
).rejects.toThrow('User rejected the request.');

expect(result).toStrictEqual({
[MOCK_SNAP_ID]: { error: expect.objectContaining({ code, message }) },
});
expect(controller.get(MOCK_SNAP_ID)).toBeUndefined();
});

Expand Down Expand Up @@ -737,13 +732,11 @@ describe('SnapController', () => {

const expectedSnapObject = getTruncatedSnap();

expect(
await snapController.installSnaps(MOCK_ORIGIN, {
await expect(
snapController.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: {},
}),
).toStrictEqual({
[MOCK_SNAP_ID]: { error: serializeError(new Error('foo')) },
});
).rejects.toThrow('foo');

expect(messengerCallMock).toHaveBeenCalledTimes(3);
expect(messengerCallMock).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -2220,20 +2213,11 @@ describe('SnapController', () => {
const controller = getSnapController(
getSnapControllerOptions({ messenger }),
);

const result = await controller.installSnaps(MOCK_ORIGIN, {
[snapId]: {},
});

expect(result).toStrictEqual({
[snapId]: { error: expect.any(EthereumRpcError) },
});
expect(messenger.call).toHaveBeenCalledTimes(1);
expect(messenger.call).toHaveBeenCalledWith(
'PermissionController:hasPermission',
MOCK_ORIGIN,
expect.anything(),
);
await expect(
controller.installSnaps(MOCK_ORIGIN, {
[snapId]: {},
}),
).rejects.toThrow('Invalid snap id. Unknown prefix. Received: "foo"');
hmalik88 marked this conversation as resolved.
Show resolved Hide resolved
});

it('updates a snap', async () => {
Expand Down Expand Up @@ -2376,9 +2360,13 @@ describe('SnapController', () => {
}),
);

const result = await controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: newVersionRange },
});
await expect(
controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: newVersionRange },
}),
).rejects.toThrow(
`Snap "${MOCK_SNAP_ID}@1.0.0" is already installed, couldn't update to a version inside requested "${newVersionRange}" range.`,
);

expect(messenger.call).toHaveBeenCalledTimes(1);
expect(messenger.call).toHaveBeenCalledWith(
Expand All @@ -2391,10 +2379,6 @@ describe('SnapController', () => {
MOCK_SNAP_ID,
expect.objectContaining({ versionRange: newVersionRange }),
);

expect(result).toStrictEqual({
[MOCK_SNAP_ID]: { error: expect.any(EthereumRpcError) },
});
});

it('returns an error when a throw happens inside an update', async () => {
Expand All @@ -2417,9 +2401,11 @@ describe('SnapController', () => {
}),
);

const result = await controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: newVersionRange },
});
await expect(
controller.installSnaps(MOCK_ORIGIN, {
[MOCK_SNAP_ID]: { version: newVersionRange },
}),
).rejects.toThrow('foo');

expect(messenger.call).toHaveBeenCalledTimes(1);
expect(messenger.call).toHaveBeenCalledWith(
Expand All @@ -2432,10 +2418,133 @@ describe('SnapController', () => {
MOCK_SNAP_ID,
expect.objectContaining({ versionRange: newVersionRange }),
);
});

expect(result).toStrictEqual({
[MOCK_SNAP_ID]: { error: expect.anything() },
it('rolls back any updates & installs made during a failure scenario', async () => {
hmalik88 marked this conversation as resolved.
Show resolved Hide resolved
const snapId1 = 'npm:@metamask/example-snap1';
const snapId2 = 'npm:@metamask/example-snap2';
const snapId3 = 'npm:@metamask/example-snap3';
const oldVersion = '1.0.0';
const newVersion = '1.0.1';

const manifest = getSnapManifest();
const detect = jest
.fn()
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(
() =>
new LoopbackLocation({
manifest: getSnapManifest({ version: newVersion }),
}),
)
.mockImplementationOnce(
() =>
new LoopbackLocation({
manifest: getSnapManifest({
version: newVersion,
shasum: getSnapSourceShasum('foo'),
}),
files: [
new VirtualFile({
value: 'foo',
path: manifest.source.location.npm.filePath,
}),
new VirtualFile({
value: DEFAULT_SNAP_ICON,
path: manifest.source.location.npm.iconPath,
}),
],
}),
);

const [controller, service] = getSnapControllerWithEES(
getSnapControllerWithEESOptions({ detectSnapLocation: detect }),
);

await controller.installSnaps(MOCK_ORIGIN, { [snapId1]: {} });
await controller.installSnaps(MOCK_ORIGIN, { [snapId2]: {} });
await controller.stopSnap(snapId1);
await controller.stopSnap(snapId2);

expect(controller.get(snapId1)).toBeDefined();
expect(controller.get(snapId2)).toBeDefined();

await expect(
controller.installSnaps(MOCK_ORIGIN, {
[snapId3]: {},
[snapId1]: { version: newVersion },
[snapId2]: { version: newVersion },
}),
).rejects.toThrow(`Snap ${snapId2} crashed with updated source code.`);

expect(detect).toHaveBeenCalledTimes(5);

expect(controller.get(snapId3)).toBeUndefined();
expect(controller.get(snapId1)?.manifest.version).toBe(oldVersion);
expect(controller.get(snapId2)?.manifest.version).toBe(oldVersion);

controller.destroy();
await service.terminateAllSnaps();
});

it('will not create snapshots for already installed snaps that have invalid requested ranges', async () => {
const snapId1 = 'npm:@metamask/example-snap1';
const snapId2 = 'npm:@metamask/example-snap2';
const snapId3 = 'npm:@metamask/example-snap3';
const oldVersion = '1.0.0';
const newVersion = '1.0.1';
const olderVersion = '0.9.0';

const detect = jest
.fn()
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(() => new LoopbackLocation())
.mockImplementationOnce(
() =>
new LoopbackLocation({
manifest: getSnapManifest({ version: olderVersion }),
}),
);

const options = getSnapControllerWithEESOptions({
detectSnapLocation: detect,
});
const { messenger } = options;
const [controller, service] = getSnapControllerWithEES(options);

const listener = jest.fn();
messenger.subscribe('SnapController:snapRolledback' as any, listener);

await controller.installSnaps(MOCK_ORIGIN, { [snapId1]: {} });
await controller.installSnaps(MOCK_ORIGIN, { [snapId2]: {} });
await controller.stopSnap(snapId1);
await controller.stopSnap(snapId2);

expect(controller.get(snapId1)).toBeDefined();
expect(controller.get(snapId2)).toBeDefined();

await expect(
controller.installSnaps(MOCK_ORIGIN, {
[snapId3]: {},
[snapId1]: { version: olderVersion },
[snapId2]: { version: newVersion },
}),
).rejects.toThrow(
`Snap "${snapId1}@${oldVersion}" is already installed, couldn't update to a version inside requested "${olderVersion}" range.`,
);

expect(detect).toHaveBeenCalledTimes(4);

expect(controller.get(snapId3)).toBeUndefined();
expect(controller.get(snapId1)?.manifest.version).toBe(oldVersion);
expect(controller.get(snapId2)?.manifest.version).toBe(oldVersion);
expect(listener).toHaveBeenCalledTimes(0);

controller.destroy();
await service.terminateAllSnaps();
});
});

Expand All @@ -2459,9 +2568,11 @@ describe('SnapController', () => {
controller.updateSnap(
MOCK_ORIGIN,
MOCK_SNAP_ID,
'this is not a version',
'this is not a version' as SemVerRange,
),
).rejects.toThrow('Received invalid snap version range');
).rejects.toThrow(
'Received invalid snap version range: "this is not a version"',
hmalik88 marked this conversation as resolved.
Show resolved Hide resolved
);
});

it('throws an error if the new version of the snap is blocked', async () => {
Expand Down