Skip to content
Merged
8 changes: 4 additions & 4 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 93.06,
"functions": 96.54,
"lines": 98.02,
"statements": 97.74
"branches": 92.41,
"functions": 96.56,
"lines": 98,
"statements": 97.72
}
143 changes: 142 additions & 1 deletion packages/snaps-controllers/src/snaps/SnapController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
MOCK_SNAP_NAME,
DEFAULT_SOURCE_PATH,
DEFAULT_ICON_PATH,
TEST_SECRET_RECOVERY_PHRASE_BYTES,
} from '@metamask/snaps-utils/test-utils';
import type { SemVerRange, SemVerVersion, Json } from '@metamask/utils';
import {
Expand All @@ -60,6 +61,7 @@
AssertionError,
base64ToBytes,
stringToBytes,
createDeferredPromise,
} from '@metamask/utils';
import { File } from 'buffer';
import { webcrypto } from 'crypto';
Expand All @@ -78,6 +80,7 @@
getNodeEESMessenger,
getPersistedSnapsState,
getSnapController,
getSnapControllerEncryptor,
getSnapControllerMessenger,
getSnapControllerOptions,
getSnapControllerWithEES,
Expand All @@ -97,6 +100,7 @@
MOCK_WALLET_SNAP_PERMISSION,
MockSnapsRegistry,
sleep,
waitForStateChange,
} from '../test-utils';
import { delay } from '../utils';
import { LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS } from './constants';
Expand Down Expand Up @@ -1838,7 +1842,7 @@
});

// This isn't stable in CI unfortunately
it.skip('throws if the Snap is terminated while executing', async () => {

Check warning on line 1845 in packages/snaps-controllers/src/snaps/SnapController.test.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (@metamask/snaps-controllers)

Disabled test
const { manifest, sourceCode, svgIcon } =
await getMockSnapFilesWithUpdatedChecksum({
sourceCode: `
Expand Down Expand Up @@ -2117,6 +2121,59 @@
await service.terminateAllSnaps();
});

it('clears encrypted state of Snaps when the client is locked', async () => {
const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);

const state = { myVariable: 1 };

const mockEncryptedState = await encrypt(
ENCRYPTION_KEY,
state,
undefined,
undefined,
DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS,
);

const getMnemonic = jest
.fn()
.mockReturnValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);

const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: {
[MOCK_SNAP_ID]: getPersistedSnapObject(),
},
snapStates: {
[MOCK_SNAP_ID]: mockEncryptedState,
},
},
getMnemonic,
}),
);

expect(
await messenger.call('SnapController:getSnapState', MOCK_SNAP_ID, true),
).toStrictEqual(state);
expect(getMnemonic).toHaveBeenCalledTimes(1);

rootMessenger.publish('KeyringController:lock');

expect(
await messenger.call('SnapController:getSnapState', MOCK_SNAP_ID, true),
).toStrictEqual(state);

// We assume `getMnemonic` is called again because the controller needs to
// decrypt the state again. This is not an ideal way to test this, but it
// is the easiest to test this without exposing the internal state of the
// `SnapController`.
expect(getMnemonic).toHaveBeenCalledTimes(2);

snapController.destroy();
});

describe('handleRequest', () => {
it.each(
Object.keys(handlerEndowments).filter(
Expand Down Expand Up @@ -8801,6 +8858,7 @@
);

const newState = { myVariable: 2 };
const promise = waitForStateChange(messenger);

await messenger.call(
'SnapController:updateSnapState',
Expand All @@ -8817,6 +8875,8 @@
DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS,
);

await promise;

const result = await messenger.call(
'SnapController:getSnapState',
MOCK_SNAP_ID,
Expand All @@ -8831,7 +8891,7 @@
snapController.destroy();
});

it('different snaps use different encryption keys', async () => {
it('uses different encryption keys for different snaps', async () => {
const messenger = getSnapControllerMessenger();

const state = { foo: 'bar' };
Expand All @@ -8857,13 +8917,17 @@
true,
);

const promise = waitForStateChange(messenger);

await messenger.call(
'SnapController:updateSnapState',
MOCK_LOCAL_SNAP_ID,
state,
true,
);

await promise;

const encryptedState1 = await encrypt(
ENCRYPTION_KEY,
state,
Expand Down Expand Up @@ -9073,13 +9137,17 @@
undefined,
DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS,
);

const promise = waitForStateChange(messenger);
await messenger.call(
'SnapController:updateSnapState',
MOCK_SNAP_ID,
state,
true,
);

await promise;

expect(updateSnapStateSpy).toHaveBeenCalledTimes(1);
expect(snapController.state.snapStates[MOCK_SNAP_ID]).toStrictEqual(
mockEncryptedState,
Expand Down Expand Up @@ -9137,17 +9205,55 @@
);

const state = { foo: 'bar' };

const promise = waitForStateChange(messenger);
await messenger.call(
'SnapController:updateSnapState',
MOCK_SNAP_ID,
state,
true,
);

await promise;

expect(pbkdf2Sha512).toHaveBeenCalledTimes(1);

snapController.destroy();
});

it('logs an error message if the state fails to persist', async () => {
const messenger = getSnapControllerMessenger();

const errorValue = new Error('Failed to persist state.');
const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(),
},
// @ts-expect-error - Missing required properties.
encryptor: {
...getSnapControllerEncryptor(),
encryptWithKey: jest.fn().mockRejectedValue(errorValue),
},
}),
);

const { promise, resolve } = createDeferredPromise();
const error = jest.spyOn(console, 'error').mockImplementation(resolve);

await messenger.call(
'SnapController:updateSnapState',
MOCK_SNAP_ID,
{ foo: 'bar' },
true,
);

await promise;
expect(error).toHaveBeenCalledWith(errorValue);

snapController.destroy();
});
});

describe('SnapController:clearSnapState', () => {
Expand Down Expand Up @@ -9206,6 +9312,41 @@

snapController.destroy();
});

it('logs an error message if the state fails to persist', async () => {
const messenger = getSnapControllerMessenger();

const errorValue = new Error('Failed to persist state.');
const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(),
},
// @ts-expect-error - Missing required properties.
encryptor: {
...getSnapControllerEncryptor(),
encryptWithKey: jest.fn().mockRejectedValue(errorValue),
},
}),
);

const { promise, resolve } = createDeferredPromise();
const error = jest.spyOn(console, 'error').mockImplementation(resolve);

// @ts-expect-error - Property `update` is protected.
// eslint-disable-next-line jest/prefer-spy-on
snapController.update = jest.fn().mockImplementation(() => {
throw errorValue;
});

await messenger.call('SnapController:clearSnapState', MOCK_SNAP_ID, true);

await promise;
expect(error).toHaveBeenCalledWith(errorValue);

snapController.destroy();
});
});

describe('SnapController:updateBlockedSnaps', () => {
Expand Down
Loading
Loading