-
-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: network-syncing and user storage controller integration (#4687)
## Explanation This is a follow up on #4685, and adds the controller integration for the network mutation syncs. NOTE - we are currently using mock/temporary events that are not yet exposed on the network controller. We will add these network events in an upcoming PR. ## References https://consensyssoftware.atlassian.net/browse/NOTIFY-1032 ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/proflile-sync-controller` - **ADDED**: temporarily added non-existing `NetworkController:networkAdded`; `NetworkController:networkChanged`; and `NetworkController:networkDeleted` events. - These will be provided in a future PR. - **ADDED**: add `isNetworkSyncingEnabled` environment switch to `UserStorageController` to control when we enable network syncing. - **ADDED**: add `startNetworkSyncing()` to initialise and listen to all the events required for network syncing. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate
- Loading branch information
1 parent
d32ada7
commit 6cf64c6
Showing
4 changed files
with
306 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
type WaitForOptions = { | ||
intervalMs?: number; | ||
timeoutMs?: number; | ||
}; | ||
|
||
/** | ||
* Testing Utility - waitFor. Waits for and checks (at an interval) if assertion is reached. | ||
* | ||
* @param assertionFn - assertion function | ||
* @param options - set wait for options | ||
* @returns promise that you need to await in tests | ||
*/ | ||
export const waitFor = async ( | ||
assertionFn: () => void, | ||
options: WaitForOptions = {}, | ||
): Promise<void> => { | ||
const { intervalMs = 50, timeoutMs = 2000 } = options; | ||
|
||
const startTime = Date.now(); | ||
|
||
return new Promise<void>((resolve, reject) => { | ||
const intervalId = setInterval(() => { | ||
try { | ||
assertionFn(); | ||
clearInterval(intervalId); | ||
resolve(); | ||
} catch (error) { | ||
if (Date.now() - startTime >= timeoutMs) { | ||
clearInterval(intervalId); | ||
reject(new Error(`waitFor: timeout reached after ${timeoutMs}ms`)); | ||
} | ||
} | ||
}, intervalMs); | ||
}); | ||
}; |
150 changes: 150 additions & 0 deletions
150
...nc-controller/src/controllers/user-storage/network-syncing/controller-integration.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import type { NotNamespacedBy } from '@metamask/base-controller'; | ||
import { ControllerMessenger } from '@metamask/base-controller'; | ||
import log from 'loglevel'; | ||
|
||
import type { AllowedActions, AllowedEvents } from '..'; | ||
import { MOCK_STORAGE_KEY } from '../__fixtures__'; | ||
import { waitFor } from '../__fixtures__/test-utils'; | ||
import type { UserStorageBaseOptions } from '../services'; | ||
import { createMockNetworkConfiguration } from './__fixtures__/mockNetwork'; | ||
import { startNetworkSyncing } from './controller-integration'; | ||
import * as SyncModule from './sync'; | ||
|
||
jest.mock('loglevel', () => { | ||
const actual = jest.requireActual('loglevel'); | ||
return { | ||
...actual, | ||
default: { | ||
...actual.default, | ||
warn: jest.fn(), | ||
}, | ||
// Mocking an ESModule. | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
__esModule: true, | ||
}; | ||
}); | ||
const warnMock = jest.mocked(log.warn); | ||
|
||
const storageOpts: UserStorageBaseOptions = { | ||
bearerToken: 'MOCK_TOKEN', | ||
storageKey: MOCK_STORAGE_KEY, | ||
}; | ||
|
||
type ExternalEvents = NotNamespacedBy< | ||
'UserStorageController', | ||
AllowedEvents['type'] | ||
>; | ||
const getEvents = (): ExternalEvents[] => [ | ||
'NetworkController:networkAdded', | ||
'NetworkController:networkChanged', | ||
'NetworkController:networkDeleted', | ||
]; | ||
|
||
const testMatrix = [ | ||
{ | ||
event: 'NetworkController:networkAdded' as const, | ||
arrangeSyncFnMock: () => | ||
jest.spyOn(SyncModule, 'addNetwork').mockResolvedValue(), | ||
}, | ||
{ | ||
event: 'NetworkController:networkChanged' as const, | ||
arrangeSyncFnMock: () => | ||
jest.spyOn(SyncModule, 'updateNetwork').mockResolvedValue(), | ||
}, | ||
{ | ||
event: 'NetworkController:networkDeleted' as const, | ||
arrangeSyncFnMock: () => | ||
jest.spyOn(SyncModule, 'deleteNetwork').mockResolvedValue(), | ||
}, | ||
]; | ||
|
||
describe.each(testMatrix)( | ||
'network-syncing/controller-integration - $event', | ||
({ event, arrangeSyncFnMock }) => { | ||
it(`should successfully sync when ${event} is emitted`, async () => { | ||
const syncFnMock = arrangeSyncFnMock(); | ||
const { baseMessenger, messenger, getStorageConfig } = arrangeMocks(); | ||
startNetworkSyncing({ messenger, getStorageConfig }); | ||
baseMessenger.publish(event, createMockNetworkConfiguration()); | ||
|
||
await waitFor(() => { | ||
expect(getStorageConfig).toHaveBeenCalled(); | ||
expect(syncFnMock).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
it('should silently fail is unable to authenticate or get storage key', async () => { | ||
const syncFnMock = arrangeSyncFnMock(); | ||
const { baseMessenger, messenger, getStorageConfig } = arrangeMocks(); | ||
getStorageConfig.mockRejectedValue(new Error('Mock Error')); | ||
startNetworkSyncing({ messenger, getStorageConfig }); | ||
baseMessenger.publish(event, createMockNetworkConfiguration()); | ||
|
||
expect(getStorageConfig).toHaveBeenCalled(); | ||
expect(syncFnMock).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it(`should emit a warning if controller messenger is missing the ${event} event`, async () => { | ||
const { baseMessenger, getStorageConfig } = arrangeMocks(); | ||
|
||
const eventsWithoutNetworkAdded = getEvents().filter((e) => e !== event); | ||
const messenger = mockUserStorageMessenger( | ||
baseMessenger, | ||
eventsWithoutNetworkAdded, | ||
); | ||
|
||
startNetworkSyncing({ messenger, getStorageConfig }); | ||
expect(warnMock).toHaveBeenCalled(); | ||
}); | ||
}, | ||
); | ||
|
||
/** | ||
* Test Utility - arrange mocks and parameters | ||
* @returns the mocks and parameters used when testing `startNetworkSyncing()` | ||
*/ | ||
function arrangeMocks() { | ||
const baseMessenger = mockBaseMessenger(); | ||
const messenger = mockUserStorageMessenger(baseMessenger); | ||
const getStorageConfigMock = jest.fn().mockResolvedValue(storageOpts); | ||
|
||
return { | ||
getStorageConfig: getStorageConfigMock, | ||
baseMessenger, | ||
messenger, | ||
}; | ||
} | ||
|
||
/** | ||
* Test Utility - creates a base messenger so we can invoke/publish events | ||
* @returns Base messenger for publishing events | ||
*/ | ||
function mockBaseMessenger() { | ||
const baseMessenger = new ControllerMessenger< | ||
AllowedActions, | ||
AllowedEvents | ||
>(); | ||
|
||
return baseMessenger; | ||
} | ||
|
||
/** | ||
* Test Utility - creates a UserStorageMessenger to simulate the messenger used inside the UserStorageController | ||
* @param baseMessenger - base messenger to restrict | ||
* @param eventsOverride - provide optional override events | ||
* @returns UserStorageMessenger | ||
*/ | ||
function mockUserStorageMessenger( | ||
baseMessenger: ReturnType<typeof mockBaseMessenger>, | ||
eventsOverride?: ExternalEvents[], | ||
) { | ||
const allowedEvents = eventsOverride ?? getEvents(); | ||
|
||
const messenger = baseMessenger.getRestricted({ | ||
name: 'UserStorageController', | ||
allowedActions: [], | ||
allowedEvents, | ||
}); | ||
|
||
return messenger; | ||
} |
69 changes: 69 additions & 0 deletions
69
...le-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import log from 'loglevel'; | ||
|
||
import type { UserStorageBaseOptions } from '../services'; | ||
import type { UserStorageControllerMessenger } from '../UserStorageController'; | ||
import { addNetwork, deleteNetwork, updateNetwork } from './sync'; | ||
|
||
type SetupNetworkSyncingProps = { | ||
messenger: UserStorageControllerMessenger; | ||
getStorageConfig: () => Promise<UserStorageBaseOptions>; | ||
}; | ||
|
||
/** | ||
* Initialize and setup events to listen to for network syncing | ||
* @param props - parameters used for initializing and enabling network syncing | ||
*/ | ||
export function startNetworkSyncing(props: SetupNetworkSyncingProps) { | ||
const { messenger, getStorageConfig } = props; | ||
|
||
try { | ||
messenger.subscribe( | ||
'NetworkController:networkAdded', | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
async (networkConfiguration) => { | ||
try { | ||
const opts = await getStorageConfig(); | ||
await addNetwork(networkConfiguration, opts); | ||
} catch { | ||
// Silently fail sync | ||
} | ||
}, | ||
); | ||
} catch (e) { | ||
log.warn('NetworkSyncing, event subscription failed', e); | ||
} | ||
|
||
try { | ||
messenger.subscribe( | ||
'NetworkController:networkDeleted', | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
async (networkConfiguration) => { | ||
try { | ||
const opts = await getStorageConfig(); | ||
await deleteNetwork(networkConfiguration, opts); | ||
} catch { | ||
// Silently fail sync | ||
} | ||
}, | ||
); | ||
} catch (e) { | ||
log.warn('NetworkSyncing, event subscription failed', e); | ||
} | ||
|
||
try { | ||
messenger.subscribe( | ||
'NetworkController:networkChanged', | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
async (networkConfiguration) => { | ||
try { | ||
const opts = await getStorageConfig(); | ||
await updateNetwork(networkConfiguration, opts); | ||
} catch { | ||
// Silently fail sync | ||
} | ||
}, | ||
); | ||
} catch (e) { | ||
log.warn('NetworkSyncing, event subscription failed', e); | ||
} | ||
} |