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
5 changes: 3 additions & 2 deletions packages/snap-account-service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The service messenger now requires the `KeyringController:withController` action.
- Add `handleKeyringSnapMessage` ([#8758](https://github.com/MetaMask/core/pull/8758))
- This will be the new entry point for consumer that needs to forward keyring events to a account management Snap (instead of using the legacy Snap keyring instance directly).
- Forward selected account group accounts ([#8763](https://github.com/MetaMask/core/pull/8763))
- Forward selected account group accounts ([#8763](https://github.com/MetaMask/core/pull/8763)), ([#8770](https://github.com/MetaMask/core/pull/8770))
- This logic used to live on the clients.
- The service messenger now requires the `KeyringController:unlock`, `AccountTreeController:selectedAccountGroupChange` events and `AccountTreeController:getAccountGroupObject` action.
- The service messenger now requires the `KeyringController:unlock`, `AccountTreeController:selectedAccountGroupChange`, `AccountTreeController:accountGroup{Created,Updated,Removed}` events.
- The service messenger now requires the `AccountTreeController:getSelectedAccountGroup` and `AccountTreeController:getAccountGroupObject` actions.

### Changed

Expand Down
186 changes: 176 additions & 10 deletions packages/snap-account-service/src/SnapAccountService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ type MockTruncatedSnap = Pick<
'id' | 'initialPermissions' | 'enabled' | 'blocked'
>;

/** Mock account group type for tests. */
type MockAccountGroup = Pick<AccountGroupObject, 'id' | 'accounts'>;

/** Mock Snap keyring type for tests. */
type MockSnapKeyring = {
type: KeyringTypes.snap;
handleKeyringSnapMessage?: jest.MockedFunction<
SnapKeyring['handleKeyringSnapMessage']
>;
setSelectedAccounts?: jest.MockedFunction<SnapKeyring['setSelectedAccounts']>;
};

type Mocks = {
// eslint-disable-next-line @typescript-eslint/naming-convention
SnapController: {
Expand Down Expand Up @@ -105,6 +117,9 @@ function getMessenger(
'KeyringController:stateChange',
'KeyringController:unlock',
'AccountTreeController:selectedAccountGroupChange',
'AccountTreeController:accountGroupCreated',
'AccountTreeController:accountGroupUpdated',
'AccountTreeController:accountGroupRemoved',
],
});
return messenger;
Expand Down Expand Up @@ -202,11 +217,57 @@ function buildSnap(id: string, hasKeyring: boolean): TruncatedSnap {
/**
* Builds a minimal `AccountGroupObject` for tests.
*
* @param id - The group ID.
* @param accounts - The list of account IDs in the group.
* @returns A minimal `AccountGroupObject`.
*/
function buildGroup(accounts: string[]): AccountGroupObject {
return { accounts } as unknown as AccountGroupObject;
function buildGroup(
id: AccountGroupId,
accounts: string[],
): AccountGroupObject {
return { id, accounts } as MockAccountGroup as AccountGroupObject;
}

/**
* Publishes an AccountTreeController accountGroupCreated event on the root
* messenger.
*
* @param rootMessenger - The root messenger.
* @param group - The created account group.
*/
function publishAccountGroupCreated(
rootMessenger: RootMessenger,
group: AccountGroupObject,
): void {
rootMessenger.publish('AccountTreeController:accountGroupCreated', group);
}

/**
* Publishes an AccountTreeController accountGroupUpdated event on the root
* messenger.
*
* @param rootMessenger - The root messenger.
* @param group - The updated account group.
*/
function publishAccountGroupUpdated(
rootMessenger: RootMessenger,
group: AccountGroupObject,
): void {
rootMessenger.publish('AccountTreeController:accountGroupUpdated', group);
}

/**
* Publishes an AccountTreeController accountGroupRemoved event on the root
* messenger.
*
* @param rootMessenger - The root messenger.
* @param groupId - The removed account group ID.
*/
function publishAccountGroupRemoved(
rootMessenger: RootMessenger,
groupId: AccountGroupId,
): void {
rootMessenger.publish('AccountTreeController:accountGroupRemoved', groupId);
}

/**
Expand Down Expand Up @@ -277,7 +338,7 @@ function mockLegacySnapKeyring(
>;
},
): void {
const snapKeyring = {
const snapKeyring: MockSnapKeyring = {
type: KeyringTypes.snap,
handleKeyringSnapMessage,
setSelectedAccounts,
Expand All @@ -287,7 +348,7 @@ function mockLegacySnapKeyring(
get keyrings() {
return Object.freeze([
{
keyring: snapKeyring as unknown as KeyringEntry['keyring'],
keyring: snapKeyring as KeyringEntry['keyring'],
metadata: { id: 'id-snap', name: KeyringTypes.snap },
},
]);
Expand Down Expand Up @@ -642,7 +703,7 @@ describe('SnapAccountService', () => {
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getAccountGroupObject.mockReturnValue(
buildGroup(MOCK_ACCOUNTS),
buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS),
);
expect(service).toBeDefined();

Expand Down Expand Up @@ -694,7 +755,7 @@ describe('SnapAccountService', () => {
const setSelectedAccounts = jest.fn().mockRejectedValue(error);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getAccountGroupObject.mockReturnValue(
buildGroup(MOCK_ACCOUNTS),
buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS),
);
const consoleErrorSpy = jest
.spyOn(console, 'error')
Expand All @@ -706,7 +767,7 @@ describe('SnapAccountService', () => {

expect(setSelectedAccounts).toHaveBeenCalledWith(MOCK_ACCOUNTS);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error handling selected account group change:',
'Error forwarding selected accounts:',
error,
);

Expand All @@ -729,7 +790,7 @@ describe('SnapAccountService', () => {
MOCK_GROUP_ID,
);
mocks.AccountTreeController.getAccountGroupObject.mockReturnValue(
buildGroup(MOCK_ACCOUNTS),
buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS),
);
expect(service).toBeDefined();

Expand Down Expand Up @@ -770,7 +831,7 @@ describe('SnapAccountService', () => {
MOCK_GROUP_ID,
);
mocks.AccountTreeController.getAccountGroupObject.mockReturnValue(
buildGroup(MOCK_ACCOUNTS),
buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS),
);
const consoleErrorSpy = jest
.spyOn(console, 'error')
Expand All @@ -782,11 +843,116 @@ describe('SnapAccountService', () => {

expect(setSelectedAccounts).toHaveBeenCalledWith(MOCK_ACCOUNTS);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error forwarding selected account group on unlock:',
'Error forwarding selected accounts:',
error,
);

consoleErrorSpy.mockRestore();
});
});

describe.each([
['accountGroupCreated', publishAccountGroupCreated] as const,
['accountGroupUpdated', publishAccountGroupUpdated] as const,
])('on AccountTreeController:%s', (_eventName, publishEvent) => {
const MOCK_GROUP_ID = 'keyring:01JABC/group-1' as AccountGroupId;
const OTHER_GROUP_ID = 'keyring:01JABC/group-2' as AccountGroupId;
const MOCK_ACCOUNTS = [
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
];

it('forwards the accounts from the event payload when the affected group is the selected one', async () => {
const { service, rootMessenger, mocks } = setup();
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getSelectedAccountGroup.mockReturnValue(
MOCK_GROUP_ID,
);
expect(service).toBeDefined();

publishEvent(rootMessenger, buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS));
await flushMicrotasks();

expect(
mocks.AccountTreeController.getAccountGroupObject,
).not.toHaveBeenCalled();
expect(setSelectedAccounts).toHaveBeenCalledWith(MOCK_ACCOUNTS);
});

it('does nothing when the affected group is not the selected one', async () => {
const { service, rootMessenger, mocks } = setup();
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getSelectedAccountGroup.mockReturnValue(
OTHER_GROUP_ID,
);
expect(service).toBeDefined();

publishEvent(rootMessenger, buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS));
await flushMicrotasks();

expect(
mocks.AccountTreeController.getAccountGroupObject,
).not.toHaveBeenCalled();
expect(setSelectedAccounts).not.toHaveBeenCalled();
});

it('does nothing when no account group is selected', async () => {
const { service, rootMessenger, mocks } = setup();
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getSelectedAccountGroup.mockReturnValue('');
expect(service).toBeDefined();

publishEvent(rootMessenger, buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS));
await flushMicrotasks();

expect(
mocks.AccountTreeController.getAccountGroupObject,
).not.toHaveBeenCalled();
expect(setSelectedAccounts).not.toHaveBeenCalled();
});
});

describe('on AccountTreeController:accountGroupRemoved', () => {
const MOCK_GROUP_ID = 'keyring:01JABC/group-1' as AccountGroupId;
const OTHER_GROUP_ID = 'keyring:01JABC/group-2' as AccountGroupId;

it('clears the selected accounts when the removed group is the selected one', async () => {
const { service, rootMessenger, mocks } = setup();
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getSelectedAccountGroup.mockReturnValue(
MOCK_GROUP_ID,
);
expect(service).toBeDefined();

publishAccountGroupRemoved(rootMessenger, MOCK_GROUP_ID);
await flushMicrotasks();

expect(
mocks.AccountTreeController.getAccountGroupObject,
).not.toHaveBeenCalled();
expect(setSelectedAccounts).toHaveBeenCalledWith([]);
});

it('does nothing when the removed group is not the selected one', async () => {
const { service, rootMessenger, mocks } = setup();
const setSelectedAccounts = jest.fn().mockResolvedValue(undefined);
mockLegacySnapKeyring(mocks, { setSelectedAccounts });
mocks.AccountTreeController.getSelectedAccountGroup.mockReturnValue(
OTHER_GROUP_ID,
);
expect(service).toBeDefined();

publishAccountGroupRemoved(rootMessenger, MOCK_GROUP_ID);
await flushMicrotasks();

expect(
mocks.AccountTreeController.getAccountGroupObject,
).not.toHaveBeenCalled();
expect(setSelectedAccounts).not.toHaveBeenCalled();
});
});
});
Loading
Loading