From 809749383976b3bc19c3c5b4e4d4f7ec013e9c8b Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Tue, 17 Dec 2024 23:10:44 +0100 Subject: [PATCH 1/9] feat: decouple account sync code from userstoragecontroller --- .../UserStorageController.test.ts | 1316 +--------------- .../user-storage/UserStorageController.ts | 397 +---- .../user-storage/__fixtures__/test-utils.ts | 62 + .../__fixtures__/mockAccounts.ts | 4 +- .../__fixtures__/test-utils.ts | 68 + .../constants.ts | 0 .../controller-integration.test.ts | 1378 +++++++++++++++++ .../account-syncing/controller-integration.ts | 363 +++++ .../setup-subscriptions.test.ts | 29 + .../account-syncing/setup-subscriptions.ts | 48 + .../account-syncing/sync-utils.test.ts | 153 ++ .../account-syncing/sync-utils.ts | 87 ++ .../user-storage/account-syncing/types.ts | 31 + .../account-syncing/utils.test.ts | 111 ++ .../utils.ts} | 29 +- .../accounts/user-storage.test.ts | 21 - 16 files changed, 2465 insertions(+), 1632 deletions(-) rename packages/profile-sync-controller/src/controllers/user-storage/{ => account-syncing}/__fixtures__/mockAccounts.ts (96%) create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/test-utils.ts rename packages/profile-sync-controller/src/controllers/user-storage/{accounts => account-syncing}/constants.ts (100%) create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/types.ts create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts rename packages/profile-sync-controller/src/controllers/user-storage/{accounts/user-storage.ts => account-syncing/utils.ts} (71%) delete mode 100644 packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.test.ts diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index 300d0e04d4..82e50f1a33 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -1,13 +1,8 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import type nock from 'nock'; -import encryption, { createSHA256Hash } from '../../shared/encryption'; import { USER_STORAGE_FEATURE_NAMES } from '../../shared/storage-schema'; -import { - MOCK_INTERNAL_ACCOUNTS, - MOCK_USER_STORAGE_ACCOUNTS, -} from './__fixtures__/mockAccounts'; -import { mockUserStorageMessenger as _mockUserStorageMessenger } from './__fixtures__/mockMessenger'; +import { mockUserStorageMessenger } from './__fixtures__/mockMessenger'; import { mockEndpointBatchUpsertUserStorage, mockEndpointGetUserStorage, @@ -22,13 +17,10 @@ import { MOCK_STORAGE_KEY, } from './__fixtures__/mockStorage'; import { waitFor } from './__fixtures__/test-utils'; -import * as AccountsUserStorageModule from './accounts/user-storage'; +import { mockUserStorageMessengerForAccountSyncing } from './account-syncing/__fixtures__/test-utils'; +import * as AccountSyncControllerIntegrationModule from './account-syncing/controller-integration'; import * as NetworkSyncIntegrationModule from './network-syncing/controller-integration'; -import type { - GetUserStorageAllFeatureEntriesResponse, - GetUserStorageResponse, - UserStorageBaseOptions, -} from './services'; +import type { UserStorageBaseOptions } from './services'; import UserStorageController, { defaultState } from './UserStorageController'; describe('user-storage/user-storage-controller - constructor() tests', () => { @@ -114,6 +106,7 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', () state: { isProfileSyncingEnabled: false, isProfileSyncingUpdateLoading: false, + isAccountSyncingInProgress: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, }, @@ -197,6 +190,7 @@ describe('user-storage/user-storage-controller - performGetStorageAllFeatureEntr isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -278,6 +272,7 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', () isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -383,6 +378,7 @@ describe('user-storage/user-storage-controller - performBatchSetStorage() tests' isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -485,6 +481,7 @@ describe('user-storage/user-storage-controller - performBatchDeleteStorage() tes isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -588,6 +585,7 @@ describe('user-storage/user-storage-controller - performDeleteStorage() tests', isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -688,6 +686,7 @@ describe('user-storage/user-storage-controller - performDeleteStorageAllFeatureE isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -780,6 +779,7 @@ describe('user-storage/user-storage-controller - getStorageKey() tests', () => { isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -826,6 +826,7 @@ describe('user-storage/user-storage-controller - enableProfileSyncing() tests', isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }, }); @@ -838,1165 +839,81 @@ describe('user-storage/user-storage-controller - enableProfileSyncing() tests', }); describe('user-storage/user-storage-controller - syncInternalAccountsWithUserStorage() tests', () => { - it('returns void if UserStorage is not enabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: mockEndpointGetUserStorage(), - }; - }; - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ + const arrangeMocks = () => { + const messengerMocks = mockUserStorageMessengerForAccountSyncing(); + const mockSyncInternalAccountsWithUserStorage = jest.spyOn( + AccountSyncControllerIntegrationModule, + 'syncInternalAccountsWithUserStorage', + ); + return { messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - env: { - isAccountSyncingEnabled: true, - }, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - hasAccountSyncingSyncedAtLeastOnce: false, - isAccountSyncingReadyToBeDispatched: false, - }, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - expect(messengerMocks.mockAccountsListAccounts).not.toHaveBeenCalled(); - }); - - it('returns void if account syncing feature flag is disabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; + mockSyncInternalAccountsWithUserStorage, }; + }; - const { messengerMocks, mockAPI } = await arrangeMocks(); + // NOTE the actual testing of the implementation is done in `controller-integration.ts` file. + // See relevant unit tests to see how this feature works and is tested + it('should invoke syncing from the integration module', async () => { + const { messenger, mockSyncInternalAccountsWithUserStorage } = + arrangeMocks(); const controller = new UserStorageController({ - messenger: messengerMocks.messenger, + messenger, getMetaMetricsState: () => true, env: { + // We're only verifying that calling this controller method will call the integration module + // The actual implementation is tested in the integration tests + // This is done to prevent creating unnecessary nock instances in this test isAccountSyncingEnabled: false, }, }); - await controller.syncInternalAccountsWithUserStorage(); - expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(false); - }); - - it('throws if AccountsController:listAccounts fails or returns an empty list', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: [], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await expect( - controller.syncInternalAccountsWithUserStorage(), - ).rejects.toThrow(expect.any(Error)); - - mockAPI.mockEndpointGetUserStorage.done(); - }); - - it('uploads accounts list to user storage if user storage is empty', async () => { - const mockUserStorageAccountsResponse = { - status: 404, - body: [], - }; - - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( - 0, - 2, - ) as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - mockUserStorageAccountsResponse, - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - const decryptedBody = await decryptBatchUpsertBody( - requestBody, - MOCK_STORAGE_KEY, - ); - - const expectedBody = createExpectedAccountSyncBatchUpsertBody( - MOCK_INTERNAL_ACCOUNTS.ALL.slice(0, 2).map((account) => [ - account.address, - account as InternalAccount, - ]), - MOCK_STORAGE_KEY, - ); - - expect(decryptedBody).toStrictEqual(expectedBody); - }, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - mockAPI.mockEndpointGetUserStorage.done(); - - expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); - expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(true); - }); - - it('creates internal accounts if user storage has more accounts. it also updates hasAccountSyncingSyncedAtLeastOnce accordingly', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchDeleteUserStorage: - mockEndpointBatchDeleteUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - if (typeof requestBody === 'string') { - return; - } - - const expectedBody = createExpectedAccountSyncBatchDeleteBody( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( - (account) => - !MOCK_INTERNAL_ACCOUNTS.ONE.find( - (internalAccount) => - internalAccount.address === account.a, - ), - ).map((account) => account.a), - MOCK_STORAGE_KEY, - ); - - expect(requestBody.batch_delete).toStrictEqual(expectedBody); - }, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, + mockSyncInternalAccountsWithUserStorage.mockImplementation(async () => { + return undefined; }); await controller.syncInternalAccountsWithUserStorage(); - mockAPI.mockEndpointGetUserStorage.done(); - - expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); - - expect(messengerMocks.mockKeyringAddNewAccount).toHaveBeenCalledTimes( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.length - - MOCK_INTERNAL_ACCOUNTS.ONE.length, - ); - - expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); - - expect(controller.state.hasAccountSyncingSyncedAtLeastOnce).toBe(true); - }); - - describe('handles corrupted user storage gracefully', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.TWO_DEFAULT_NAMES_WITH_ONE_BOGUS, - ), - }; - }; - - const arrangeMocksForBogusAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchDeleteUserStorage: - mockEndpointBatchDeleteUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - if (typeof requestBody === 'string') { - return; - } - - const expectedBody = createExpectedAccountSyncBatchDeleteBody( - [ - MOCK_USER_STORAGE_ACCOUNTS - .TWO_DEFAULT_NAMES_WITH_ONE_BOGUS[1].a, - ], - MOCK_STORAGE_KEY, - ); - - expect(requestBody.batch_delete).toStrictEqual(expectedBody); - }, - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - it('does not save the bogus account to user storage, and deletes it from user storage', async () => { - const { messengerMocks, mockAPI } = await arrangeMocksForBogusAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); - expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(false); - expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); - }); - - it('fires the onAccountSyncErroneousSituation callback in erroneous situations', async () => { - const onAccountSyncErroneousSituation = jest.fn(); - - const { messengerMocks } = await arrangeMocksForBogusAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - config: { - accountSyncing: { - onAccountSyncErroneousSituation, - }, - }, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - expect(onAccountSyncErroneousSituation).toHaveBeenCalledTimes(1); - }); - }); - - it('fires the onAccountAdded callback when adding an account', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchDeleteUserStorage: - mockEndpointBatchDeleteUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - if (typeof requestBody === 'string') { - return; - } - - const expectedBody = createExpectedAccountSyncBatchDeleteBody( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( - (account) => - !MOCK_INTERNAL_ACCOUNTS.ONE.find( - (internalAccount) => - internalAccount.address === account.a, - ), - ).map((account) => account.a), - MOCK_STORAGE_KEY, - ); - - expect(requestBody.batch_delete).toStrictEqual(expectedBody); - }, - ), - }, - }; - }; - - const onAccountAdded = jest.fn(); - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - config: { - accountSyncing: { - onAccountAdded, - }, - }, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect(onAccountAdded).toHaveBeenCalledTimes( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.length - - MOCK_INTERNAL_ACCOUNTS.ONE.length, - ); - - expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); - }); - - it('does not create internal accounts if user storage has less accounts', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.slice(0, 1), - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( - 0, - 2, - ) as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - mockAPI.mockEndpointBatchUpsertUserStorage.done(); - - expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); - expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(true); - - expect(messengerMocks.mockKeyringAddNewAccount).not.toHaveBeenCalled(); - }); - - describe('User storage name is a default name', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, - ), - }; - }; - - it('does not update the internal account name if both user storage and internal accounts have default names', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); - - it('does not update the internal account name if the internal account name is custom without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - mockAPI.mockEndpointBatchUpsertUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); - - it('does not update the internal account name if the internal account name is custom with last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - mockAPI.mockEndpointBatchUpsertUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); - }); - - describe('User storage name is a custom name without last updated', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, - ), - }; - }; - - it('updates the internal account name if the internal account name is a default name', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).toHaveBeenCalledWith( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED[0].i, - { - name: MOCK_USER_STORAGE_ACCOUNTS - .ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED[0].n, - }, - ); - }); - - it('does not update internal account name if both user storage and internal accounts have custom names without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); - - it('does not update the internal account name if the internal account name is custom with last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - mockAPI.mockEndpointBatchUpsertUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); - - it('fires the onAccountNameUpdated callback when renaming an internal account', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const onAccountNameUpdated = jest.fn(); - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - config: { - accountSyncing: { - onAccountNameUpdated, - }, - }, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect(onAccountNameUpdated).toHaveBeenCalledTimes(1); - }); - }); - - describe('User storage name is a custom name with last updated', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, - ), - }; - }; - - it('updates the internal account name if the internal account name is a default name', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).toHaveBeenCalledWith( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, - { - name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] - .n, - }, - ); - }); - - it('updates the internal account name and last updated if the internal account name is a custom name without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).toHaveBeenCalledWith( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, - { - name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] - .n, - nameLastUpdatedAt: - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].nlu, - }, - ); - }); - - it('updates the internal account name and last updated if the user storage account is more recent', async () => { - const arrangeMocksForAccounts = async () => { - const mockGetEntriesResponse = await mockUserStorageAccountsResponse(); - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - mockGetEntriesResponse, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).toHaveBeenCalledWith( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, - { - name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] - .n, - nameLastUpdatedAt: - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].nlu, - }, - ); - }); - - it('does not update the internal account if the user storage account is less recent', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - await controller.syncInternalAccountsWithUserStorage(); - - mockAPI.mockEndpointGetUserStorage.done(); - mockAPI.mockEndpointBatchUpsertUserStorage.done(); - - expect( - messengerMocks.mockAccountsUpdateAccountMetadata, - ).not.toHaveBeenCalled(); - }); + expect(mockSyncInternalAccountsWithUserStorage).toHaveBeenCalled(); }); }); describe('user-storage/user-storage-controller - saveInternalAccountToUserStorage() tests', () => { - it('returns void if UserStorage is not enabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - }; - }; - - const mapInternalAccountToUserStorageAccountMock = jest.spyOn( - AccountsUserStorageModule, - 'mapInternalAccountToUserStorageAccount', - ); - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - hasAccountSyncingSyncedAtLeastOnce: false, - isAccountSyncingReadyToBeDispatched: false, - }, - }); - - await controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + const arrangeMocks = () => { + const messengerMocks = mockUserStorageMessengerForAccountSyncing(); + const mockSaveInternalAccountToUserStorage = jest.spyOn( + AccountSyncControllerIntegrationModule, + 'saveInternalAccountToUserStorage', ); - - expect(mapInternalAccountToUserStorageAccountMock).not.toHaveBeenCalled(); - }); - - it('returns void if account syncing feature flag is disabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - ), - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ + return { messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: false, - }, - getMetaMetricsState: () => true, - }); - - await controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ); - - expect(mockAPI.isDone()).toBe(false); - }); - - it('saves an internal account to user storage', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - ), - }; + mockSaveInternalAccountToUserStorage, }; + }; - const { messengerMocks, mockAPI } = await arrangeMocks(); + // NOTE the actual testing of the implementation is done in `controller-integration.ts` file. + // See relevant unit tests to see how this feature works and is tested + it('should invoke syncing from the integration module', async () => { + const { messenger, mockSaveInternalAccountToUserStorage } = arrangeMocks(); const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, + messenger, getMetaMetricsState: () => true, - }); - - await controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ); - - expect(mockAPI.isDone()).toBe(true); - }); - - it('rejects if api call fails', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - { status: 500 }, - ), - }; - }; - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, env: { - isAccountSyncingEnabled: true, + // We're only verifying that calling this controller method will call the integration module + // The actual implementation is tested in the integration tests + // This is done to prevent creating unnecessary nock instances in this test + isAccountSyncingEnabled: false, }, - getMetaMetricsState: () => true, }); - await expect( - controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ), - ).rejects.toThrow(expect.any(Error)); - }); - - describe('it reacts to other controller events', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessenger({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - it('saves an internal account to user storage when the AccountsController:accountRenamed event is fired', async () => { - await arrangeMocksForAccounts(); - - const { baseMessenger, messenger } = mockUserStorageMessenger(); - - const controller = new UserStorageController({ - messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - // We need to sync at least once before we listen for other controller events - await controller.syncInternalAccountsWithUserStorage(); - - const mockSaveInternalAccountToUserStorage = jest - .spyOn(controller, 'saveInternalAccountToUserStorage') - .mockImplementation(); - - baseMessenger.publish( - 'AccountsController:accountRenamed', - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ); - - expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( - MOCK_INTERNAL_ACCOUNTS.ONE[0], - ); + mockSaveInternalAccountToUserStorage.mockImplementation(async () => { + return undefined; }); - it('saves an internal account to user storage when the AccountsController:accountAdded event is fired', async () => { - await arrangeMocksForAccounts(); - - const { baseMessenger, messenger } = mockUserStorageMessenger(); - - const controller = new UserStorageController({ - messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - - // We need to sync at least once before we listen for other controller events - await controller.syncInternalAccountsWithUserStorage(); - - const mockSaveInternalAccountToUserStorage = jest - .spyOn(controller, 'saveInternalAccountToUserStorage') - .mockImplementation(); + await controller.saveInternalAccountToUserStorage({ + id: '1', + } as InternalAccount); - baseMessenger.publish( - 'AccountsController:accountAdded', - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ); - - expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( - MOCK_INTERNAL_ACCOUNTS.ONE[0], - ); - }); + expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalled(); }); }); @@ -2079,122 +996,3 @@ describe('user-storage/user-storage-controller - syncNetworks() tests', () => { expect(controller.state.hasNetworkSyncingSyncedAtLeastOnce).toBe(true); }); }); - -/** - * Jest Mock Utility - create a mock user storage messenger - * - * @param options - options for the mock messenger - * @param options.accounts - options for the accounts part of the controller - * @param options.accounts.accountsList - list of accounts to return for the 'AccountsController:listAccounts' action - * @returns Mock User Storage Messenger - */ -function mockUserStorageMessenger(options?: { - accounts?: { - accountsList?: InternalAccount[]; - }; -}) { - const messengerMocks = _mockUserStorageMessenger(); - - messengerMocks.mockKeyringAddNewAccount.mockImplementation(async () => { - messengerMocks.baseMessenger.publish( - 'AccountsController:accountAdded', - MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - ); - return MOCK_INTERNAL_ACCOUNTS.ONE[0].address; - }); - - messengerMocks.mockAccountsListAccounts.mockReturnValue( - (options?.accounts?.accountsList ?? - MOCK_INTERNAL_ACCOUNTS.ALL) as InternalAccount[], - ); - - return messengerMocks; -} - -/** - * Test Utility - creates a realistic mock user-storage entry - * @param data - data to encrypt - * @returns user storage entry - */ -async function createMockUserStorageEntry( - data: unknown, -): Promise { - return { - HashedKey: 'HASHED_KEY', - Data: await encryption.encryptString( - JSON.stringify(data), - MOCK_STORAGE_KEY, - ), - }; -} - -/** - * Test Utility - creates a realistic mock user-storage get-all entry - * @param data - data array to encrypt - * @returns user storage entry - */ -async function createMockUserStorageEntries( - data: unknown[], -): Promise { - return await Promise.all(data.map((d) => createMockUserStorageEntry(d))); -} - -/** - * Test Utility - decrypts a realistic batch upsert payload - * @param requestBody - nock body - * @param storageKey - storage key - * @returns decrypted body - */ -async function decryptBatchUpsertBody( - requestBody: nock.Body, - storageKey: string, -) { - if (typeof requestBody === 'string') { - return requestBody; - } - return await Promise.all( - Object.entries(requestBody.data).map( - async ([entryKey, entryValue]) => { - return [ - entryKey, - await encryption.decryptString(entryValue, storageKey), - ]; - }, - ), - ); -} - -/** - * Test Utility - creates a realistic expected batch upsert payload - * @param data - data supposed to be upserted - * @param storageKey - storage key - * @returns expected body - */ -function createExpectedAccountSyncBatchUpsertBody( - data: [string, InternalAccount][], - storageKey: string, -) { - return data.map(([entryKey, entryValue]) => [ - createSHA256Hash(String(entryKey) + storageKey), - JSON.stringify( - AccountsUserStorageModule.mapInternalAccountToUserStorageAccount( - entryValue, - ), - ), - ]); -} - -/** - * Test Utility - creates a realistic expected batch delete payload - * @param data - data supposed to be deleted - * @param storageKey - storage key - * @returns expected body - */ -function createExpectedAccountSyncBatchDeleteBody( - data: string[], - storageKey: string, -) { - return data.map((entryKey) => - createSHA256Hash(String(entryKey) + storageKey), - ); -} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 74a3eab880..99c5606bdb 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -11,13 +11,11 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import { isEvmAccountType } from '@metamask/keyring-api'; import { type KeyringControllerGetStateAction, type KeyringControllerLockEvent, type KeyringControllerUnlockEvent, type KeyringControllerAddNewAccountAction, - KeyringTypes, } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { @@ -32,7 +30,6 @@ import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import { createSHA256Hash } from '../../shared/encryption'; import type { UserStorageFeatureKeys } from '../../shared/storage-schema'; import { - USER_STORAGE_FEATURE_NAMES, type UserStoragePathWithFeatureAndKey, type UserStoragePathWithFeatureOnly, } from '../../shared/storage-schema'; @@ -45,11 +42,11 @@ import type { AuthenticationControllerPerformSignIn, AuthenticationControllerPerformSignOut, } from '../authentication/AuthenticationController'; -import type { UserStorageAccount } from './accounts/user-storage'; import { - isNameDefaultAccountName, - mapInternalAccountToUserStorageAccount, -} from './accounts/user-storage'; + saveInternalAccountToUserStorage, + syncInternalAccountsWithUserStorage, +} from './account-syncing/controller-integration'; +import { setupAccountSyncingSubscriptions } from './account-syncing/setup-subscriptions'; import { performMainNetworkSync, startNetworkSyncing, @@ -97,6 +94,10 @@ export type UserStorageControllerState = { * Condition used by UI to determine if account syncing is ready to be dispatched. */ isAccountSyncingReadyToBeDispatched: boolean; + /** + * Condition used by UI to determine if account syncing is in progress. + */ + isAccountSyncingInProgress: boolean; /** * Condition used to ensure that we do not perform any network sync mutations until we have synced at least once */ @@ -108,6 +109,7 @@ export const defaultState: UserStorageControllerState = { isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, }; const metadata: StateMetadata = { @@ -127,6 +129,10 @@ const metadata: StateMetadata = { persist: false, anonymous: false, }, + isAccountSyncingInProgress: { + persist: false, + anonymous: false, + }, hasNetworkSyncingSyncedAtLeastOnce: { persist: true, anonymous: false, @@ -334,121 +340,6 @@ export default class UserStorageController extends BaseController< }, }; - #accounts = { - isAccountSyncingInProgress: false, - maxNumberOfAccountsToAdd: 0, - canSync: () => { - try { - this.#assertProfileSyncingEnabled(); - - if (this.#accounts.isAccountSyncingInProgress) { - return false; - } - - if (!this.#env.isAccountSyncingEnabled) { - return false; - } - - if (!this.#auth.isAuthEnabled()) { - return false; - } - - return true; - } catch { - return false; - } - }, - setupAccountSyncingSubscriptions: () => { - this.messagingSystem.subscribe( - 'AccountsController:accountAdded', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (account) => { - if ( - !this.#accounts.canSync() || - !this.state.hasAccountSyncingSyncedAtLeastOnce - ) { - return; - } - - await this.saveInternalAccountToUserStorage(account); - }, - ); - - this.messagingSystem.subscribe( - 'AccountsController:accountRenamed', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (account) => { - if ( - !this.#accounts.canSync() || - !this.state.hasAccountSyncingSyncedAtLeastOnce - ) { - return; - } - await this.saveInternalAccountToUserStorage(account); - }, - ); - }, - doesInternalAccountHaveCorrectKeyringType: (account: InternalAccount) => { - return account.metadata.keyring.type === KeyringTypes.hd; - }, - getInternalAccountsList: async (): Promise => { - // eslint-disable-next-line @typescript-eslint/await-thenable - const internalAccountsList = await this.messagingSystem.call( - 'AccountsController:listAccounts', - ); - - return internalAccountsList?.filter( - this.#accounts.doesInternalAccountHaveCorrectKeyringType, - ); - }, - getUserStorageAccountsList: async (): Promise< - UserStorageAccount[] | null - > => { - const rawAccountsListResponse = - await this.performGetStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - ); - - return ( - rawAccountsListResponse?.map((rawAccount) => JSON.parse(rawAccount)) ?? - null - ); - }, - saveInternalAccountToUserStorage: async ( - internalAccount: InternalAccount, - ) => { - // Map the internal account to the user storage account schema - const mappedAccount = - mapInternalAccountToUserStorageAccount(internalAccount); - - await this.performSetStorage( - // ESLint is confused here. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${USER_STORAGE_FEATURE_NAMES.accounts}.${internalAccount.address}`, - JSON.stringify(mappedAccount), - ); - }, - saveInternalAccountsListToUserStorage: async () => { - const internalAccountsList = - await this.#accounts.getInternalAccountsList(); - - if (!internalAccountsList) { - return; - } - - const internalAccountsListFormattedForUserStorage = - internalAccountsList.map(mapInternalAccountToUserStorageAccount); - - await this.performBatchSetStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - internalAccountsListFormattedForUserStorage.map((account) => [ - account.a, - JSON.stringify(account), - ]), - ); - }, - }; - #config?: ControllerConfig; #notificationServices = { @@ -516,14 +407,21 @@ export default class UserStorageController extends BaseController< this.#env.isNetworkSyncingEnabled = Boolean(env?.isNetworkSyncingEnabled); this.#config = config; - this.#accounts.maxNumberOfAccountsToAdd = - config?.accountSyncing?.maxNumberOfAccountsToAdd ?? 100; - this.getMetaMetricsState = getMetaMetricsState; this.#keyringController.setupLockedStateSubscriptions(); this.#registerMessageHandlers(); this.#nativeScryptCrypto = nativeScryptCrypto; - this.#accounts.setupAccountSyncingSubscriptions(); + + // Account Syncing + if (this.#env.isAccountSyncingEnabled) { + setupAccountSyncingSubscriptions( + { isAccountSyncingEnabled: true }, + { + getUserStorageControllerInstance: () => this, + getMessenger: () => this.messagingSystem, + }, + ); + } // Network Syncing if (this.#env.isNetworkSyncingEnabled) { @@ -923,7 +821,7 @@ export default class UserStorageController extends BaseController< }); } - private async setHasAccountSyncingSyncedAtLeastOnce( + async setHasAccountSyncingSyncedAtLeastOnce( hasAccountSyncingSyncedAtLeastOnce: boolean, ): Promise { this.update((state) => { @@ -932,7 +830,7 @@ export default class UserStorageController extends BaseController< }); } - public async setIsAccountSyncingReadyToBeDispatched( + async setIsAccountSyncingReadyToBeDispatched( isAccountSyncingReadyToBeDispatched: boolean, ): Promise { this.update((state) => { @@ -941,203 +839,42 @@ export default class UserStorageController extends BaseController< }); } + async setIsAccountSyncingInProgress( + isAccountSyncingInProgress: boolean, + ): Promise { + this.update((state) => { + state.isAccountSyncingInProgress = isAccountSyncingInProgress; + }); + } + /** * Syncs the internal accounts list with the user storage accounts list. * This method is used to make sure that the internal accounts list is up-to-date with the user storage accounts list and vice-versa. * It will add new accounts to the internal accounts list, update/merge conflicting names and re-upload the results in some cases to the user storage. */ async syncInternalAccountsWithUserStorage(): Promise { - if (!this.#accounts.canSync()) { - return; - } - - try { - this.#accounts.isAccountSyncingInProgress = true; - - const profileId = await this.#auth.getProfileId(); - - const userStorageAccountsList = - await this.#accounts.getUserStorageAccountsList(); - - if (!userStorageAccountsList || !userStorageAccountsList.length) { - await this.#accounts.saveInternalAccountsListToUserStorage(); - await this.setHasAccountSyncingSyncedAtLeastOnce(true); - return; - } - - // Prepare an array of internal accounts to be saved to the user storage - const internalAccountsToBeSavedToUserStorage: InternalAccount[] = []; - - // Compare internal accounts list with user storage accounts list - // First step: compare lengths - const internalAccountsList = - await this.#accounts.getInternalAccountsList(); - - if (!internalAccountsList || !internalAccountsList.length) { - throw new Error(`Failed to get internal accounts list`); - } - - const hasMoreUserStorageAccountsThanInternalAccounts = - userStorageAccountsList.length > internalAccountsList.length; - - // We don't want to remove existing accounts for a user - // so we only add new accounts if the user has more accounts in user storage than internal accounts - if (hasMoreUserStorageAccountsThanInternalAccounts) { - const numberOfAccountsToAdd = - Math.min( - userStorageAccountsList.length, - this.#accounts.maxNumberOfAccountsToAdd, - ) - internalAccountsList.length; - - // Create new accounts to match the user storage accounts list - for (let i = 0; i < numberOfAccountsToAdd; i++) { - await this.messagingSystem.call('KeyringController:addNewAccount'); - this.#config?.accountSyncing?.onAccountAdded?.(profileId); - } - } - - // Second step: compare account names - // Get the internal accounts list again since new accounts might have been added in the previous step - const refreshedInternalAccountsList = - await this.#accounts.getInternalAccountsList(); - - const newlyAddedAccounts = refreshedInternalAccountsList.filter( - (account) => - !internalAccountsList.find((a) => a.address === account.address), - ); - - for (const internalAccount of refreshedInternalAccountsList) { - const userStorageAccount = userStorageAccountsList.find( - (account) => account.a === internalAccount.address, - ); - - // If the account is not present in user storage - if (!userStorageAccount) { - // If the account was just added in the previous step, skip saving it, it's likely to be a bogus account - if (newlyAddedAccounts.includes(internalAccount)) { - this.#config?.accountSyncing?.onAccountSyncErroneousSituation?.( - profileId, - 'An account was added to the internal accounts list but was not present in the user storage accounts list', - ); - continue; - } - // Otherwise, it means that this internal account was present before the sync, and needs to be saved to the user storage - internalAccountsToBeSavedToUserStorage.push(internalAccount); - continue; - } - - // From this point on, we know that the account is present in - // both the internal accounts list and the user storage accounts list - - // One or both accounts have default names - const isInternalAccountNameDefault = isNameDefaultAccountName( - internalAccount.metadata.name, - ); - const isUserStorageAccountNameDefault = isNameDefaultAccountName( - userStorageAccount.n, - ); - - // Internal account has default name - if (isInternalAccountNameDefault) { - if (!isUserStorageAccountNameDefault) { - this.messagingSystem.call( - 'AccountsController:updateAccountMetadata', - internalAccount.id, - { - name: userStorageAccount.n, - }, - ); - - this.#config?.accountSyncing?.onAccountNameUpdated?.(profileId); - } - continue; - } - - // Internal account has custom name but user storage account has default name - if (isUserStorageAccountNameDefault) { - internalAccountsToBeSavedToUserStorage.push(internalAccount); - continue; - } - - // Both accounts have custom names - - // User storage account has a nameLastUpdatedAt timestamp - // Note: not storing the undefined checks in constants to act as a type guard - if (userStorageAccount.nlu !== undefined) { - if (internalAccount.metadata.nameLastUpdatedAt !== undefined) { - const isInternalAccountNameNewer = - internalAccount.metadata.nameLastUpdatedAt > - userStorageAccount.nlu; - - if (isInternalAccountNameNewer) { - internalAccountsToBeSavedToUserStorage.push(internalAccount); - continue; - } - } - - this.messagingSystem.call( - 'AccountsController:updateAccountMetadata', - internalAccount.id, - { - name: userStorageAccount.n, - nameLastUpdatedAt: userStorageAccount.nlu, - }, - ); - - const areInternalAndUserStorageAccountNamesEqual = - internalAccount.metadata.name === userStorageAccount.n; - - if (!areInternalAndUserStorageAccountNamesEqual) { - this.#config?.accountSyncing?.onAccountNameUpdated?.(profileId); - } - - continue; - } else if (internalAccount.metadata.nameLastUpdatedAt !== undefined) { - internalAccountsToBeSavedToUserStorage.push(internalAccount); - continue; - } - } - - // Save the internal accounts list to the user storage - if (internalAccountsToBeSavedToUserStorage.length) { - await this.performBatchSetStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - internalAccountsToBeSavedToUserStorage.map((account) => [ - account.address, - JSON.stringify(mapInternalAccountToUserStorageAccount(account)), - ]), - ); - } - - // In case we have corrupted user storage with accounts that don't exist in the internal accounts list - // Delete those accounts from the user storage - const userStorageAccountsToBeDeleted = userStorageAccountsList.filter( - (account) => - !refreshedInternalAccountsList.find((a) => a.address === account.a), - ); - - if (userStorageAccountsToBeDeleted.length) { - await this.performBatchDeleteStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - userStorageAccountsToBeDeleted.map((account) => account.a), - ); - this.#config?.accountSyncing?.onAccountSyncErroneousSituation?.( - profileId, - 'An account was present in the user storage accounts list but was not found in the internal accounts list after the sync', - ); - } + const profileId = await this.#auth.getProfileId(); - // We do this here and not in the finally statement because we want to make sure that - // the accounts are saved / updated / deleted at least once before we set this flag - await this.setHasAccountSyncingSyncedAtLeastOnce(true); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); - throw new Error( - `${controllerName} - failed to sync user storage accounts list - ${errorMessage}`, - ); - } finally { - this.#accounts.isAccountSyncingInProgress = false; - } + await syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: this.#env.isAccountSyncingEnabled, + maxNumberOfAccountsToAdd: + this.#config?.accountSyncing?.maxNumberOfAccountsToAdd, + onAccountAdded: () => + this.#config?.accountSyncing?.onAccountAdded?.(profileId), + onAccountNameUpdated: () => + this.#config?.accountSyncing?.onAccountNameUpdated?.(profileId), + onAccountSyncErroneousSituation: (situationMessage) => + this.#config?.accountSyncing?.onAccountSyncErroneousSituation?.( + profileId, + situationMessage, + ), + }, + { + getMessenger: () => this.messagingSystem, + getUserStorageControllerInstance: () => this, + }, + ); } /** @@ -1147,22 +884,14 @@ export default class UserStorageController extends BaseController< async saveInternalAccountToUserStorage( internalAccount: InternalAccount, ): Promise { - if ( - !this.#accounts.canSync() || - !isEvmAccountType(internalAccount.type) || - !this.#accounts.doesInternalAccountHaveCorrectKeyringType(internalAccount) - ) { - return; - } - - try { - await this.#accounts.saveInternalAccountToUserStorage(internalAccount); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); - throw new Error( - `${controllerName} - failed to save account to user storage - ${errorMessage}`, - ); - } + await saveInternalAccountToUserStorage( + internalAccount, + { isAccountSyncingEnabled: this.#env.isAccountSyncingEnabled }, + { + getMessenger: () => this.messagingSystem, + getUserStorageControllerInstance: () => this, + }, + ); } async syncNetworks() { diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts index 6c0983fd23..6bb1b38689 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts @@ -1,3 +1,65 @@ +import type nock from 'nock'; + +import encryption from '../../../shared/encryption/encryption'; +import type { + GetUserStorageAllFeatureEntriesResponse, + GetUserStorageResponse, +} from '../services'; +import { MOCK_STORAGE_KEY } from './mockStorage'; + +/** + * Test Utility - creates a realistic mock user-storage entry + * @param data - data to encrypt + * @returns user storage entry + */ +export async function createMockUserStorageEntry( + data: unknown, +): Promise { + return { + HashedKey: 'HASHED_KEY', + Data: await encryption.encryptString( + JSON.stringify(data), + MOCK_STORAGE_KEY, + ), + }; +} + +/** + * Test Utility - creates a realistic mock user-storage get-all entry + * @param data - data array to encrypt + * @returns user storage entry + */ +export async function createMockUserStorageEntries( + data: unknown[], +): Promise { + return await Promise.all(data.map((d) => createMockUserStorageEntry(d))); +} + +/** + * Test Utility - decrypts a realistic batch upsert payload + * @param requestBody - nock body + * @param storageKey - storage key + * @returns decrypted body + */ +export async function decryptBatchUpsertBody( + requestBody: nock.Body, + storageKey: string, +) { + if (typeof requestBody === 'string') { + return requestBody; + } + return await Promise.all( + Object.entries(requestBody.data).map( + async ([entryKey, entryValue]) => { + return [ + entryKey, + await encryption.decryptString(entryValue, storageKey), + ]; + }, + ), + ); +} + type WaitForOptions = { intervalMs?: number; timeoutMs?: number; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/mockAccounts.ts similarity index 96% rename from packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts rename to packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/mockAccounts.ts index 8664fcd6cc..faf0e027fa 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/mockAccounts.ts @@ -2,8 +2,8 @@ import { EthAccountType } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; -import { LOCALIZED_DEFAULT_ACCOUNT_NAMES } from '../accounts/constants'; -import { mapInternalAccountToUserStorageAccount } from '../accounts/user-storage'; +import { LOCALIZED_DEFAULT_ACCOUNT_NAMES } from '../constants'; +import { mapInternalAccountToUserStorageAccount } from '../utils'; /** * Map an array of internal accounts to an array of user storage accounts diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/test-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/test-utils.ts new file mode 100644 index 0000000000..fc03dd2343 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/__fixtures__/test-utils.ts @@ -0,0 +1,68 @@ +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { createSHA256Hash } from '../../../../shared/encryption'; +import { mockUserStorageMessenger } from '../../__fixtures__/mockMessenger'; +import { mapInternalAccountToUserStorageAccount } from '../utils'; +import { MOCK_INTERNAL_ACCOUNTS } from './mockAccounts'; + +/** + * Test Utility - create a mock user storage messenger for account syncing tests + * + * @param options - options for the mock messenger + * @param options.accounts - options for the accounts part of the controller + * @param options.accounts.accountsList - list of accounts to return for the 'AccountsController:listAccounts' action + * @returns Mock User Storage Messenger + */ +export function mockUserStorageMessengerForAccountSyncing(options?: { + accounts?: { + accountsList?: InternalAccount[]; + }; +}) { + const messengerMocks = mockUserStorageMessenger(); + + messengerMocks.mockKeyringAddNewAccount.mockImplementation(async () => { + messengerMocks.baseMessenger.publish( + 'AccountsController:accountAdded', + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + ); + return MOCK_INTERNAL_ACCOUNTS.ONE[0].address; + }); + + messengerMocks.mockAccountsListAccounts.mockReturnValue( + (options?.accounts?.accountsList ?? + MOCK_INTERNAL_ACCOUNTS.ALL) as InternalAccount[], + ); + + return messengerMocks; +} + +/** + * Test Utility - creates a realistic expected batch upsert payload + * @param data - data supposed to be upserted + * @param storageKey - storage key + * @returns expected body + */ +export function createExpectedAccountSyncBatchUpsertBody( + data: [string, InternalAccount][], + storageKey: string, +) { + return data.map(([entryKey, entryValue]) => [ + createSHA256Hash(String(entryKey) + storageKey), + JSON.stringify(mapInternalAccountToUserStorageAccount(entryValue)), + ]); +} + +/** + * Test Utility - creates a realistic expected batch delete payload + * @param data - data supposed to be deleted + * @param storageKey - storage key + * @returns expected body + */ +export function createExpectedAccountSyncBatchDeleteBody( + data: string[], + storageKey: string, +) { + return data.map((entryKey) => + createSHA256Hash(String(entryKey) + storageKey), + ); +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/accounts/constants.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/constants.ts similarity index 100% rename from packages/profile-sync-controller/src/controllers/user-storage/accounts/constants.ts rename to packages/profile-sync-controller/src/controllers/user-storage/account-syncing/constants.ts diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts new file mode 100644 index 0000000000..855fa81b24 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts @@ -0,0 +1,1378 @@ +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import UserStorageController, { USER_STORAGE_FEATURE_NAMES } from '..'; +import { MOCK_STORAGE_KEY } from '../__fixtures__'; +import { + mockEndpointBatchDeleteUserStorage, + mockEndpointBatchUpsertUserStorage, + mockEndpointGetUserStorage, + mockEndpointGetUserStorageAllFeatureEntries, + mockEndpointUpsertUserStorage, +} from '../__fixtures__/mockServices'; +import { + createMockUserStorageEntries, + decryptBatchUpsertBody, +} from '../__fixtures__/test-utils'; +import { + MOCK_INTERNAL_ACCOUNTS, + MOCK_USER_STORAGE_ACCOUNTS, +} from './__fixtures__/mockAccounts'; +import { + createExpectedAccountSyncBatchDeleteBody, + createExpectedAccountSyncBatchUpsertBody, + mockUserStorageMessengerForAccountSyncing, +} from './__fixtures__/test-utils'; +import * as AccountSyncingControllerIntegrationModule from './controller-integration'; +import * as AccountsUserStorageModule from './utils'; + +describe('user-storage/account-syncing/controller-integration - syncInternalAccountsWithUserStorage() tests', () => { + it('returns void if UserStorage is not enabled', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + mockAPI: mockEndpointGetUserStorage(), + }; + }; + + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + env: { + isAccountSyncingEnabled: true, + }, + state: { + isProfileSyncingEnabled: false, + isProfileSyncingUpdateLoading: false, + hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, + }, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(messengerMocks.mockAccountsListAccounts).not.toHaveBeenCalled(); + }); + + it('returns void if account syncing feature flag is disabled', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + env: { + isAccountSyncingEnabled: false, + }, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: false, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(false); + }); + + it('throws if AccountsController:listAccounts fails or returns an empty list', async () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }; + }; + + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: [], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await expect( + AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ), + ).rejects.toThrow(expect.any(Error)); + + mockAPI.mockEndpointGetUserStorage.done(); + }); + + it('uploads accounts list to user storage if user storage is empty', async () => { + const mockUserStorageAccountsResponse = { + status: 404, + body: [], + }; + + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( + 0, + 2, + ) as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + mockUserStorageAccountsResponse, + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + const decryptedBody = await decryptBatchUpsertBody( + requestBody, + MOCK_STORAGE_KEY, + ); + + const expectedBody = createExpectedAccountSyncBatchUpsertBody( + MOCK_INTERNAL_ACCOUNTS.ALL.slice(0, 2).map((account) => [ + account.address, + account as InternalAccount, + ]), + MOCK_STORAGE_KEY, + ); + + expect(decryptedBody).toStrictEqual(expectedBody); + }, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + mockAPI.mockEndpointGetUserStorage.done(); + + expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); + expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(true); + }); + + it('creates internal accounts if user storage has more accounts. it also updates hasAccountSyncingSyncedAtLeastOnce accordingly', async () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }; + }; + + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchDeleteUserStorage: + mockEndpointBatchDeleteUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + if (typeof requestBody === 'string') { + return; + } + + const expectedBody = createExpectedAccountSyncBatchDeleteBody( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( + (account) => + !MOCK_INTERNAL_ACCOUNTS.ONE.find( + (internalAccount) => + internalAccount.address === account.a, + ), + ).map((account) => account.a), + MOCK_STORAGE_KEY, + ); + + expect(requestBody.batch_delete).toStrictEqual(expectedBody); + }, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); + + expect(messengerMocks.mockKeyringAddNewAccount).toHaveBeenCalledTimes( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.length - + MOCK_INTERNAL_ACCOUNTS.ONE.length, + ); + + expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); + + expect(controller.state.hasAccountSyncingSyncedAtLeastOnce).toBe(true); + }); + + describe('handles corrupted user storage gracefully', () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.TWO_DEFAULT_NAMES_WITH_ONE_BOGUS, + ), + }; + }; + + const arrangeMocksForBogusAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchDeleteUserStorage: + mockEndpointBatchDeleteUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + if (typeof requestBody === 'string') { + return; + } + + const expectedBody = createExpectedAccountSyncBatchDeleteBody( + [ + MOCK_USER_STORAGE_ACCOUNTS + .TWO_DEFAULT_NAMES_WITH_ONE_BOGUS[1].a, + ], + MOCK_STORAGE_KEY, + ); + + expect(requestBody.batch_delete).toStrictEqual(expectedBody); + }, + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + it('does not save the bogus account to user storage, and deletes it from user storage', async () => { + const { messengerMocks, mockAPI } = await arrangeMocksForBogusAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); + expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(false); + expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); + }); + + it('fires the onAccountSyncErroneousSituation callback in erroneous situations', async () => { + const onAccountSyncErroneousSituation = jest.fn(); + + const { messengerMocks } = await arrangeMocksForBogusAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + onAccountSyncErroneousSituation, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(onAccountSyncErroneousSituation).toHaveBeenCalledTimes(1); + }); + }); + + it('fires the onAccountAdded callback when adding an account', async () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }; + }; + + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchDeleteUserStorage: + mockEndpointBatchDeleteUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + if (typeof requestBody === 'string') { + return; + } + + const expectedBody = createExpectedAccountSyncBatchDeleteBody( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( + (account) => + !MOCK_INTERNAL_ACCOUNTS.ONE.find( + (internalAccount) => + internalAccount.address === account.a, + ), + ).map((account) => account.a), + MOCK_STORAGE_KEY, + ); + + expect(requestBody.batch_delete).toStrictEqual(expectedBody); + }, + ), + }, + }; + }; + + const onAccountAdded = jest.fn(); + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + onAccountAdded, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect(onAccountAdded).toHaveBeenCalledTimes( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.length - + MOCK_INTERNAL_ACCOUNTS.ONE.length, + ); + + expect(mockAPI.mockEndpointBatchDeleteUserStorage.isDone()).toBe(true); + }); + + it('does not create internal accounts if user storage has less accounts', async () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.slice(0, 1), + ), + }; + }; + + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( + 0, + 2, + ) as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + mockAPI.mockEndpointBatchUpsertUserStorage.done(); + + expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); + expect(mockAPI.mockEndpointBatchUpsertUserStorage.isDone()).toBe(true); + + expect(messengerMocks.mockKeyringAddNewAccount).not.toHaveBeenCalled(); + }); + + describe('User storage name is a default name', () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, + ), + }; + }; + + it('does not update the internal account name if both user storage and internal accounts have default names', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + + it('does not update the internal account name if the internal account name is custom without last updated', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + mockAPI.mockEndpointBatchUpsertUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + + it('does not update the internal account name if the internal account name is custom with last updated', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + mockAPI.mockEndpointBatchUpsertUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + }); + + describe('User storage name is a custom name without last updated', () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, + ), + }; + }; + + it('updates the internal account name if the internal account name is a default name', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).toHaveBeenCalledWith( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED[0].i, + { + name: MOCK_USER_STORAGE_ACCOUNTS + .ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED[0].n, + }, + ); + }); + + it('does not update internal account name if both user storage and internal accounts have custom names without last updated', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + + it('does not update the internal account name if the internal account name is custom with last updated', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + mockAPI.mockEndpointBatchUpsertUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + + it('fires the onAccountNameUpdated callback when renaming an internal account', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const onAccountNameUpdated = jest.fn(); + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + onAccountNameUpdated, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect(onAccountNameUpdated).toHaveBeenCalledTimes(1); + }); + }); + + describe('User storage name is a custom name with last updated', () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, + ), + }; + }; + + it('updates the internal account name if the internal account name is a default name', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).toHaveBeenCalledWith( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, + { + name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] + .n, + }, + ); + }); + + it('updates the internal account name and last updated if the internal account name is a custom name without last updated', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).toHaveBeenCalledWith( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, + { + name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] + .n, + nameLastUpdatedAt: + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].nlu, + }, + ); + }); + + it('updates the internal account name and last updated if the user storage account is more recent', async () => { + const arrangeMocksForAccounts = async () => { + const mockGetEntriesResponse = await mockUserStorageAccountsResponse(); + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + mockGetEntriesResponse, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).toHaveBeenCalledWith( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].i, + { + name: MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0] + .n, + nameLastUpdatedAt: + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED[0].nlu, + }, + ); + }); + + it('does not update the internal account if the user storage account is less recent', async () => { + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }, + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + mockAPI.mockEndpointGetUserStorage.done(); + mockAPI.mockEndpointBatchUpsertUserStorage.done(); + + expect( + messengerMocks.mockAccountsUpdateAccountMetadata, + ).not.toHaveBeenCalled(); + }); + }); +}); + +describe('user-storage/account-syncing/controller-integration - saveInternalAccountToUserStorage() tests', () => { + it('returns void if UserStorage is not enabled', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + }; + }; + + const mapInternalAccountToUserStorageAccountMock = jest.spyOn( + AccountsUserStorageModule, + 'mapInternalAccountToUserStorageAccount', + ); + + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + state: { + isProfileSyncingEnabled: false, + isProfileSyncingUpdateLoading: false, + hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, + }, + }); + + await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mapInternalAccountToUserStorageAccountMock).not.toHaveBeenCalled(); + }); + + it('returns void if account syncing feature flag is disabled', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + mockAPI: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + ), + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: false, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + { + isAccountSyncingEnabled: false, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mockAPI.isDone()).toBe(false); + }); + + it('saves an internal account to user storage', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + mockAPI: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + ), + }; + }; + + const { messengerMocks, mockAPI } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mockAPI.isDone()).toBe(true); + }); + + it('rejects if api call fails', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + mockAPI: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + { status: 500 }, + ), + }; + }; + + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + await expect( + AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ), + ).rejects.toThrow(expect.any(Error)); + }); + + describe('it reacts to other controller events', () => { + const mockUserStorageAccountsResponse = async () => { + return { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }; + }; + + const arrangeMocksForAccounts = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing({ + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], + }, + }), + mockAPI: { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + await mockUserStorageAccountsResponse(), + ), + mockEndpointBatchUpsertUserStorage: + mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + mockEndpointUpsertUserStorage: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + ), + }, + }; + }; + + it('saves an internal account to user storage when the AccountsController:accountRenamed event is fired', async () => { + const { messengerMocks } = await arrangeMocksForAccounts(); + const { baseMessenger, messenger } = messengerMocks; + + const controller = new UserStorageController({ + messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + // We need to sync at least once before we listen for other controller events + await controller.setHasAccountSyncingSyncedAtLeastOnce(true); + + const mockSaveInternalAccountToUserStorage = jest + .spyOn( + AccountSyncingControllerIntegrationModule, + 'saveInternalAccountToUserStorage', + ) + .mockImplementation(); + + baseMessenger.publish( + 'AccountsController:accountRenamed', + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + ); + + expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( + MOCK_INTERNAL_ACCOUNTS.ONE[0], + expect.anything(), + expect.anything(), + ); + }); + + it('saves an internal account to user storage when the AccountsController:accountAdded event is fired', async () => { + const { messengerMocks } = await arrangeMocksForAccounts(); + const { baseMessenger, messenger } = messengerMocks; + + const controller = new UserStorageController({ + messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + // We need to sync at least once before we listen for other controller events + await controller.setHasAccountSyncingSyncedAtLeastOnce(true); + + const mockSaveInternalAccountToUserStorage = jest + .spyOn( + AccountSyncingControllerIntegrationModule, + 'saveInternalAccountToUserStorage', + ) + .mockImplementation(); + + baseMessenger.publish( + 'AccountsController:accountAdded', + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + ); + + expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( + MOCK_INTERNAL_ACCOUNTS.ONE[0], + expect.anything(), + expect.anything(), + ); + }); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts new file mode 100644 index 0000000000..4b63c1ba58 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -0,0 +1,363 @@ +import { isEvmAccountType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { USER_STORAGE_FEATURE_NAMES } from '../../../shared/storage-schema'; +import { + canPerformAccountSyncing, + getInternalAccountsList, + getUserStorageAccountsList, +} from './sync-utils'; +import type { AccountSyncingConfig, AccountSyncingOptions } from './types'; +import { + doesInternalAccountHaveCorrectKeyringType, + isNameDefaultAccountName, + mapInternalAccountToUserStorageAccount, +} from './utils'; + +/** + * Initialize and setup events to listen to for account syncing + * @param config - configuration parameters + * @param options - parameters used for initializing and enabling account syncing + */ +export function setupAccountSyncingSubscriptions( + config: AccountSyncingConfig, + options: AccountSyncingOptions, +) { + const { getMessenger, getUserStorageControllerInstance } = options; + const messenger = getMessenger(); + + messenger.subscribe( + 'AccountsController:accountAdded', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (account) => { + if ( + !canPerformAccountSyncing(config, options) || + !getUserStorageControllerInstance().state + .hasAccountSyncingSyncedAtLeastOnce + ) { + return; + } + + await saveInternalAccountToUserStorage(account, config, options); + }, + ); + + messenger.subscribe( + 'AccountsController:accountRenamed', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (account) => { + if ( + !canPerformAccountSyncing(config, options) || + !getUserStorageControllerInstance().state + .hasAccountSyncingSyncedAtLeastOnce + ) { + return; + } + + await saveInternalAccountToUserStorage(account, config, options); + }, + ); +} + +/** + * Saves an individual internal account to the user storage. + * @param internalAccount - The internal account to save + * @param config - parameters used for saving the internal account + * @param options - parameters used for saving the internal account + */ +export async function saveInternalAccountToUserStorage( + internalAccount: InternalAccount, + config: AccountSyncingConfig, + options: AccountSyncingOptions, +): Promise { + console.log('saveInternalAccountToUserStorage'); + + const { isAccountSyncingEnabled } = config; + const { getUserStorageControllerInstance } = options; + const userStorageControllerInstance = getUserStorageControllerInstance(); + + if ( + !isAccountSyncingEnabled || + !canPerformAccountSyncing(config, options) || + !isEvmAccountType(internalAccount.type) || + !doesInternalAccountHaveCorrectKeyringType(internalAccount) + ) { + return; + } + + try { + // Map the internal account to the user storage account schema + const mappedAccount = + mapInternalAccountToUserStorageAccount(internalAccount); + + await userStorageControllerInstance.performSetStorage( + // ESLint is confused here. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${USER_STORAGE_FEATURE_NAMES.accounts}.${internalAccount.address}`, + JSON.stringify(mappedAccount), + ); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); + throw new Error( + `${userStorageControllerInstance.name} - failed to save account to user storage - ${errorMessage}`, + ); + } +} + +/** + * Saves the list of internal accounts to the user storage. + * @param config - parameters used for saving the list of internal accounts + * @param options - parameters used for saving the list of internal accounts + */ +export async function saveInternalAccountsListToUserStorage( + config: AccountSyncingConfig, + options: AccountSyncingOptions, +): Promise { + const { isAccountSyncingEnabled } = config; + const { getUserStorageControllerInstance } = options; + const userStorageControllerInstance = getUserStorageControllerInstance(); + + if (!isAccountSyncingEnabled) { + return; + } + + const internalAccountsList = await getInternalAccountsList(options); + + if (!internalAccountsList) { + return; + } + + const internalAccountsListFormattedForUserStorage = internalAccountsList.map( + mapInternalAccountToUserStorageAccount, + ); + + await userStorageControllerInstance.performBatchSetStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + internalAccountsListFormattedForUserStorage.map((account) => [ + account.a, + JSON.stringify(account), + ]), + ); +} + +type SyncInternalAccountsWithUserStorageConfig = AccountSyncingConfig & { + maxNumberOfAccountsToAdd?: number; + onAccountAdded?: () => void; + onAccountNameUpdated?: () => void; + onAccountSyncErroneousSituation?: (errorMessage: string) => void; +}; + +/** + * Syncs the internal accounts list with the user storage accounts list. + * This method is used to make sure that the internal accounts list is up-to-date with the user storage accounts list and vice-versa. + * It will add new accounts to the internal accounts list, update/merge conflicting names and re-upload the results in some cases to the user storage. + * @param config - parameters used for syncing the internal accounts list with the user storage accounts list + * @param options - parameters used for syncing the internal accounts list with the user storage accounts list + */ +export async function syncInternalAccountsWithUserStorage( + config: SyncInternalAccountsWithUserStorageConfig, + options: AccountSyncingOptions, +): Promise { + const { isAccountSyncingEnabled } = config; + + if (!canPerformAccountSyncing(config, options) || !isAccountSyncingEnabled) { + return; + } + + const { + maxNumberOfAccountsToAdd = 100, + onAccountAdded, + onAccountNameUpdated, + onAccountSyncErroneousSituation, + } = config; + const { getMessenger, getUserStorageControllerInstance } = options; + const messenger = getMessenger(); + const userStorageControllerInstance = getUserStorageControllerInstance(); + + try { + await userStorageControllerInstance.setIsAccountSyncingInProgress(true); + + const userStorageAccountsList = await getUserStorageAccountsList(options); + + if (!userStorageAccountsList || !userStorageAccountsList.length) { + await saveInternalAccountsListToUserStorage( + { isAccountSyncingEnabled }, + options, + ); + await userStorageControllerInstance.setHasAccountSyncingSyncedAtLeastOnce( + true, + ); + return; + } + + // Prepare an array of internal accounts to be saved to the user storage + const internalAccountsToBeSavedToUserStorage: InternalAccount[] = []; + + // Compare internal accounts list with user storage accounts list + // First step: compare lengths + const internalAccountsList = await getInternalAccountsList(options); + + if (!internalAccountsList || !internalAccountsList.length) { + throw new Error(`Failed to get internal accounts list`); + } + + const hasMoreUserStorageAccountsThanInternalAccounts = + userStorageAccountsList.length > internalAccountsList.length; + + // We don't want to remove existing accounts for a user + // so we only add new accounts if the user has more accounts in user storage than internal accounts + if (hasMoreUserStorageAccountsThanInternalAccounts) { + const numberOfAccountsToAdd = + Math.min(userStorageAccountsList.length, maxNumberOfAccountsToAdd) - + internalAccountsList.length; + + // Create new accounts to match the user storage accounts list + for (let i = 0; i < numberOfAccountsToAdd; i++) { + await messenger.call('KeyringController:addNewAccount'); + onAccountAdded?.(); + } + } + + // Second step: compare account names + // Get the internal accounts list again since new accounts might have been added in the previous step + const refreshedInternalAccountsList = await getInternalAccountsList( + options, + ); + + const newlyAddedAccounts = refreshedInternalAccountsList.filter( + (account) => + !internalAccountsList.find((a) => a.address === account.address), + ); + + for (const internalAccount of refreshedInternalAccountsList) { + const userStorageAccount = userStorageAccountsList.find( + (account) => account.a === internalAccount.address, + ); + + // If the account is not present in user storage + if (!userStorageAccount) { + // If the account was just added in the previous step, skip saving it, it's likely to be a bogus account + if (newlyAddedAccounts.includes(internalAccount)) { + onAccountSyncErroneousSituation?.( + 'An account was added to the internal accounts list but was not present in the user storage accounts list', + ); + continue; + } + // Otherwise, it means that this internal account was present before the sync, and needs to be saved to the user storage + internalAccountsToBeSavedToUserStorage.push(internalAccount); + continue; + } + + // From this point on, we know that the account is present in + // both the internal accounts list and the user storage accounts list + + // One or both accounts have default names + const isInternalAccountNameDefault = isNameDefaultAccountName( + internalAccount.metadata.name, + ); + const isUserStorageAccountNameDefault = isNameDefaultAccountName( + userStorageAccount.n, + ); + + // Internal account has default name + if (isInternalAccountNameDefault) { + if (!isUserStorageAccountNameDefault) { + messenger.call( + 'AccountsController:updateAccountMetadata', + internalAccount.id, + { + name: userStorageAccount.n, + }, + ); + + onAccountNameUpdated?.(); + } + continue; + } + + // Internal account has custom name but user storage account has default name + if (isUserStorageAccountNameDefault) { + internalAccountsToBeSavedToUserStorage.push(internalAccount); + continue; + } + + // Both accounts have custom names + + // User storage account has a nameLastUpdatedAt timestamp + // Note: not storing the undefined checks in constants to act as a type guard + if (userStorageAccount.nlu !== undefined) { + if (internalAccount.metadata.nameLastUpdatedAt !== undefined) { + const isInternalAccountNameNewer = + internalAccount.metadata.nameLastUpdatedAt > userStorageAccount.nlu; + + if (isInternalAccountNameNewer) { + internalAccountsToBeSavedToUserStorage.push(internalAccount); + continue; + } + } + + messenger.call( + 'AccountsController:updateAccountMetadata', + internalAccount.id, + { + name: userStorageAccount.n, + nameLastUpdatedAt: userStorageAccount.nlu, + }, + ); + + const areInternalAndUserStorageAccountNamesEqual = + internalAccount.metadata.name === userStorageAccount.n; + + if (!areInternalAndUserStorageAccountNamesEqual) { + onAccountNameUpdated?.(); + } + + continue; + } else if (internalAccount.metadata.nameLastUpdatedAt !== undefined) { + internalAccountsToBeSavedToUserStorage.push(internalAccount); + continue; + } + } + + // Save the internal accounts list to the user storage + if (internalAccountsToBeSavedToUserStorage.length) { + await userStorageControllerInstance.performBatchSetStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + internalAccountsToBeSavedToUserStorage.map((account) => [ + account.address, + JSON.stringify(mapInternalAccountToUserStorageAccount(account)), + ]), + ); + } + + // In case we have corrupted user storage with accounts that don't exist in the internal accounts list + // Delete those accounts from the user storage + const userStorageAccountsToBeDeleted = userStorageAccountsList.filter( + (account) => + !refreshedInternalAccountsList.find((a) => a.address === account.a), + ); + + if (userStorageAccountsToBeDeleted.length) { + await userStorageControllerInstance.performBatchDeleteStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + userStorageAccountsToBeDeleted.map((account) => account.a), + ); + onAccountSyncErroneousSituation?.( + 'An account was present in the user storage accounts list but was not found in the internal accounts list after the sync', + ); + } + + // We do this here and not in the finally statement because we want to make sure that + // the accounts are saved / updated / deleted at least once before we set this flag + await userStorageControllerInstance.setHasAccountSyncingSyncedAtLeastOnce( + true, + ); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); + throw new Error( + `${userStorageControllerInstance.name} - failed to sync user storage accounts list - ${errorMessage}`, + ); + } finally { + await userStorageControllerInstance.setIsAccountSyncingInProgress(false); + } +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts new file mode 100644 index 0000000000..c50ca425b9 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts @@ -0,0 +1,29 @@ +import { setupAccountSyncingSubscriptions } from './setup-subscriptions'; + +describe('setupAccountSyncing', () => { + it('should subscribe to accountAdded and accountRenamed events', () => { + const config = { isAccountSyncingEnabled: true }; + const options = { + getMessenger: jest.fn().mockReturnValue({ + subscribe: jest.fn(), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + hasAccountSyncingSyncedAtLeastOnce: true, + }, + }), + }; + + setupAccountSyncingSubscriptions(config, options); + + expect(options.getMessenger().subscribe).toHaveBeenCalledWith( + 'AccountsController:accountAdded', + expect.any(Function), + ); + + expect(options.getMessenger().subscribe).toHaveBeenCalledWith( + 'AccountsController:accountRenamed', + expect.any(Function), + ); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts new file mode 100644 index 0000000000..33d506d396 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts @@ -0,0 +1,48 @@ +import { saveInternalAccountToUserStorage } from './controller-integration'; +import { canPerformAccountSyncing } from './sync-utils'; +import type { AccountSyncingConfig, AccountSyncingOptions } from './types'; + +/** + * Initialize and setup events to listen to for account syncing + * @param config - configuration parameters + * @param options - parameters used for initializing and enabling account syncing + */ +export function setupAccountSyncingSubscriptions( + config: AccountSyncingConfig, + options: AccountSyncingOptions, +) { + const { getMessenger, getUserStorageControllerInstance } = options; + const messenger = getMessenger(); + + messenger.subscribe( + 'AccountsController:accountAdded', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (account) => { + if ( + !canPerformAccountSyncing(config, options) || + !getUserStorageControllerInstance().state + .hasAccountSyncingSyncedAtLeastOnce + ) { + return; + } + + await saveInternalAccountToUserStorage(account, config, options); + }, + ); + + messenger.subscribe( + 'AccountsController:accountRenamed', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (account) => { + if ( + !canPerformAccountSyncing(config, options) || + !getUserStorageControllerInstance().state + .hasAccountSyncingSyncedAtLeastOnce + ) { + return; + } + + await saveInternalAccountToUserStorage(account, config, options); + }, + ); +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts new file mode 100644 index 0000000000..90a5dccc2f --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts @@ -0,0 +1,153 @@ +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { + canPerformAccountSyncing, + getInternalAccountsList, + getUserStorageAccountsList, +} from './sync-utils'; +import type { AccountSyncingConfig, AccountSyncingOptions } from './types'; +import * as utils from './utils'; + +describe('user-storage/account-syncing/sync-utils', () => { + describe('canPerformAccountSyncing', () => { + it('returns false if profile syncing is not enabled', () => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockReturnValue(true), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + isProfileSyncingEnabled: false, + isAccountSyncingInProgress: false, + }, + }), + }; + + expect(canPerformAccountSyncing(config, options)).toBe(false); + }); + + it('returns false if authentication is not enabled', () => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockReturnValue(false), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + isProfileSyncingEnabled: true, + isAccountSyncingInProgress: false, + }, + }), + }; + + expect(canPerformAccountSyncing(config, options)).toBe(false); + }); + + it('returns false if account syncing is not enabled', () => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled: false }; + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockReturnValue(true), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + isProfileSyncingEnabled: true, + isAccountSyncingInProgress: false, + }, + }), + }; + + expect(canPerformAccountSyncing(config, options)).toBe(false); + }); + + it('returns false if account syncing is in progress', () => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockReturnValue(true), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + isProfileSyncingEnabled: true, + isAccountSyncingInProgress: true, + }, + }), + }; + + expect(canPerformAccountSyncing(config, options)).toBe(false); + }); + + it('returns true if all conditions are met', () => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockReturnValue(true), + }), + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + state: { + isProfileSyncingEnabled: true, + isAccountSyncingInProgress: false, + }, + }), + }; + + expect(canPerformAccountSyncing(config, options)).toBe(true); + }); + }); + + describe('getInternalAccountsList', () => { + it('returns filtered internal accounts list', async () => { + const internalAccounts = [ + { id: '1', metadata: { keyring: { type: KeyringTypes.hd } } }, + { id: '2', metadata: { keyring: { type: KeyringTypes.trezor } } }, + ] as InternalAccount[]; + + const options: AccountSyncingOptions = { + getMessenger: jest.fn().mockReturnValue({ + call: jest.fn().mockResolvedValue(internalAccounts), + }), + getUserStorageControllerInstance: jest.fn(), + }; + + jest + .spyOn(utils, 'doesInternalAccountHaveCorrectKeyringType') + .mockImplementation( + (account) => account.metadata.keyring.type === KeyringTypes.hd, + ); + + const result = await getInternalAccountsList(options); + expect(result).toStrictEqual([internalAccounts[0]]); + }); + }); + + describe('getUserStorageAccountsList', () => { + it('returns parsed user storage accounts list', async () => { + const rawAccounts = ['{"id":"1"}', '{"id":"2"}']; + const options: AccountSyncingOptions = { + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + performGetStorageAllFeatureEntries: jest + .fn() + .mockResolvedValue(rawAccounts), + }), + getMessenger: jest.fn(), + }; + + const result = await getUserStorageAccountsList(options); + expect(result).toStrictEqual([{ id: '1' }, { id: '2' }]); + }); + + it('returns null if no raw accounts are found', async () => { + const options: AccountSyncingOptions = { + getUserStorageControllerInstance: jest.fn().mockReturnValue({ + performGetStorageAllFeatureEntries: jest.fn().mockResolvedValue(null), + }), + getMessenger: jest.fn(), + }; + + const result = await getUserStorageAccountsList(options); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts new file mode 100644 index 0000000000..23c6e8c318 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts @@ -0,0 +1,87 @@ +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { USER_STORAGE_FEATURE_NAMES } from '../../../shared/storage-schema'; +import type { + AccountSyncingConfig, + AccountSyncingOptions, + UserStorageAccount, +} from './types'; +import { doesInternalAccountHaveCorrectKeyringType } from './utils'; + +/** + * Checks if account syncing can be performed based on a set of conditions + * @param config - configuration parameters + * @param options - parameters used for checking if account syncing can be performed + * @returns Returns true if account syncing can be performed, false otherwise. + */ +export function canPerformAccountSyncing( + config: AccountSyncingConfig, + options: AccountSyncingOptions, +): boolean { + const { isAccountSyncingEnabled } = config; + const { getMessenger, getUserStorageControllerInstance } = options; + + const messenger = getMessenger(); + const userStorageControllerInstance = getUserStorageControllerInstance(); + + const { isProfileSyncingEnabled, isAccountSyncingInProgress } = + userStorageControllerInstance.state; + const isAuthEnabled = messenger.call('AuthenticationController:isSignedIn'); + + try { + if ( + !isProfileSyncingEnabled || + !isAuthEnabled || + !isAccountSyncingEnabled + ) { + return false; + } + if (isAccountSyncingInProgress) { + return false; + } + + return true; + } catch { + return false; + } +} + +/** + * Get the list of internal accounts + * @param options - parameters used for getting the list of internal accounts + */ +export async function getInternalAccountsList( + options: AccountSyncingOptions, +): Promise { + const { getMessenger } = options; + const messenger = getMessenger(); + + // eslint-disable-next-line @typescript-eslint/await-thenable + const internalAccountsList = await messenger.call( + 'AccountsController:listAccounts', + ); + + return internalAccountsList?.filter( + doesInternalAccountHaveCorrectKeyringType, + ); +} + +/** + * Get the list of user storage accounts + * @param options - parameters used for getting the list of user storage accounts + */ +export async function getUserStorageAccountsList( + options: AccountSyncingOptions, +): Promise { + const { getUserStorageControllerInstance } = options; + const userStorageControllerInstance = getUserStorageControllerInstance(); + + const rawAccountsListResponse = + await userStorageControllerInstance.performGetStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + ); + + return ( + rawAccountsListResponse?.map((rawAccount) => JSON.parse(rawAccount)) ?? null + ); +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/types.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/types.ts new file mode 100644 index 0000000000..745107f814 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/types.ts @@ -0,0 +1,31 @@ +import type { UserStorageControllerMessenger } from '../UserStorageController'; +import type UserStorageController from '../UserStorageController'; +import type { + USER_STORAGE_VERSION_KEY, + USER_STORAGE_VERSION, +} from './constants'; + +export type UserStorageAccount = { + /** + * The Version 'v' of the User Storage. + * NOTE - will allow us to support upgrade/downgrades in the future + */ + [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION; + /** the id 'i' of the account */ + i: string; + /** the address 'a' of the account */ + a: string; + /** the name 'n' of the account */ + n: string; + /** the nameLastUpdatedAt timestamp 'nlu' of the account */ + nlu?: number; +}; + +export type AccountSyncingConfig = { + isAccountSyncingEnabled: boolean; +}; + +export type AccountSyncingOptions = { + getUserStorageControllerInstance: () => UserStorageController; + getMessenger: () => UserStorageControllerMessenger; +}; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts new file mode 100644 index 0000000000..d9c6dba4f0 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts @@ -0,0 +1,111 @@ +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { getMockRandomDefaultAccountName } from './__fixtures__/mockAccounts'; +import { USER_STORAGE_VERSION, USER_STORAGE_VERSION_KEY } from './constants'; +import { + doesInternalAccountHaveCorrectKeyringType, + isNameDefaultAccountName, + mapInternalAccountToUserStorageAccount, +} from './utils'; + +describe('user-storage/account-syncing/utils', () => { + describe('isNameDefaultAccountName', () => { + it('should return true for default account names', () => { + expect( + isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 89`), + ).toBe(true); + expect( + isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 1`), + ).toBe(true); + expect( + isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 123543`), + ).toBe(true); + }); + + it('should return false for non-default account names', () => { + expect(isNameDefaultAccountName('My Account')).toBe(false); + expect(isNameDefaultAccountName('Mon compte 34')).toBe(false); + }); + }); + describe('mapInternalAccountToUserStorageAccount', () => { + it('should map an internal account to a user storage account with default account name', () => { + const internalAccount = { + address: '0x123', + id: '1', + metadata: { + name: `${getMockRandomDefaultAccountName()} 1`, + nameLastUpdatedAt: 1620000000000, + keyring: { + type: KeyringTypes.hd, + }, + }, + } as InternalAccount; + + const userStorageAccount = + mapInternalAccountToUserStorageAccount(internalAccount); + + expect(userStorageAccount).toStrictEqual({ + [USER_STORAGE_VERSION_KEY]: USER_STORAGE_VERSION, + a: internalAccount.address, + i: internalAccount.id, + n: internalAccount.metadata.name, + }); + }); + + it('should map an internal account to a user storage account with non-default account name', () => { + const internalAccount = { + address: '0x123', + id: '1', + metadata: { + name: 'My Account', + nameLastUpdatedAt: 1620000000000, + keyring: { + type: KeyringTypes.hd, + }, + }, + } as InternalAccount; + + const userStorageAccount = + mapInternalAccountToUserStorageAccount(internalAccount); + + expect(userStorageAccount).toStrictEqual({ + [USER_STORAGE_VERSION_KEY]: USER_STORAGE_VERSION, + a: internalAccount.address, + i: internalAccount.id, + n: internalAccount.metadata.name, + nlu: internalAccount.metadata.nameLastUpdatedAt, + }); + }); + }); + + describe('doesInternalAccountHaveCorrectKeyringType', () => { + it('should return true if the internal account has the correct keyring type', () => { + const internalAccount = { + metadata: { + keyring: { + type: KeyringTypes.hd, + }, + }, + } as InternalAccount; + + expect(doesInternalAccountHaveCorrectKeyringType(internalAccount)).toBe( + true, + ); + }); + + it('should return false if the internal account does not have the correct keyring type', () => { + const internalAccount = { + metadata: { + keyring: { + type: KeyringTypes.snap, + }, + }, + } as InternalAccount; + + expect(doesInternalAccountHaveCorrectKeyringType(internalAccount)).toBe( + false, + ); + }); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.ts similarity index 71% rename from packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts rename to packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.ts index 374063a20f..59a8048f10 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.ts @@ -1,3 +1,4 @@ +import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { @@ -5,22 +6,7 @@ import { USER_STORAGE_VERSION, LOCALIZED_DEFAULT_ACCOUNT_NAMES, } from './constants'; - -export type UserStorageAccount = { - /** - * The Version 'v' of the User Storage. - * NOTE - will allow us to support upgrade/downgrades in the future - */ - [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION; - /** the id 'i' of the account */ - i: string; - /** the address 'a' of the account */ - a: string; - /** the name 'n' of the account */ - n: string; - /** the nameLastUpdatedAt timestamp 'nlu' of the account */ - nlu?: number; -}; +import type { UserStorageAccount } from './types'; /** * Tells if the given name is a default account name. @@ -55,3 +41,14 @@ export const mapInternalAccountToUserStorageAccount = ( ...(isNameDefaultAccountName(name) ? {} : { nlu: nameLastUpdatedAt }), }; }; + +/** + * Checks if the given internal account has the correct keyring type. + * @param account - The internal account to check + * @returns Returns true if the internal account has the correct keyring type, false otherwise. + */ +export function doesInternalAccountHaveCorrectKeyringType( + account: InternalAccount, +) { + return account.metadata.keyring.type === KeyringTypes.hd; +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.test.ts deleted file mode 100644 index a28ffa6d70..0000000000 --- a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getMockRandomDefaultAccountName } from '../__fixtures__/mockAccounts'; -import { isNameDefaultAccountName } from './user-storage'; - -describe('user-storage/acounts/isNameDefaultAccountName', () => { - it('should return true for default account names', () => { - expect( - isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 89`), - ).toBe(true); - expect( - isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 1`), - ).toBe(true); - expect( - isNameDefaultAccountName(`${getMockRandomDefaultAccountName()} 123543`), - ).toBe(true); - }); - - it('should return false for non-default account names', () => { - expect(isNameDefaultAccountName('My Account')).toBe(false); - expect(isNameDefaultAccountName('Mon compte 34')).toBe(false); - }); -}); From 330adab1ea93b595f1bef7deca269afe3f87600f Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 09:42:35 +0100 Subject: [PATCH 2/9] fix: call for controller & messenger instead of storing in memory --- .../account-syncing/controller-integration.ts | 83 +++++-------------- .../account-syncing/setup-subscriptions.ts | 5 +- .../account-syncing/sync-utils.ts | 15 ++-- 3 files changed, 29 insertions(+), 74 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index 4b63c1ba58..e578c482d8 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -14,51 +14,6 @@ import { mapInternalAccountToUserStorageAccount, } from './utils'; -/** - * Initialize and setup events to listen to for account syncing - * @param config - configuration parameters - * @param options - parameters used for initializing and enabling account syncing - */ -export function setupAccountSyncingSubscriptions( - config: AccountSyncingConfig, - options: AccountSyncingOptions, -) { - const { getMessenger, getUserStorageControllerInstance } = options; - const messenger = getMessenger(); - - messenger.subscribe( - 'AccountsController:accountAdded', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (account) => { - if ( - !canPerformAccountSyncing(config, options) || - !getUserStorageControllerInstance().state - .hasAccountSyncingSyncedAtLeastOnce - ) { - return; - } - - await saveInternalAccountToUserStorage(account, config, options); - }, - ); - - messenger.subscribe( - 'AccountsController:accountRenamed', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (account) => { - if ( - !canPerformAccountSyncing(config, options) || - !getUserStorageControllerInstance().state - .hasAccountSyncingSyncedAtLeastOnce - ) { - return; - } - - await saveInternalAccountToUserStorage(account, config, options); - }, - ); -} - /** * Saves an individual internal account to the user storage. * @param internalAccount - The internal account to save @@ -74,7 +29,6 @@ export async function saveInternalAccountToUserStorage( const { isAccountSyncingEnabled } = config; const { getUserStorageControllerInstance } = options; - const userStorageControllerInstance = getUserStorageControllerInstance(); if ( !isAccountSyncingEnabled || @@ -90,7 +44,7 @@ export async function saveInternalAccountToUserStorage( const mappedAccount = mapInternalAccountToUserStorageAccount(internalAccount); - await userStorageControllerInstance.performSetStorage( + await getUserStorageControllerInstance().performSetStorage( // ESLint is confused here. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${USER_STORAGE_FEATURE_NAMES.accounts}.${internalAccount.address}`, @@ -99,7 +53,9 @@ export async function saveInternalAccountToUserStorage( } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); throw new Error( - `${userStorageControllerInstance.name} - failed to save account to user storage - ${errorMessage}`, + `${ + getUserStorageControllerInstance().name + } - failed to save account to user storage - ${errorMessage}`, ); } } @@ -115,7 +71,6 @@ export async function saveInternalAccountsListToUserStorage( ): Promise { const { isAccountSyncingEnabled } = config; const { getUserStorageControllerInstance } = options; - const userStorageControllerInstance = getUserStorageControllerInstance(); if (!isAccountSyncingEnabled) { return; @@ -131,7 +86,7 @@ export async function saveInternalAccountsListToUserStorage( mapInternalAccountToUserStorageAccount, ); - await userStorageControllerInstance.performBatchSetStorage( + await getUserStorageControllerInstance().performBatchSetStorage( USER_STORAGE_FEATURE_NAMES.accounts, internalAccountsListFormattedForUserStorage.map((account) => [ account.a, @@ -171,11 +126,11 @@ export async function syncInternalAccountsWithUserStorage( onAccountSyncErroneousSituation, } = config; const { getMessenger, getUserStorageControllerInstance } = options; - const messenger = getMessenger(); - const userStorageControllerInstance = getUserStorageControllerInstance(); try { - await userStorageControllerInstance.setIsAccountSyncingInProgress(true); + await getUserStorageControllerInstance().setIsAccountSyncingInProgress( + true, + ); const userStorageAccountsList = await getUserStorageAccountsList(options); @@ -184,7 +139,7 @@ export async function syncInternalAccountsWithUserStorage( { isAccountSyncingEnabled }, options, ); - await userStorageControllerInstance.setHasAccountSyncingSyncedAtLeastOnce( + await getUserStorageControllerInstance().setHasAccountSyncingSyncedAtLeastOnce( true, ); return; @@ -213,7 +168,7 @@ export async function syncInternalAccountsWithUserStorage( // Create new accounts to match the user storage accounts list for (let i = 0; i < numberOfAccountsToAdd; i++) { - await messenger.call('KeyringController:addNewAccount'); + await getMessenger().call('KeyringController:addNewAccount'); onAccountAdded?.(); } } @@ -262,7 +217,7 @@ export async function syncInternalAccountsWithUserStorage( // Internal account has default name if (isInternalAccountNameDefault) { if (!isUserStorageAccountNameDefault) { - messenger.call( + getMessenger().call( 'AccountsController:updateAccountMetadata', internalAccount.id, { @@ -296,7 +251,7 @@ export async function syncInternalAccountsWithUserStorage( } } - messenger.call( + getMessenger().call( 'AccountsController:updateAccountMetadata', internalAccount.id, { @@ -321,7 +276,7 @@ export async function syncInternalAccountsWithUserStorage( // Save the internal accounts list to the user storage if (internalAccountsToBeSavedToUserStorage.length) { - await userStorageControllerInstance.performBatchSetStorage( + await getUserStorageControllerInstance().performBatchSetStorage( USER_STORAGE_FEATURE_NAMES.accounts, internalAccountsToBeSavedToUserStorage.map((account) => [ account.address, @@ -338,7 +293,7 @@ export async function syncInternalAccountsWithUserStorage( ); if (userStorageAccountsToBeDeleted.length) { - await userStorageControllerInstance.performBatchDeleteStorage( + await getUserStorageControllerInstance().performBatchDeleteStorage( USER_STORAGE_FEATURE_NAMES.accounts, userStorageAccountsToBeDeleted.map((account) => account.a), ); @@ -349,15 +304,19 @@ export async function syncInternalAccountsWithUserStorage( // We do this here and not in the finally statement because we want to make sure that // the accounts are saved / updated / deleted at least once before we set this flag - await userStorageControllerInstance.setHasAccountSyncingSyncedAtLeastOnce( + await getUserStorageControllerInstance().setHasAccountSyncingSyncedAtLeastOnce( true, ); } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); throw new Error( - `${userStorageControllerInstance.name} - failed to sync user storage accounts list - ${errorMessage}`, + `${ + getUserStorageControllerInstance().name + } - failed to sync user storage accounts list - ${errorMessage}`, ); } finally { - await userStorageControllerInstance.setIsAccountSyncingInProgress(false); + await getUserStorageControllerInstance().setIsAccountSyncingInProgress( + false, + ); } } diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts index 33d506d396..5e1732e842 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.ts @@ -12,9 +12,8 @@ export function setupAccountSyncingSubscriptions( options: AccountSyncingOptions, ) { const { getMessenger, getUserStorageControllerInstance } = options; - const messenger = getMessenger(); - messenger.subscribe( + getMessenger().subscribe( 'AccountsController:accountAdded', // eslint-disable-next-line @typescript-eslint/no-misused-promises async (account) => { @@ -30,7 +29,7 @@ export function setupAccountSyncingSubscriptions( }, ); - messenger.subscribe( + getMessenger().subscribe( 'AccountsController:accountRenamed', // eslint-disable-next-line @typescript-eslint/no-misused-promises async (account) => { diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts index 23c6e8c318..7e920c2f06 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts @@ -21,12 +21,11 @@ export function canPerformAccountSyncing( const { isAccountSyncingEnabled } = config; const { getMessenger, getUserStorageControllerInstance } = options; - const messenger = getMessenger(); - const userStorageControllerInstance = getUserStorageControllerInstance(); - const { isProfileSyncingEnabled, isAccountSyncingInProgress } = - userStorageControllerInstance.state; - const isAuthEnabled = messenger.call('AuthenticationController:isSignedIn'); + getUserStorageControllerInstance().state; + const isAuthEnabled = getMessenger().call( + 'AuthenticationController:isSignedIn', + ); try { if ( @@ -54,10 +53,9 @@ export async function getInternalAccountsList( options: AccountSyncingOptions, ): Promise { const { getMessenger } = options; - const messenger = getMessenger(); // eslint-disable-next-line @typescript-eslint/await-thenable - const internalAccountsList = await messenger.call( + const internalAccountsList = await getMessenger().call( 'AccountsController:listAccounts', ); @@ -74,10 +72,9 @@ export async function getUserStorageAccountsList( options: AccountSyncingOptions, ): Promise { const { getUserStorageControllerInstance } = options; - const userStorageControllerInstance = getUserStorageControllerInstance(); const rawAccountsListResponse = - await userStorageControllerInstance.performGetStorageAllFeatureEntries( + await getUserStorageControllerInstance().performGetStorageAllFeatureEntries( USER_STORAGE_FEATURE_NAMES.accounts, ); From 2cd1294710556311b16e5b5ad2b9198024bbfea7 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 10:36:29 +0100 Subject: [PATCH 3/9] chore: test coverage to 100% --- .../UserStorageController.test.ts | 52 +++++++- .../controller-integration.test.ts | 102 ++++++++++++++++ .../account-syncing/controller-integration.ts | 5 +- .../account-syncing/sync-utils.test.ts | 113 +++++++----------- .../account-syncing/sync-utils.ts | 22 ++-- .../account-syncing/utils.test.ts | 45 ++++--- 6 files changed, 226 insertions(+), 113 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index 82e50f1a33..5aa1f3b8a1 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -845,9 +845,14 @@ describe('user-storage/user-storage-controller - syncInternalAccountsWithUserSto AccountSyncControllerIntegrationModule, 'syncInternalAccountsWithUserStorage', ); + const mockSaveInternalAccountToUserStorage = jest.spyOn( + AccountSyncControllerIntegrationModule, + 'saveInternalAccountToUserStorage', + ); return { messenger: messengerMocks.messenger, mockSyncInternalAccountsWithUserStorage, + mockSaveInternalAccountToUserStorage, }; }; @@ -865,11 +870,35 @@ describe('user-storage/user-storage-controller - syncInternalAccountsWithUserSto // This is done to prevent creating unnecessary nock instances in this test isAccountSyncingEnabled: false, }, + config: { + accountSyncing: { + onAccountAdded: jest.fn(), + onAccountNameUpdated: jest.fn(), + onAccountSyncErroneousSituation: jest.fn(), + }, + }, }); - mockSyncInternalAccountsWithUserStorage.mockImplementation(async () => { - return undefined; - }); + mockSyncInternalAccountsWithUserStorage.mockImplementation( + async ( + { + onAccountAdded, + onAccountNameUpdated, + onAccountSyncErroneousSituation, + }, + { + getMessenger = jest.fn(), + getUserStorageControllerInstance = jest.fn(), + }, + ) => { + onAccountAdded?.(); + onAccountNameUpdated?.(); + onAccountSyncErroneousSituation?.('error message'); + getMessenger(); + getUserStorageControllerInstance(); + return undefined; + }, + ); await controller.syncInternalAccountsWithUserStorage(); @@ -905,9 +934,20 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag }, }); - mockSaveInternalAccountToUserStorage.mockImplementation(async () => { - return undefined; - }); + mockSaveInternalAccountToUserStorage.mockImplementation( + async ( + _internalAccount, + _config, + { + getMessenger = jest.fn(), + getUserStorageControllerInstance = jest.fn(), + }, + ) => { + getMessenger(); + getUserStorageControllerInstance(); + return undefined; + }, + ); await controller.saveInternalAccountToUserStorage({ id: '1', diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts index 855fa81b24..e28e7cd7e7 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts @@ -23,8 +23,81 @@ import { mockUserStorageMessengerForAccountSyncing, } from './__fixtures__/test-utils'; import * as AccountSyncingControllerIntegrationModule from './controller-integration'; +import * as AccountSyncingUtils from './sync-utils'; import * as AccountsUserStorageModule from './utils'; +describe('user-storage/account-syncing/controller-integration - saveInternalAccountsListToUserStorage() tests', () => { + it('returns void if account syncing is not enabled', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + }; + }; + + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: false, + }, + getMetaMetricsState: () => true, + }); + + const mockPerformBatchSetStorage = jest + .spyOn(controller, 'performBatchSetStorage') + .mockImplementation(() => Promise.resolve()); + + await AccountSyncingControllerIntegrationModule.saveInternalAccountsListToUserStorage( + { + isAccountSyncingEnabled: false, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mockPerformBatchSetStorage).not.toHaveBeenCalled(); + }); + + it('returns void if account syncing is enabled but the internal accounts list is empty', async () => { + const arrangeMocks = async () => { + return { + messengerMocks: mockUserStorageMessengerForAccountSyncing(), + }; + }; + + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + jest + .spyOn(AccountSyncingUtils, 'getInternalAccountsList') + .mockResolvedValue([]); + + const mockPerformBatchSetStorage = jest + .spyOn(controller, 'performBatchSetStorage') + .mockImplementation(() => Promise.resolve()); + + await AccountSyncingControllerIntegrationModule.saveInternalAccountsListToUserStorage( + { + isAccountSyncingEnabled: true, + }, + { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }, + ); + + expect(mockPerformBatchSetStorage).not.toHaveBeenCalled(); + }); +}); + describe('user-storage/account-syncing/controller-integration - syncInternalAccountsWithUserStorage() tests', () => { it('returns void if UserStorage is not enabled', async () => { const arrangeMocks = async () => { @@ -50,6 +123,8 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }, }); + await controller.setIsAccountSyncingReadyToBeDispatched(true); + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( { isAccountSyncingEnabled: true, @@ -1341,6 +1416,33 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco ); }); + it('does not save an internal account to user storage when the AccountsController:accountRenamed event is fired and account syncing has never been dispatched at least once', async () => { + const { messengerMocks } = await arrangeMocksForAccounts(); + const { baseMessenger, messenger } = messengerMocks; + + new UserStorageController({ + messenger, + env: { + isAccountSyncingEnabled: true, + }, + getMetaMetricsState: () => true, + }); + + const mockSaveInternalAccountToUserStorage = jest + .spyOn( + AccountSyncingControllerIntegrationModule, + 'saveInternalAccountToUserStorage', + ) + .mockImplementation(); + + baseMessenger.publish( + 'AccountsController:accountRenamed', + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + ); + + expect(mockSaveInternalAccountToUserStorage).not.toHaveBeenCalled(); + }); + it('saves an internal account to user storage when the AccountsController:accountAdded event is fired', async () => { const { messengerMocks } = await arrangeMocksForAccounts(); const { baseMessenger, messenger } = messengerMocks; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index e578c482d8..bba7eb5341 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -51,6 +51,7 @@ export async function saveInternalAccountToUserStorage( JSON.stringify(mappedAccount), ); } catch (e) { + // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); throw new Error( `${ @@ -78,7 +79,7 @@ export async function saveInternalAccountsListToUserStorage( const internalAccountsList = await getInternalAccountsList(options); - if (!internalAccountsList) { + if (!internalAccountsList?.length) { return; } @@ -190,6 +191,7 @@ export async function syncInternalAccountsWithUserStorage( ); // If the account is not present in user storage + // istanbul ignore next if (!userStorageAccount) { // If the account was just added in the previous step, skip saving it, it's likely to be a bogus account if (newlyAddedAccounts.includes(internalAccount)) { @@ -308,6 +310,7 @@ export async function syncInternalAccountsWithUserStorage( true, ); } catch (e) { + // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); throw new Error( `${ diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts index 90a5dccc2f..c19d698c22 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.test.ts @@ -11,87 +11,57 @@ import * as utils from './utils'; describe('user-storage/account-syncing/sync-utils', () => { describe('canPerformAccountSyncing', () => { - it('returns false if profile syncing is not enabled', () => { - const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; + const arrangeMocks = ({ + isAccountSyncingEnabled = true, + isProfileSyncingEnabled = true, + isAccountSyncingInProgress = false, + messengerCallControllerAndAction = 'AuthenticationController:isSignedIn', + messengerCallCallback = () => true, + }) => { + const config: AccountSyncingConfig = { isAccountSyncingEnabled }; const options: AccountSyncingOptions = { getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockReturnValue(true), - }), - getUserStorageControllerInstance: jest.fn().mockReturnValue({ - state: { - isProfileSyncingEnabled: false, - isAccountSyncingInProgress: false, - }, - }), - }; - - expect(canPerformAccountSyncing(config, options)).toBe(false); - }); - - it('returns false if authentication is not enabled', () => { - const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; - const options: AccountSyncingOptions = { - getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockReturnValue(false), - }), - getUserStorageControllerInstance: jest.fn().mockReturnValue({ - state: { - isProfileSyncingEnabled: true, - isAccountSyncingInProgress: false, - }, - }), - }; - - expect(canPerformAccountSyncing(config, options)).toBe(false); - }); - - it('returns false if account syncing is not enabled', () => { - const config: AccountSyncingConfig = { isAccountSyncingEnabled: false }; - const options: AccountSyncingOptions = { - getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockReturnValue(true), + call: jest + .fn() + .mockImplementation((controllerAndActionName) => + controllerAndActionName === messengerCallControllerAndAction + ? messengerCallCallback() + : null, + ), }), getUserStorageControllerInstance: jest.fn().mockReturnValue({ state: { - isProfileSyncingEnabled: true, - isAccountSyncingInProgress: false, + isProfileSyncingEnabled, + isAccountSyncingInProgress, }, }), }; - expect(canPerformAccountSyncing(config, options)).toBe(false); - }); - - it('returns false if account syncing is in progress', () => { - const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; - const options: AccountSyncingOptions = { - getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockReturnValue(true), - }), - getUserStorageControllerInstance: jest.fn().mockReturnValue({ - state: { - isProfileSyncingEnabled: true, - isAccountSyncingInProgress: true, - }, - }), - }; + return { config, options }; + }; + + const failureCases = [ + ['profile syncing is not enabled', { isProfileSyncingEnabled: false }], + [ + 'authentication is not enabled', + { + messengerCallControllerAndAction: + 'AuthenticationController:isSignedIn', + messengerCallCallback: () => false, + }, + ], + ['account syncing is not enabled', { isAccountSyncingEnabled: false }], + ['account syncing is in progress', { isAccountSyncingInProgress: true }], + ] as const; + + it.each(failureCases)('returns false if %s', (_message, mocks) => { + const { config, options } = arrangeMocks(mocks); expect(canPerformAccountSyncing(config, options)).toBe(false); }); it('returns true if all conditions are met', () => { - const config: AccountSyncingConfig = { isAccountSyncingEnabled: true }; - const options: AccountSyncingOptions = { - getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockReturnValue(true), - }), - getUserStorageControllerInstance: jest.fn().mockReturnValue({ - state: { - isProfileSyncingEnabled: true, - isAccountSyncingInProgress: false, - }, - }), - }; + const { config, options } = arrangeMocks({}); expect(canPerformAccountSyncing(config, options)).toBe(true); }); @@ -106,7 +76,13 @@ describe('user-storage/account-syncing/sync-utils', () => { const options: AccountSyncingOptions = { getMessenger: jest.fn().mockReturnValue({ - call: jest.fn().mockResolvedValue(internalAccounts), + call: jest + .fn() + .mockImplementation((controllerAndActionName) => + controllerAndActionName === 'AccountsController:listAccounts' + ? internalAccounts + : null, + ), }), getUserStorageControllerInstance: jest.fn(), }; @@ -125,6 +101,7 @@ describe('user-storage/account-syncing/sync-utils', () => { describe('getUserStorageAccountsList', () => { it('returns parsed user storage accounts list', async () => { const rawAccounts = ['{"id":"1"}', '{"id":"2"}']; + const options: AccountSyncingOptions = { getUserStorageControllerInstance: jest.fn().mockReturnValue({ performGetStorageAllFeatureEntries: jest diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts index 7e920c2f06..c20a6ca44d 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/sync-utils.ts @@ -27,22 +27,16 @@ export function canPerformAccountSyncing( 'AuthenticationController:isSignedIn', ); - try { - if ( - !isProfileSyncingEnabled || - !isAuthEnabled || - !isAccountSyncingEnabled - ) { - return false; - } - if (isAccountSyncingInProgress) { - return false; - } - - return true; - } catch { + if ( + !isProfileSyncingEnabled || + !isAuthEnabled || + !isAccountSyncingEnabled || + isAccountSyncingInProgress + ) { return false; } + + return true; } /** diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts index d9c6dba4f0..bd8123822a 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/utils.test.ts @@ -29,19 +29,19 @@ describe('user-storage/account-syncing/utils', () => { }); }); describe('mapInternalAccountToUserStorageAccount', () => { - it('should map an internal account to a user storage account with default account name', () => { - const internalAccount = { - address: '0x123', - id: '1', - metadata: { - name: `${getMockRandomDefaultAccountName()} 1`, - nameLastUpdatedAt: 1620000000000, - keyring: { - type: KeyringTypes.hd, - }, + const internalAccount = { + address: '0x123', + id: '1', + metadata: { + name: `${getMockRandomDefaultAccountName()} 1`, + nameLastUpdatedAt: 1620000000000, + keyring: { + type: KeyringTypes.hd, }, - } as InternalAccount; + }, + } as InternalAccount; + it('should map an internal account to a user storage account with default account name', () => { const userStorageAccount = mapInternalAccountToUserStorageAccount(internalAccount); @@ -54,27 +54,24 @@ describe('user-storage/account-syncing/utils', () => { }); it('should map an internal account to a user storage account with non-default account name', () => { - const internalAccount = { - address: '0x123', - id: '1', + const internalAccountWithCustomName = { + ...internalAccount, metadata: { + ...internalAccount.metadata, name: 'My Account', - nameLastUpdatedAt: 1620000000000, - keyring: { - type: KeyringTypes.hd, - }, }, } as InternalAccount; - const userStorageAccount = - mapInternalAccountToUserStorageAccount(internalAccount); + const userStorageAccount = mapInternalAccountToUserStorageAccount( + internalAccountWithCustomName, + ); expect(userStorageAccount).toStrictEqual({ [USER_STORAGE_VERSION_KEY]: USER_STORAGE_VERSION, - a: internalAccount.address, - i: internalAccount.id, - n: internalAccount.metadata.name, - nlu: internalAccount.metadata.nameLastUpdatedAt, + a: internalAccountWithCustomName.address, + i: internalAccountWithCustomName.id, + n: internalAccountWithCustomName.metadata.name, + nlu: internalAccountWithCustomName.metadata.nameLastUpdatedAt, }); }); }); From deaf74f762ac34f55473d665370ba2fdef397263 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 10:38:52 +0100 Subject: [PATCH 4/9] fix: test names --- .../user-storage/account-syncing/setup-subscriptions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts index c50ca425b9..3e7d5811f1 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/setup-subscriptions.test.ts @@ -1,6 +1,6 @@ import { setupAccountSyncingSubscriptions } from './setup-subscriptions'; -describe('setupAccountSyncing', () => { +describe('user-storage/account-syncing/setup-subscriptions - setupAccountSyncingSubscriptions', () => { it('should subscribe to accountAdded and accountRenamed events', () => { const config = { isAccountSyncingEnabled: true }; const options = { From dc5701d5e33cac52f3e094763732e68646166b73 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 12:02:34 +0100 Subject: [PATCH 5/9] feat: use arrangeMocks for controller-integration tests --- .../controller-integration.test.ts | 1545 +++++++---------- 1 file changed, 628 insertions(+), 917 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts index e28e7cd7e7..7ea095e612 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts @@ -26,21 +26,57 @@ import * as AccountSyncingControllerIntegrationModule from './controller-integra import * as AccountSyncingUtils from './sync-utils'; import * as AccountsUserStorageModule from './utils'; +// By default, everything is disabled +const baseState = { + isProfileSyncingEnabled: false, + isProfileSyncingUpdateLoading: false, + hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, +}; + +const arrangeMocks = async ({ + isAccountSyncingEnabled = false, + stateOverrides = baseState as Partial, + messengerMockOptions = undefined as Parameters< + typeof mockUserStorageMessengerForAccountSyncing + >[0], +}) => { + const messengerMocks = + mockUserStorageMessengerForAccountSyncing(messengerMockOptions); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + env: { + isAccountSyncingEnabled, + }, + getMetaMetricsState: () => true, + state: { + ...baseState, + ...stateOverrides, + }, + }); + + const config = { + isAccountSyncingEnabled, + }; + + const options = { + getMessenger: () => messengerMocks.messenger, + getUserStorageControllerInstance: () => controller, + }; + + return { + messengerMocks, + controller, + config, + options, + }; +}; + describe('user-storage/account-syncing/controller-integration - saveInternalAccountsListToUserStorage() tests', () => { it('returns void if account syncing is not enabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - }; - }; - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: false, - }, - getMetaMetricsState: () => true, + const { controller, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: false, }); const mockPerformBatchSetStorage = jest @@ -48,50 +84,29 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco .mockImplementation(() => Promise.resolve()); await AccountSyncingControllerIntegrationModule.saveInternalAccountsListToUserStorage( - { - isAccountSyncingEnabled: false, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(mockPerformBatchSetStorage).not.toHaveBeenCalled(); }); it('returns void if account syncing is enabled but the internal accounts list is empty', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - }; - }; - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, + const { controller, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, }); - jest - .spyOn(AccountSyncingUtils, 'getInternalAccountsList') - .mockResolvedValue([]); - const mockPerformBatchSetStorage = jest .spyOn(controller, 'performBatchSetStorage') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(AccountSyncingUtils, 'getInternalAccountsList') + .mockResolvedValue([]); + await AccountSyncingControllerIntegrationModule.saveInternalAccountsListToUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(mockPerformBatchSetStorage).not.toHaveBeenCalled(); @@ -100,123 +115,71 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco describe('user-storage/account-syncing/controller-integration - syncInternalAccountsWithUserStorage() tests', () => { it('returns void if UserStorage is not enabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - mockAPI: mockEndpointGetUserStorage(), - }; - }; + const { config, controller, messengerMocks, options } = await arrangeMocks( + {}, + ); - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - env: { - isAccountSyncingEnabled: true, - }, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - hasAccountSyncingSyncedAtLeastOnce: false, - isAccountSyncingReadyToBeDispatched: false, - isAccountSyncingInProgress: false, - }, - }); + await mockEndpointGetUserStorage(); await controller.setIsAccountSyncingReadyToBeDispatched(true); await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(messengerMocks.mockAccountsListAccounts).not.toHaveBeenCalled(); }); it('returns void if account syncing feature flag is disabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - env: { - isAccountSyncingEnabled: false, - }, + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: false, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: false, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(false); }); it('throws if AccountsController:listAccounts fails or returns an empty list', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: [], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: [], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }, + ), + }; + await expect( AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ), ).rejects.toThrow(expect.any(Error)); @@ -224,69 +187,55 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('uploads accounts list to user storage if user storage is empty', async () => { - const mockUserStorageAccountsResponse = { - status: 404, - body: [], - }; + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( + 0, + 2, + ) as InternalAccount[], + }, + }, + }); - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( - 0, - 2, - ) as InternalAccount[], + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 404, + body: [], }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - mockUserStorageAccountsResponse, - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - const decryptedBody = await decryptBatchUpsertBody( - requestBody, - MOCK_STORAGE_KEY, - ); - - const expectedBody = createExpectedAccountSyncBatchUpsertBody( - MOCK_INTERNAL_ACCOUNTS.ALL.slice(0, 2).map((account) => [ - account.address, - account as InternalAccount, - ]), - MOCK_STORAGE_KEY, - ); - - expect(decryptedBody).toStrictEqual(expectedBody); - }, - ), + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + const decryptedBody = await decryptBatchUpsertBody( + requestBody, + MOCK_STORAGE_KEY, + ); + + const expectedBody = createExpectedAccountSyncBatchUpsertBody( + MOCK_INTERNAL_ACCOUNTS.ALL.slice(0, 2).map((account) => [ + account.address, + account as InternalAccount, + ]), + MOCK_STORAGE_KEY, + ); + + expect(decryptedBody).toStrictEqual(expectedBody); }, - }; + ), }; - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -295,72 +244,55 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('creates internal accounts if user storage has more accounts. it also updates hasAccountSyncingSyncedAtLeastOnce accordingly', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchDeleteUserStorage: - mockEndpointBatchDeleteUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - if (typeof requestBody === 'string') { - return; - } - - const expectedBody = createExpectedAccountSyncBatchDeleteBody( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( - (account) => - !MOCK_INTERNAL_ACCOUNTS.ONE.find( - (internalAccount) => - internalAccount.address === account.a, - ), - ).map((account) => account.a), - MOCK_STORAGE_KEY, - ); + const { messengerMocks, controller, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], + }, + }, + }); - expect(requestBody.batch_delete).toStrictEqual(expectedBody); - }, + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, ), + }, + ), + mockEndpointBatchDeleteUserStorage: mockEndpointBatchDeleteUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + if (typeof requestBody === 'string') { + return; + } + + const expectedBody = createExpectedAccountSyncBatchDeleteBody( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( + (account) => + !MOCK_INTERNAL_ACCOUNTS.ONE.find( + (internalAccount) => internalAccount.address === account.a, + ), + ).map((account) => account.a), + MOCK_STORAGE_KEY, + ); + + expect(requestBody.batch_delete).toStrictEqual(expectedBody); }, - }; + ), }; - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -378,28 +310,34 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); describe('handles corrupted user storage gracefully', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.TWO_DEFAULT_NAMES_WITH_ONE_BOGUS, - ), - }; - }; - const arrangeMocksForBogusAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { accounts: { accountsList: MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], }, - }), + }, + }); + + return { + config, + options, + messengerMocks, mockAPI: { mockEndpointGetUserStorage: await mockEndpointGetUserStorageAllFeatureEntries( USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.TWO_DEFAULT_NAMES_WITH_ONE_BOGUS, + ), + }, ), mockEndpointBatchDeleteUserStorage: mockEndpointBatchDeleteUserStorage( @@ -430,23 +368,11 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }; it('does not save the bogus account to user storage, and deletes it from user storage', async () => { - const { messengerMocks, mockAPI } = await arrangeMocksForBogusAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); + const { config, options, mockAPI } = await arrangeMocksForBogusAccounts(); await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(mockAPI.mockEndpointGetUserStorage.isDone()).toBe(true); @@ -457,24 +383,14 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('fires the onAccountSyncErroneousSituation callback in erroneous situations', async () => { const onAccountSyncErroneousSituation = jest.fn(); - const { messengerMocks } = await arrangeMocksForBogusAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); + const { config, options } = await arrangeMocksForBogusAccounts(); await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( { - isAccountSyncingEnabled: true, + ...config, onAccountSyncErroneousSituation, }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + options, ); expect(onAccountSyncErroneousSituation).toHaveBeenCalledTimes(1); @@ -482,75 +398,60 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('fires the onAccountAdded callback when adding an account', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], + }, + }, + }); - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchDeleteUserStorage: - mockEndpointBatchDeleteUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - undefined, - async (_uri, requestBody) => { - if (typeof requestBody === 'string') { - return; - } - - const expectedBody = createExpectedAccountSyncBatchDeleteBody( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( - (account) => - !MOCK_INTERNAL_ACCOUNTS.ONE.find( - (internalAccount) => - internalAccount.address === account.a, - ), - ).map((account) => account.a), - MOCK_STORAGE_KEY, - ); - - expect(requestBody.batch_delete).toStrictEqual(expectedBody); - }, + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, ), + }, + ), + mockEndpointBatchDeleteUserStorage: mockEndpointBatchDeleteUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + undefined, + async (_uri, requestBody) => { + if (typeof requestBody === 'string') { + return; + } + + const expectedBody = createExpectedAccountSyncBatchDeleteBody( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.filter( + (account) => + !MOCK_INTERNAL_ACCOUNTS.ONE.find( + (internalAccount) => internalAccount.address === account.a, + ), + ).map((account) => account.a), + MOCK_STORAGE_KEY, + ); + + expect(requestBody.batch_delete).toStrictEqual(expectedBody); }, - }; + ), }; const onAccountAdded = jest.fn(); - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( { - isAccountSyncingEnabled: true, + ...config, onAccountAdded, }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -564,56 +465,40 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not create internal accounts if user storage has less accounts', async () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.slice(0, 1), - ), - }; - }; - - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( - 0, - 2, - ) as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( + 0, + 2, + ) as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL.slice(0, 1), + ), + }, + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -626,51 +511,36 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); describe('User storage name is a default name', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, - ), - }; - }; - it('does not update the internal account name if both user storage and internal accounts have default names', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -681,45 +551,38 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not update the internal account name if the internal account name is custom without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, + ), + }, + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -731,45 +594,38 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not update the internal account name if the internal account name is custom with last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_DEFAULT_NAME, + ), + }, + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -782,51 +638,36 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); describe('User storage name is a custom name without last updated', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, - ), - }; - }; - it('updates the internal account name if the internal account name is a default name', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], + }, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -843,41 +684,35 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not update internal account name if both user storage and internal accounts have custom names without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -888,45 +723,38 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not update the internal account name if the internal account name is custom with last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, + ), + }, + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -938,44 +766,40 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('fires the onAccountNameUpdated callback when renaming an internal account', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], }, - }; + }, + }); + + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED, + ), + }, + ), }; const onAccountNameUpdated = jest.fn(); - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); - await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( { - isAccountSyncingEnabled: true, + ...config, onAccountNameUpdated, }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -985,51 +809,36 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); describe('User storage name is a custom name with last updated', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, - ), - }; - }; - it('updates the internal account name if the internal account name is a default name', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_DEFAULT_NAME as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -1046,41 +855,35 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('updates the internal account name and last updated if the internal account name is a custom name without last updated', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITHOUT_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -1099,42 +902,35 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('updates the internal account name and last updated if the user storage account is more recent', async () => { - const arrangeMocksForAccounts = async () => { - const mockGetEntriesResponse = await mockUserStorageAccountsResponse(); - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - mockGetEntriesResponse, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, + ), + }, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -1153,45 +949,38 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco }); it('does not update the internal account if the user storage account is less recent', async () => { - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], - }, - }), - mockAPI: { - mockEndpointGetUserStorage: - await mockEndpointGetUserStorageAllFeatureEntries( - USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), - ), - mockEndpointBatchUpsertUserStorage: - mockEndpointBatchUpsertUserStorage( - USER_STORAGE_FEATURE_NAMES.accounts, - ), + const { messengerMocks, config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], }, - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocksForAccounts(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointGetUserStorage: + await mockEndpointGetUserStorageAllFeatureEntries( + USER_STORAGE_FEATURE_NAMES.accounts, + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED, + ), + }, + ), + mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( + USER_STORAGE_FEATURE_NAMES.accounts, + ), + }; + await AccountSyncingControllerIntegrationModule.syncInternalAccountsWithUserStorage( - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); mockAPI.mockEndpointGetUserStorage.done(); @@ -1206,170 +995,119 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('user-storage/account-syncing/controller-integration - saveInternalAccountToUserStorage() tests', () => { it('returns void if UserStorage is not enabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - }; - }; + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + }); const mapInternalAccountToUserStorageAccountMock = jest.spyOn( AccountsUserStorageModule, 'mapInternalAccountToUserStorageAccount', ); - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - hasAccountSyncingSyncedAtLeastOnce: false, - isAccountSyncingReadyToBeDispatched: false, - isAccountSyncingInProgress: false, - }, - }); - await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); expect(mapInternalAccountToUserStorageAccountMock).not.toHaveBeenCalled(); }); it('returns void if account syncing feature flag is disabled', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - ), - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: false, - }, - getMetaMetricsState: () => true, + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: false, }); + const mockAPI = { + mockEndpointUpsertUserStorage: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + ), + }; + await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - { - isAccountSyncingEnabled: false, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); - expect(mockAPI.isDone()).toBe(false); + expect(mockAPI.mockEndpointUpsertUserStorage.isDone()).toBe(false); }); it('saves an internal account to user storage', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - ), - }; - }; - - const { messengerMocks, mockAPI } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + const mockAPI = { + mockEndpointUpsertUserStorage: mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + ), + }; await AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ); - expect(mockAPI.isDone()).toBe(true); + expect(mockAPI.mockEndpointUpsertUserStorage.isDone()).toBe(true); }); it('rejects if api call fails', async () => { - const arrangeMocks = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing(), - mockAPI: mockEndpointUpsertUserStorage( - `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, - { status: 500 }, - ), - }; - }; - - const { messengerMocks } = await arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - env: { - isAccountSyncingEnabled: true, + const { config, options } = await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, }, - getMetaMetricsState: () => true, }); + mockEndpointUpsertUserStorage( + `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, + { status: 500 }, + ); + await expect( AccountSyncingControllerIntegrationModule.saveInternalAccountToUserStorage( MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, - { - isAccountSyncingEnabled: true, - }, - { - getMessenger: () => messengerMocks.messenger, - getUserStorageControllerInstance: () => controller, - }, + config, + options, ), ).rejects.toThrow(expect.any(Error)); }); describe('it reacts to other controller events', () => { - const mockUserStorageAccountsResponse = async () => { - return { - status: 200, - body: await createMockUserStorageEntries( - MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, - ), - }; - }; - const arrangeMocksForAccounts = async () => { - return { - messengerMocks: mockUserStorageMessengerForAccountSyncing({ - accounts: { - accountsList: - MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], + const { messengerMocks, controller, config, options } = + await arrangeMocks({ + isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: true, + }, + messengerMockOptions: { + accounts: { + accountsList: + MOCK_INTERNAL_ACCOUNTS.ONE_CUSTOM_NAME_WITH_LAST_UPDATED_MOST_RECENT as InternalAccount[], + }, }, - }), + }); + + return { + config, + options, + controller, + messengerMocks, mockAPI: { mockEndpointGetUserStorage: await mockEndpointGetUserStorageAllFeatureEntries( USER_STORAGE_FEATURE_NAMES.accounts, - await mockUserStorageAccountsResponse(), + { + status: 200, + body: await createMockUserStorageEntries( + MOCK_USER_STORAGE_ACCOUNTS.SAME_AS_INTERNAL_ALL, + ), + }, ), mockEndpointBatchUpsertUserStorage: mockEndpointBatchUpsertUserStorage( @@ -1383,16 +1121,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco }; it('saves an internal account to user storage when the AccountsController:accountRenamed event is fired', async () => { - const { messengerMocks } = await arrangeMocksForAccounts(); - const { baseMessenger, messenger } = messengerMocks; - - const controller = new UserStorageController({ - messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); + const { messengerMocks, controller } = await arrangeMocksForAccounts(); // We need to sync at least once before we listen for other controller events await controller.setHasAccountSyncingSyncedAtLeastOnce(true); @@ -1404,7 +1133,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco ) .mockImplementation(); - baseMessenger.publish( + messengerMocks.baseMessenger.publish( 'AccountsController:accountRenamed', MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ); @@ -1418,15 +1147,6 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco it('does not save an internal account to user storage when the AccountsController:accountRenamed event is fired and account syncing has never been dispatched at least once', async () => { const { messengerMocks } = await arrangeMocksForAccounts(); - const { baseMessenger, messenger } = messengerMocks; - - new UserStorageController({ - messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); const mockSaveInternalAccountToUserStorage = jest .spyOn( @@ -1435,7 +1155,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco ) .mockImplementation(); - baseMessenger.publish( + messengerMocks.baseMessenger.publish( 'AccountsController:accountRenamed', MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ); @@ -1444,16 +1164,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco }); it('saves an internal account to user storage when the AccountsController:accountAdded event is fired', async () => { - const { messengerMocks } = await arrangeMocksForAccounts(); - const { baseMessenger, messenger } = messengerMocks; - - const controller = new UserStorageController({ - messenger, - env: { - isAccountSyncingEnabled: true, - }, - getMetaMetricsState: () => true, - }); + const { controller, messengerMocks } = await arrangeMocksForAccounts(); // We need to sync at least once before we listen for other controller events await controller.setHasAccountSyncingSyncedAtLeastOnce(true); @@ -1465,7 +1176,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco ) .mockImplementation(); - baseMessenger.publish( + messengerMocks.baseMessenger.publish( 'AccountsController:accountAdded', MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ); From 4f9fd4e31c8e98666130fad380b11ff3d18ba2eb Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 12:24:23 +0100 Subject: [PATCH 6/9] chore: better test state defaults --- .../controller-integration.test.ts | 107 +++--------------- 1 file changed, 13 insertions(+), 94 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts index 7ea095e612..d0a9bf109e 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.test.ts @@ -26,9 +26,8 @@ import * as AccountSyncingControllerIntegrationModule from './controller-integra import * as AccountSyncingUtils from './sync-utils'; import * as AccountsUserStorageModule from './utils'; -// By default, everything is disabled const baseState = { - isProfileSyncingEnabled: false, + isProfileSyncingEnabled: true, isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, @@ -36,7 +35,7 @@ const baseState = { }; const arrangeMocks = async ({ - isAccountSyncingEnabled = false, + isAccountSyncingEnabled = true, stateOverrides = baseState as Partial, messengerMockOptions = undefined as Parameters< typeof mockUserStorageMessengerForAccountSyncing @@ -92,9 +91,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco }); it('returns void if account syncing is enabled but the internal accounts list is empty', async () => { - const { controller, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - }); + const { controller, config, options } = await arrangeMocks({}); const mockPerformBatchSetStorage = jest .spyOn(controller, 'performBatchSetStorage') @@ -115,9 +112,11 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco describe('user-storage/account-syncing/controller-integration - syncInternalAccountsWithUserStorage() tests', () => { it('returns void if UserStorage is not enabled', async () => { - const { config, controller, messengerMocks, options } = await arrangeMocks( - {}, - ); + const { config, controller, messengerMocks, options } = await arrangeMocks({ + stateOverrides: { + isProfileSyncingEnabled: false, + }, + }); await mockEndpointGetUserStorage(); @@ -152,10 +151,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('throws if AccountsController:listAccounts fails or returns an empty list', async () => { const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: [], @@ -188,10 +183,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('uploads accounts list to user storage if user storage is empty', async () => { const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( @@ -245,10 +236,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('creates internal accounts if user storage has more accounts. it also updates hasAccountSyncingSyncedAtLeastOnce accordingly', async () => { const { messengerMocks, controller, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], @@ -312,10 +299,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('handles corrupted user storage gracefully', () => { const arrangeMocksForBogusAccounts = async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -399,10 +382,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('fires the onAccountAdded callback when adding an account', async () => { const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: MOCK_INTERNAL_ACCOUNTS.ONE as InternalAccount[], @@ -466,10 +445,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not create internal accounts if user storage has less accounts', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: MOCK_INTERNAL_ACCOUNTS.ALL.slice( @@ -513,10 +488,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('User storage name is a default name', () => { it('does not update the internal account name if both user storage and internal accounts have default names', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -552,10 +523,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not update the internal account name if the internal account name is custom without last updated', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -595,10 +562,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not update the internal account name if the internal account name is custom with last updated', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -640,10 +603,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('User storage name is a custom name without last updated', () => { it('updates the internal account name if the internal account name is a default name', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -685,10 +644,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not update internal account name if both user storage and internal accounts have custom names without last updated', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -724,10 +679,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not update the internal account name if the internal account name is custom with last updated', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -767,10 +718,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('fires the onAccountNameUpdated callback when renaming an internal account', async () => { const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -811,10 +758,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('User storage name is a custom name with last updated', () => { it('updates the internal account name if the internal account name is a default name', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -856,10 +799,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('updates the internal account name and last updated if the internal account name is a custom name without last updated', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -903,10 +842,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('updates the internal account name and last updated if the user storage account is more recent', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -950,10 +885,6 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco it('does not update the internal account if the user storage account is less recent', async () => { const { messengerMocks, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: @@ -996,7 +927,9 @@ describe('user-storage/account-syncing/controller-integration - syncInternalAcco describe('user-storage/account-syncing/controller-integration - saveInternalAccountToUserStorage() tests', () => { it('returns void if UserStorage is not enabled', async () => { const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, + stateOverrides: { + isProfileSyncingEnabled: false, + }, }); const mapInternalAccountToUserStorageAccountMock = jest.spyOn( @@ -1034,12 +967,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco }); it('saves an internal account to user storage', async () => { - const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, - }); + const { config, options } = await arrangeMocks({}); const mockAPI = { mockEndpointUpsertUserStorage: mockEndpointUpsertUserStorage( `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, @@ -1056,12 +984,7 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco }); it('rejects if api call fails', async () => { - const { config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, - }); + const { config, options } = await arrangeMocks({}); mockEndpointUpsertUserStorage( `${USER_STORAGE_FEATURE_NAMES.accounts}.${MOCK_INTERNAL_ACCOUNTS.ONE[0].address}`, @@ -1081,10 +1004,6 @@ describe('user-storage/account-syncing/controller-integration - saveInternalAcco const arrangeMocksForAccounts = async () => { const { messengerMocks, controller, config, options } = await arrangeMocks({ - isAccountSyncingEnabled: true, - stateOverrides: { - isProfileSyncingEnabled: true, - }, messengerMockOptions: { accounts: { accountsList: From cd45f60814a4e35663056f2dce6d8c65199c93c4 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 12:25:38 +0100 Subject: [PATCH 7/9] fix: remove console.log --- .../user-storage/account-syncing/controller-integration.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index bba7eb5341..76ae847214 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -25,8 +25,6 @@ export async function saveInternalAccountToUserStorage( config: AccountSyncingConfig, options: AccountSyncingOptions, ): Promise { - console.log('saveInternalAccountToUserStorage'); - const { isAccountSyncingEnabled } = config; const { getUserStorageControllerInstance } = options; From 8f7298035b0f79b046de50b36434ed0ae4579770 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 12:39:30 +0100 Subject: [PATCH 8/9] fix: lint --- .../user-storage/account-syncing/controller-integration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index 76ae847214..62a1526b07 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -51,6 +51,8 @@ export async function saveInternalAccountToUserStorage( } catch (e) { // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); + // ESLint is confused here. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error( `${ getUserStorageControllerInstance().name @@ -310,6 +312,8 @@ export async function syncInternalAccountsWithUserStorage( } catch (e) { // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); + // ESLint is confused here. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error( `${ getUserStorageControllerInstance().name From e218ba330d5d4cd39e0a0dfc60f28231be9dce0b Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 18 Dec 2024 12:44:55 +0100 Subject: [PATCH 9/9] fix: lint --- .../account-syncing/controller-integration.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts index 62a1526b07..e39498d58b 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/account-syncing/controller-integration.ts @@ -51,12 +51,8 @@ export async function saveInternalAccountToUserStorage( } catch (e) { // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); - // ESLint is confused here. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error( - `${ - getUserStorageControllerInstance().name - } - failed to save account to user storage - ${errorMessage}`, + `UserStorageController - failed to save account to user storage - ${errorMessage}`, ); } } @@ -312,12 +308,8 @@ export async function syncInternalAccountsWithUserStorage( } catch (e) { // istanbul ignore next const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); - // ESLint is confused here. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error( - `${ - getUserStorageControllerInstance().name - } - failed to sync user storage accounts list - ${errorMessage}`, + `UserStorageController - failed to sync user storage accounts list - ${errorMessage}`, ); } finally { await getUserStorageControllerInstance().setIsAccountSyncingInProgress(