@@ -19,7 +19,10 @@ import {
19
19
type AllowedActions ,
20
20
type AllowedEvents ,
21
21
} from './AccountGroupController' ;
22
- import type { AccountGroupControllerMessenger } from './AccountGroupController' ;
22
+ import type {
23
+ AccountGroupControllerMessenger ,
24
+ AccountGroupId ,
25
+ } from './AccountGroupController' ;
23
26
24
27
const ETH_EOA_METHODS = [
25
28
EthMethod . PersonalSign ,
@@ -31,7 +34,9 @@ const ETH_EOA_METHODS = [
31
34
] as const ;
32
35
33
36
/**
37
+ * Creates a new root messenger instance for testing.
34
38
*
39
+ * @returns A new Messenger instance.
35
40
*/
36
41
function getRootMessenger ( ) {
37
42
return new Messenger <
@@ -41,8 +46,10 @@ function getRootMessenger() {
41
46
}
42
47
43
48
/**
49
+ * Retrieves a restricted messenger for the AccountGroupController.
44
50
*
45
- * @param messenger
51
+ * @param messenger - The root messenger instance. Defaults to a new Messenger created by getRootMessenger().
52
+ * @returns The restricted messenger for the AccountGroupController.
46
53
*/
47
54
function getAccountGroupControllerMessenger (
48
55
messenger = getRootMessenger ( ) ,
@@ -58,11 +65,12 @@ function getAccountGroupControllerMessenger(
58
65
}
59
66
60
67
/**
68
+ * Sets up the AccountGroupController for testing.
61
69
*
62
- * @param options0
63
- * @param options0.initialState
64
- * @param options0 .messenger
65
- * @param options0.state
70
+ * @param options - Configuration options for setup.
71
+ * @param options.state - Partial initial state for the controller. Defaults to empty object.
72
+ * @param options .messenger - An optional messenger instance to use. Defaults to a new Messenger.
73
+ * @returns An object containing the controller instance and the messenger.
66
74
*/
67
75
function setup ( {
68
76
state = { } ,
@@ -168,39 +176,184 @@ const MOCK_SNAP_ACCOUNT_2: InternalAccount = {
168
176
} ,
169
177
} ;
170
178
179
+ const MOCK_HARDWARE_ACCOUNT_1 : InternalAccount = {
180
+ id : 'mock-hardware-id-1' ,
181
+ address : '0x123' ,
182
+ options : { } ,
183
+ methods : [ ...ETH_EOA_METHODS ] ,
184
+ type : EthAccountType . Eoa ,
185
+ scopes : [ EthScope . Eoa ] ,
186
+ metadata : {
187
+ name : '' ,
188
+ keyring : { type : KeyringTypes . ledger } ,
189
+ importTime : 1691565967600 ,
190
+ lastSelected : 1955565967656 ,
191
+ } ,
192
+ } ;
193
+
171
194
describe ( 'AccountGroupController' , ( ) => {
172
- it ( 'group accounts according to the rules' , async ( ) => {
173
- const { controller, messenger } = setup ( { } ) ;
174
-
175
- messenger . registerActionHandler (
176
- 'AccountsController:listMultichainAccounts' ,
177
- ( ) => {
178
- return [
179
- MOCK_HD_ACCOUNT_1 ,
180
- MOCK_HD_ACCOUNT_2 ,
181
- MOCK_SNAP_ACCOUNT_1 ,
182
- MOCK_SNAP_ACCOUNT_2 ,
183
- ] ;
184
- } ,
185
- ) ;
186
-
187
- await controller . init ( ) ;
188
-
189
- expect ( controller . state ) . toStrictEqual ( {
190
- accountGroups : {
191
- groups : {
192
- 'mock-keyring-id-1' : {
193
- [ DEFAULT_SUB_GROUP ] : [ MOCK_HD_ACCOUNT_1 . id ] ,
195
+ describe ( 'updateAccountGroups' , ( ) => {
196
+ it ( 'should group accounts by entropy source, then snapId, then wallet type' , async ( ) => {
197
+ const { controller, messenger } = setup ( { } ) ;
198
+
199
+ messenger . registerActionHandler (
200
+ 'AccountsController:listMultichainAccounts' ,
201
+ ( ) => {
202
+ return [
203
+ MOCK_HD_ACCOUNT_1 ,
204
+ MOCK_HD_ACCOUNT_2 ,
205
+ MOCK_SNAP_ACCOUNT_1 ,
206
+ MOCK_SNAP_ACCOUNT_2 ,
207
+ MOCK_HARDWARE_ACCOUNT_1 ,
208
+ ] ;
209
+ } ,
210
+ ) ;
211
+
212
+ await controller . updateAccountGroups ( ) ;
213
+
214
+ expect ( controller . state . accountGroups . groups ) . toStrictEqual ( {
215
+ 'mock-keyring-id-1' : {
216
+ [ DEFAULT_SUB_GROUP ] : [ MOCK_HD_ACCOUNT_1 . id ] ,
217
+ } ,
218
+ 'mock-keyring-id-2' : {
219
+ [ DEFAULT_SUB_GROUP ] : [ MOCK_HD_ACCOUNT_2 . id , MOCK_SNAP_ACCOUNT_1 . id ] ,
220
+ } ,
221
+ 'mock-snap-id-2' : {
222
+ [ DEFAULT_SUB_GROUP ] : [ MOCK_SNAP_ACCOUNT_2 . id ] ,
223
+ } ,
224
+ [ KeyringTypes . ledger ] : {
225
+ [ DEFAULT_SUB_GROUP ] : [ MOCK_HARDWARE_ACCOUNT_1 . id ] ,
226
+ } ,
227
+ } ) ;
228
+ } ) ;
229
+
230
+ it ( 'should warn and fall back to wallet type grouping if an HD account is missing entropySource' , async ( ) => {
231
+ const consoleWarnSpy = jest
232
+ . spyOn ( console , 'warn' )
233
+ . mockImplementation ( ( ) => undefined ) ;
234
+ const { controller, messenger } = setup ( { } ) ;
235
+
236
+ const mockHdAccountWithoutEntropy : InternalAccount = {
237
+ id : 'hd-account-no-entropy' ,
238
+ address : '0xHDADD' ,
239
+ metadata : {
240
+ name : 'HD Account Without Entropy' ,
241
+ keyring : {
242
+ type : KeyringTypes . hd ,
243
+ } ,
244
+ importTime : Date . now ( ) ,
245
+ lastSelected : Date . now ( ) ,
246
+ } ,
247
+ methods : [ ...ETH_EOA_METHODS ] ,
248
+ options : { } ,
249
+ type : EthAccountType . Eoa ,
250
+ scopes : [ EthScope . Eoa ] ,
251
+ } ;
252
+
253
+ const listAccountsMock = jest
254
+ . fn ( )
255
+ . mockReturnValue ( [ mockHdAccountWithoutEntropy ] ) ;
256
+ messenger . registerActionHandler (
257
+ 'AccountsController:listMultichainAccounts' ,
258
+ listAccountsMock ,
259
+ ) ;
260
+
261
+ await controller . updateAccountGroups ( ) ;
262
+
263
+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
264
+ "! Found an HD account with no entropy source: account won't be associated to its wallet" ,
265
+ ) ;
266
+
267
+ expect (
268
+ controller . state . accountGroups . groups [ KeyringTypes . hd ] ?. [
269
+ DEFAULT_SUB_GROUP
270
+ ] ,
271
+ ) . toContain ( mockHdAccountWithoutEntropy . id ) ;
272
+
273
+ expect (
274
+ controller . state . accountGroups . groups [
275
+ undefined as unknown as AccountGroupId
276
+ ] ,
277
+ ) . toBeUndefined ( ) ;
278
+
279
+ consoleWarnSpy . mockRestore ( ) ;
280
+ } ) ;
281
+ } ) ;
282
+
283
+ describe ( 'listAccountGroups' , ( ) => {
284
+ it ( 'should return an empty array if no groups exist' , async ( ) => {
285
+ const { controller } = setup ( {
286
+ state : {
287
+ accountGroups : { groups : { } } ,
288
+ accountGroupsMetadata : { } ,
289
+ } ,
290
+ } ) ;
291
+
292
+ const result = await controller . listAccountGroups ( ) ;
293
+ expect ( result ) . toEqual ( [ ] ) ;
294
+ } ) ;
295
+
296
+ it ( 'should correctly map group data and metadata to AccountGroup objects' , async ( ) => {
297
+ const group1Id = 'group-id-1' as AccountGroupId ;
298
+ const group2Id = 'group-id-2' as AccountGroupId ;
299
+
300
+ const initialState : AccountGroupControllerState = {
301
+ accountGroups : {
302
+ groups : {
303
+ [ group1Id ] : {
304
+ [ DEFAULT_SUB_GROUP ] : [ 'account-1' , 'account-2' ] ,
305
+ } ,
306
+ [ group2Id ] : {
307
+ 'sub-group-x' : [ 'account-3' ] ,
308
+ } ,
194
309
} ,
195
- 'mock-keyring-id-2' : {
196
- [ DEFAULT_SUB_GROUP ] : [ MOCK_HD_ACCOUNT_2 . id , MOCK_SNAP_ACCOUNT_1 . id ] ,
310
+ } ,
311
+ accountGroupsMetadata : {
312
+ [ group1Id ] : { name : 'Group 1 Name' } ,
313
+ [ group2Id ] : { name : 'Group 2 Name' } ,
314
+ } ,
315
+ } ;
316
+
317
+ const { controller } = setup ( { state : initialState } ) ;
318
+ const result = await controller . listAccountGroups ( ) ;
319
+
320
+ expect ( result ) . toEqual ( [
321
+ {
322
+ id : group1Id ,
323
+ name : 'Group 1 Name' ,
324
+ subGroups : {
325
+ [ DEFAULT_SUB_GROUP ] : [ 'account-1' , 'account-2' ] ,
197
326
} ,
198
- 'mock-snap-id-2' : {
199
- [ DEFAULT_SUB_GROUP ] : [ MOCK_SNAP_ACCOUNT_2 . id ] ,
327
+ } ,
328
+ {
329
+ id : group2Id ,
330
+ name : 'Group 2 Name' ,
331
+ subGroups : {
332
+ 'sub-group-x' : [ 'account-3' ] ,
200
333
} ,
201
334
} ,
202
- metadata : { } ,
203
- } ,
204
- } as AccountGroupControllerState ) ;
335
+ ] ) ;
336
+ } ) ;
337
+
338
+ it ( 'should throw a TypeError if metadata is missing for a group' , async ( ) => {
339
+ const groupIdWithMissingMetadata = 'group-missing-meta' as AccountGroupId ;
340
+ const initialState : Partial < AccountGroupControllerState > = {
341
+ accountGroups : {
342
+ groups : {
343
+ [ groupIdWithMissingMetadata ] : {
344
+ [ DEFAULT_SUB_GROUP ] : [ 'account-x' ] ,
345
+ } ,
346
+ } ,
347
+ } ,
348
+ // Metadata for groupIdWithMissingMetadata is deliberately omitted
349
+ accountGroupsMetadata : { } ,
350
+ } ;
351
+
352
+ const { controller } = setup ( { state : initialState } ) ;
353
+
354
+ // Current implementation will throw: Cannot read properties of undefined (reading 'name')
355
+ // which is a TypeError.
356
+ await expect ( controller . listAccountGroups ( ) ) . rejects . toThrow ( TypeError ) ;
357
+ } ) ;
205
358
} ) ;
206
359
} ) ;
0 commit comments