Skip to content

Commit 3840f34

Browse files
committed
test: add missing tests
1 parent 64b22ec commit 3840f34

File tree

2 files changed

+328
-1
lines changed

2 files changed

+328
-1
lines changed

packages/keyring-snap-bridge/src/SnapKeyring.test.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
AccountTransactionsUpdatedEventPayload,
1111
AccountAssetListUpdatedEventPayload,
1212
MetaMaskOptions,
13+
CreateAccountOptions,
1314
} from '@metamask/keyring-api';
1415
import {
1516
EthScope,
@@ -28,6 +29,7 @@ import {
2829
TrxScope,
2930
TrxMethod,
3031
TrxAccountType,
32+
AccountCreationType,
3133
} from '@metamask/keyring-api';
3234
import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk';
3335
import type { JsonRpcRequest } from '@metamask/keyring-utils';
@@ -2520,6 +2522,224 @@ describe('SnapKeyring', () => {
25202522
});
25212523
});
25222524

2525+
describe('createAccounts', () => {
2526+
const newAccount1 = {
2527+
...newEthEoaAccount,
2528+
id: 'aa11bb22-cc33-4d44-8e55-ff6677889900',
2529+
address: '0xaabbccddee00112233445566778899aabbccddee',
2530+
};
2531+
const newAccount2 = {
2532+
...newEthEoaAccount,
2533+
id: 'bb11bb22-cc33-4d44-9e55-ff6677889900',
2534+
address: '0xbbccddee00112233445566778899aabbccddeeff',
2535+
};
2536+
const newAccount3 = {
2537+
...newEthEoaAccount,
2538+
id: 'cc11bb22-cc33-4d44-ae55-ff6677889900',
2539+
address: '0xccddee00112233445566778899aabbccddee0011',
2540+
};
2541+
2542+
const entropySource = '01JQCAKR17JARQXZ0NDP760N1K';
2543+
2544+
const snapMetadata = {
2545+
manifest: {
2546+
proposedName: 'snap-name',
2547+
},
2548+
id: snapId,
2549+
enabled: true,
2550+
};
2551+
2552+
it('creates multiple accounts', async () => {
2553+
mockCallbacks.addAccount.mockClear();
2554+
mockCallbacks.saveState.mockClear();
2555+
2556+
mockMessenger.get.mockReturnValue(snapMetadata);
2557+
2558+
const accountsToCreate = [newAccount1, newAccount2, newAccount3];
2559+
2560+
mockMessengerHandleRequest({
2561+
[KeyringRpcMethod.CreateAccounts]: async () => {
2562+
// Unlike createAccount, createAccounts does NOT emit AccountCreated events
2563+
// for each account. It returns all accounts directly.
2564+
return accountsToCreate;
2565+
},
2566+
});
2567+
2568+
const options: CreateAccountOptions = {
2569+
type: AccountCreationType.Bip44DeriveIndexRange,
2570+
entropySource,
2571+
range: {
2572+
from: 0,
2573+
to: 2,
2574+
},
2575+
};
2576+
const result = await keyring.createAccounts(snapId, options);
2577+
2578+
expect(mockMessenger.handleRequest).toHaveBeenLastCalledWith(
2579+
mockKeyringRpcRequest(KeyringRpcMethod.CreateAccounts, options),
2580+
);
2581+
2582+
// Verify all accounts were returned
2583+
expect(result).toStrictEqual(accountsToCreate);
2584+
2585+
// Verify all accounts were added to the internal state
2586+
for (const account of accountsToCreate) {
2587+
expect(keyring.getAccountByAddress(account.address)).toMatchObject({
2588+
...account,
2589+
metadata: expect.objectContaining({
2590+
snap: expect.objectContaining({
2591+
id: snapId,
2592+
}),
2593+
}),
2594+
});
2595+
}
2596+
2597+
// Verify state was saved once after adding all accounts
2598+
expect(mockCallbacks.saveState).toHaveBeenCalled();
2599+
2600+
// IMPORTANT: Unlike createAccount, createAccounts does NOT call addAccount callback
2601+
// because accounts are created in batch
2602+
expect(mockCallbacks.addAccount).not.toHaveBeenCalled();
2603+
});
2604+
2605+
it('creates a single account through createAccounts', async () => {
2606+
mockCallbacks.addAccount.mockClear();
2607+
mockCallbacks.saveState.mockClear();
2608+
2609+
mockMessenger.get.mockReturnValue(snapMetadata);
2610+
2611+
const accountToCreate = [newAccount1];
2612+
2613+
mockMessengerHandleRequest({
2614+
[KeyringRpcMethod.CreateAccounts]: async () => accountToCreate,
2615+
});
2616+
2617+
const options: CreateAccountOptions = {
2618+
type: AccountCreationType.Bip44DeriveIndex,
2619+
groupIndex: 0,
2620+
entropySource,
2621+
};
2622+
const result = await keyring.createAccounts(snapId, options);
2623+
2624+
expect(mockMessenger.handleRequest).toHaveBeenLastCalledWith(
2625+
mockKeyringRpcRequest(KeyringRpcMethod.CreateAccounts, options),
2626+
);
2627+
2628+
expect(result).toStrictEqual(accountToCreate);
2629+
expect(result).toHaveLength(1);
2630+
2631+
// Verify the account was added to the internal state
2632+
expect(keyring.getAccountByAddress(newAccount1.address)).toMatchObject({
2633+
...newAccount1,
2634+
metadata: expect.objectContaining({
2635+
snap: expect.objectContaining({
2636+
id: snapId,
2637+
}),
2638+
}),
2639+
});
2640+
2641+
expect(mockCallbacks.saveState).toHaveBeenCalled();
2642+
expect(mockCallbacks.addAccount).not.toHaveBeenCalled();
2643+
});
2644+
2645+
it('creates accounts with custom options', async () => {
2646+
mockCallbacks.addAccount.mockClear();
2647+
mockCallbacks.saveState.mockClear();
2648+
2649+
const accountsToCreate = [newAccount1, newAccount2];
2650+
const options: CreateAccountOptions = {
2651+
type: AccountCreationType.Custom,
2652+
};
2653+
2654+
mockMessengerHandleRequest({
2655+
[KeyringRpcMethod.CreateAccounts]: async () => accountsToCreate,
2656+
});
2657+
2658+
const result = await keyring.createAccounts(snapId, options);
2659+
2660+
expect(mockMessenger.handleRequest).toHaveBeenLastCalledWith(
2661+
mockKeyringRpcRequest(KeyringRpcMethod.CreateAccounts, options),
2662+
);
2663+
2664+
expect(result).toStrictEqual(accountsToCreate);
2665+
expect(mockCallbacks.saveState).toHaveBeenCalled();
2666+
expect(mockCallbacks.addAccount).not.toHaveBeenCalled();
2667+
});
2668+
2669+
it('handles empty response from Snap', async () => {
2670+
mockCallbacks.addAccount.mockClear();
2671+
mockCallbacks.saveState.mockClear();
2672+
2673+
mockMessengerHandleRequest({
2674+
[KeyringRpcMethod.CreateAccounts]: async () => [],
2675+
});
2676+
2677+
const options: CreateAccountOptions = {
2678+
type: AccountCreationType.Bip44DeriveIndex,
2679+
entropySource,
2680+
groupIndex: 0,
2681+
};
2682+
const result = await keyring.createAccounts(snapId, options);
2683+
2684+
expect(result).toStrictEqual([]);
2685+
expect(mockCallbacks.saveState).toHaveBeenCalled();
2686+
expect(mockCallbacks.addAccount).not.toHaveBeenCalled();
2687+
});
2688+
2689+
it('handles errors from Snap', async () => {
2690+
mockCallbacks.addAccount.mockClear();
2691+
mockCallbacks.saveState.mockClear();
2692+
2693+
const errorMessage = 'Failed to create accounts';
2694+
2695+
mockMessengerHandleRequest({
2696+
[KeyringRpcMethod.CreateAccounts]: async () => {
2697+
throw new Error(errorMessage);
2698+
},
2699+
});
2700+
2701+
const options: CreateAccountOptions = {
2702+
type: AccountCreationType.Bip44DeriveIndex,
2703+
entropySource,
2704+
groupIndex: 0,
2705+
};
2706+
await expect(keyring.createAccounts(snapId, options)).rejects.toThrow(
2707+
errorMessage,
2708+
);
2709+
2710+
// State should not be saved if account creation fails
2711+
expect(mockCallbacks.saveState).not.toHaveBeenCalled();
2712+
expect(mockCallbacks.addAccount).not.toHaveBeenCalled();
2713+
});
2714+
2715+
it('adds all accounts to the internal map with correct snapId', async () => {
2716+
mockCallbacks.addAccount.mockClear();
2717+
mockCallbacks.saveState.mockClear();
2718+
2719+
mockMessenger.get.mockReturnValue(snapMetadata);
2720+
2721+
const accountsToCreate = [newAccount1, newAccount2];
2722+
2723+
mockMessengerHandleRequest({
2724+
[KeyringRpcMethod.CreateAccounts]: async () => accountsToCreate,
2725+
});
2726+
2727+
const options: CreateAccountOptions = {
2728+
type: AccountCreationType.Bip44DeriveIndex,
2729+
entropySource,
2730+
groupIndex: 0,
2731+
};
2732+
await keyring.createAccounts(snapId, options);
2733+
2734+
// Verify each account is mapped to the correct snapId
2735+
for (const account of accountsToCreate) {
2736+
const createdAccount = keyring.getAccountByAddress(account.address);
2737+
expect(createdAccount).toBeDefined();
2738+
expect(createdAccount?.metadata.snap?.id).toBe(snapId);
2739+
}
2740+
});
2741+
});
2742+
25232743
describe('resolveAccountAddress', () => {
25242744
const scope = toCaipChainId(
25252745
KnownCaipNamespace.Eip155,

packages/keyring-snap-client/src/KeyringClient.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { BtcMethod, BtcScope, KeyringRpcMethod } from '@metamask/keyring-api';
1+
import {
2+
AccountCreationType,
3+
BtcMethod,
4+
BtcScope,
5+
KeyringRpcMethod,
6+
} from '@metamask/keyring-api';
27
import type {
38
KeyringAccount,
49
KeyringRequest,
@@ -7,6 +12,7 @@ import type {
712
CaipAssetType,
813
CaipAssetTypeOrId,
914
DiscoveredAccount,
15+
CreateAccountOptions,
1016
} from '@metamask/keyring-api';
1117
import type { JsonRpcRequest } from '@metamask/keyring-utils';
1218

@@ -270,6 +276,107 @@ describe('KeyringClient', () => {
270276

271277
const client = keyringClient;
272278

279+
describe('createAccounts', () => {
280+
const entropySource = '01JQCAKR17JARQXZ0NDP760N1K';
281+
282+
it('should send a request to create multiple accounts and return the response', async () => {
283+
const expectedResponse: KeyringAccount[] = [
284+
{
285+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
286+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
287+
options: {},
288+
methods: [],
289+
scopes: ['eip155:0'],
290+
type: 'eip155:eoa',
291+
},
292+
{
293+
id: '6d9e5e9a-8f9c-4b3a-9e3a-1e5c7f8a9b0c',
294+
address: '0x1234567890123456789012345678901234567890',
295+
options: {},
296+
methods: [],
297+
scopes: ['eip155:0'],
298+
type: 'eip155:eoa',
299+
},
300+
];
301+
302+
const options: CreateAccountOptions = {
303+
type: AccountCreationType.Bip44DeriveIndexRange,
304+
entropySource,
305+
range: {
306+
from: 0,
307+
to: 2,
308+
},
309+
};
310+
311+
mockSender.send.mockResolvedValue(expectedResponse);
312+
const accounts = await client.createAccounts(options);
313+
expect(mockSender.send).toHaveBeenCalledWith({
314+
jsonrpc: '2.0',
315+
id: expect.any(String),
316+
method: 'keyring_createAccounts',
317+
params: options,
318+
});
319+
expect(accounts).toStrictEqual(expectedResponse);
320+
});
321+
322+
it('should handle creating accounts with a single index', async () => {
323+
const expectedResponse: KeyringAccount[] = [
324+
{
325+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
326+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
327+
options: {},
328+
methods: [],
329+
scopes: ['eip155:0'],
330+
type: 'eip155:eoa',
331+
},
332+
];
333+
334+
const options: CreateAccountOptions = {
335+
type: AccountCreationType.Bip44DeriveIndex,
336+
groupIndex: 0,
337+
entropySource,
338+
};
339+
340+
mockSender.send.mockResolvedValue(expectedResponse);
341+
const accounts = await client.createAccounts(options);
342+
expect(mockSender.send).toHaveBeenCalledWith({
343+
jsonrpc: '2.0',
344+
id: expect.any(String),
345+
method: 'keyring_createAccounts',
346+
params: options,
347+
});
348+
expect(accounts).toStrictEqual(expectedResponse);
349+
expect(accounts).toHaveLength(1);
350+
});
351+
352+
it('should handle creating accounts with custom options', async () => {
353+
const expectedResponse: KeyringAccount[] = [
354+
{
355+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
356+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
357+
options: {},
358+
methods: [],
359+
scopes: ['eip155:0'],
360+
type: 'eip155:eoa',
361+
},
362+
];
363+
364+
const options: CreateAccountOptions = {
365+
type: AccountCreationType.Custom,
366+
};
367+
368+
mockSender.send.mockResolvedValue(expectedResponse);
369+
const accounts = await client.createAccounts(options);
370+
expect(mockSender.send).toHaveBeenCalledWith({
371+
jsonrpc: '2.0',
372+
id: expect.any(String),
373+
method: 'keyring_createAccounts',
374+
params: options,
375+
});
376+
expect(accounts).toStrictEqual(expectedResponse);
377+
});
378+
});
379+
273380
describe('discoverAccounts', () => {
274381
const scopes = [BtcScope.Mainnet, BtcScope.Testnet];
275382
const entropySource = '01JQCAKR17JARQXZ0NDP760N1K';

0 commit comments

Comments
 (0)