Skip to content

Commit

Permalink
test: add controller-integration - performMainSync() tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Prithpal-Sooriya committed Oct 24, 2024
1 parent b18119c commit 94a67ba
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ import type {
} from './UserStorageController';
import UserStorageController from './UserStorageController';

// Creates the correct typed call params for mocks
type CallParams = {
[K in AllowedActions['type']]: [
K,
...Parameters<Extract<AllowedActions, { type: K }>['handler']>,
];
}[AllowedActions['type']];

const typedMockFn = <Func extends (...args: unknown[]) => unknown>() =>
jest.fn<ReturnType<Func>, Parameters<Func>>();

Expand Down Expand Up @@ -1730,14 +1738,6 @@ function mockUserStorageMessenger(options?: {
const mockAccountsUpdateAccountMetadata = jest.fn().mockResolvedValue(true);

jest.spyOn(messenger, 'call').mockImplementation((...args) => {
// Creates the correct typed call params for mocks
type CallParams = {
[K in AllowedActions['type']]: [
K,
...Parameters<Extract<AllowedActions, { type: K }>['handler']>,
];
}[AllowedActions['type']];

const [actionType, params] = args as unknown as CallParams;

if (actionType === 'SnapController:handleRequest') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,41 @@ import type { NotNamespacedBy } from '@metamask/base-controller';
import { ControllerMessenger } from '@metamask/base-controller';
import log from 'loglevel';

import type { AllowedActions, AllowedEvents } from '..';
import type {
AllowedActions,
AllowedEvents,
UserStorageControllerMessenger,
} 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-mutations';
import {
createMockNetworkConfiguration,
createMockRemoteNetworkConfiguration,
} from './__fixtures__/mockNetwork';
import {
performMainNetworkSync,
startNetworkSyncing,
} from './controller-integration';
import * as ServicesModule from './services';
import * as SyncAllModule from './sync-all';
import * as SyncMutationsModule from './sync-mutations';

type GetActionHandler<Type extends AllowedActions['type']> = Extract<
AllowedActions,
{ type: Type }
>['handler'];

// Creates the correct typed call params for mocks
type CallParams = {
[K in AllowedActions['type']]: [K, ...Parameters<GetActionHandler<K>>];
}[AllowedActions['type']];

const typedMockCallFn = <
Type extends AllowedActions['type'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Func extends (...args: any[]) => any = GetActionHandler<Type>,
>() => jest.fn<ReturnType<Func>, Parameters<Func>>();

jest.mock('loglevel', () => {
const actual = jest.requireActual('loglevel');
Expand Down Expand Up @@ -44,12 +72,12 @@ const testMatrix = [
{
event: 'NetworkController:networkRemoved' as const,
arrangeSyncFnMock: () =>
jest.spyOn(SyncModule, 'deleteNetwork').mockResolvedValue(),
jest.spyOn(SyncMutationsModule, 'deleteNetwork').mockResolvedValue(),
},
];

describe.each(testMatrix)(
'network-syncing/controller-integration - $event',
'network-syncing/controller-integration - startNetworkSyncing() $event',
({ event, arrangeSyncFnMock }) => {
it(`should successfully sync when ${event} is emitted`, async () => {
const syncFnMock = arrangeSyncFnMock();
Expand Down Expand Up @@ -86,24 +114,240 @@ describe.each(testMatrix)(
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 - 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);
describe('network-syncing/controller-integration - performMainSync()', () => {
it('should do nothing if unable to get storage config', async () => {
const { getStorageConfig, messenger, mockCalls } = arrangeMocks();
getStorageConfig.mockResolvedValue(null);

return {
getStorageConfig: getStorageConfigMock,
baseMessenger,
messenger,
};
}
await performMainNetworkSync({ messenger, getStorageConfig });
expect(getStorageConfig).toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerGetState).not.toHaveBeenCalled();
});

it('should do nothing if unable to calculate networks to update', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue(undefined);

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerAddNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerUpdateNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
});

it('should update remote networks if there are local networks to add', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue({
remoteNetworksToUpdate: [createMockRemoteNetworkConfiguration()],
missingLocalNetworks: [],
localNetworksToUpdate: [],
localNetworksToRemove: [],
});

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerAddNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerUpdateNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
});

it('should add missing local networks', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue({
remoteNetworksToUpdate: [],
missingLocalNetworks: [createMockNetworkConfiguration()],
localNetworksToUpdate: [],
localNetworksToRemove: [],
});

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerAddNetwork).toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerUpdateNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
});

it('should update local networks', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue({
remoteNetworksToUpdate: [],
missingLocalNetworks: [],
localNetworksToUpdate: [createMockNetworkConfiguration()],
localNetworksToRemove: [],
});

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerAddNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerUpdateNetwork).toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
});

it('should remove local networks', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue({
remoteNetworksToUpdate: [],
missingLocalNetworks: [],
localNetworksToUpdate: [],
localNetworksToRemove: [createMockNetworkConfiguration()],
});

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerAddNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerUpdateNetwork).not.toHaveBeenCalled();
expect(mockCalls.mockNetworkControllerRemoveNetwork).toHaveBeenCalled();
});

it('should handle multiple networks to update', async () => {
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
arrangeMocks();
mockSync.findNetworksToUpdate.mockReturnValue({
remoteNetworksToUpdate: [
createMockRemoteNetworkConfiguration(),
createMockRemoteNetworkConfiguration(),
],
missingLocalNetworks: [
createMockNetworkConfiguration(),
createMockNetworkConfiguration(),
],
localNetworksToUpdate: [
createMockNetworkConfiguration(),
createMockNetworkConfiguration(),
],
localNetworksToRemove: [
createMockNetworkConfiguration(),
createMockNetworkConfiguration(),
],
});

await performMainNetworkSync({ messenger, getStorageConfig });
expect(mockServices.mockBatchUpdateNetworks).toHaveBeenCalledTimes(1); // this is a batch endpoint
expect(mockCalls.mockNetworkControllerAddNetwork).toHaveBeenCalledTimes(2);
expect(mockCalls.mockNetworkControllerUpdateNetwork).toHaveBeenCalledTimes(
2,
);
expect(mockCalls.mockNetworkControllerRemoveNetwork).toHaveBeenCalledTimes(
2,
);
});

/**
* Jest Mock Utility - create suite of mocks for tests
* @returns mocks for tests
*/
function arrangeMocks() {
const baseMessenger = mockBaseMessenger();
const messenger = mockUserStorageMessenger(baseMessenger);
const getStorageConfigMock = jest
.fn<Promise<UserStorageBaseOptions | null>, []>()
.mockResolvedValue(storageOpts);

const mockCalls = mockMessengerCalls(messenger);

return {
baseMessenger,
messenger,
getStorageConfig: getStorageConfigMock,
mockCalls,
mockServices: {
mockGetAllRemoveNetworks: jest
.spyOn(ServicesModule, 'getAllRemoteNetworks')
.mockResolvedValue([]),
mockBatchUpdateNetworks: jest
.spyOn(ServicesModule, 'batchUpsertRemoteNetworks')
.mockResolvedValue(),
},
mockSync: {
findNetworksToUpdate: jest
.spyOn(SyncAllModule, 'findNetworksToUpdate')
.mockReturnValue(undefined),
},
};
}

/**
* Jest Mock Utility - create a mock User Storage Messenger
* @param messenger - The messenger to mock
* @returns messenger call mocks
*/
function mockMessengerCalls(messenger: UserStorageControllerMessenger) {
const mockNetworkControllerGetState =
typedMockCallFn<'NetworkController:getState'>().mockReturnValue({
selectedNetworkClientId: '',
networksMetadata: {},
networkConfigurationsByChainId: {},
});

const mockNetworkControllerAddNetwork =
typedMockCallFn<'NetworkController:addNetwork'>();

const mockNetworkControllerUpdateNetwork =
typedMockCallFn<'NetworkController:updateNetwork'>();

const mockNetworkControllerRemoveNetwork =
typedMockCallFn<'NetworkController:removeNetwork'>();

jest.spyOn(messenger, 'call').mockImplementation((...args) => {
const typedArgs = args as unknown as CallParams;
const [actionType] = typedArgs;

if (actionType === 'NetworkController:getState') {
return mockNetworkControllerGetState();
}

if (actionType === 'NetworkController:addNetwork') {
const [, ...params] = typedArgs;
return mockNetworkControllerAddNetwork(...params);
}

if (actionType === 'NetworkController:updateNetwork') {
const [, ...params] = typedArgs;
return mockNetworkControllerUpdateNetwork(...params);
}

if (actionType === 'NetworkController:removeNetwork') {
const [, ...params] = typedArgs;
return mockNetworkControllerRemoveNetwork(...params);
}

throw new Error(
`MOCK_FAIL - unsupported messenger call: ${actionType as string}`,
);
});

return {
mockNetworkControllerGetState,
mockNetworkControllerAddNetwork,
mockNetworkControllerUpdateNetwork,
mockNetworkControllerRemoveNetwork,
};
}
});

/**
* Test Utility - creates a base messenger so we can invoke/publish events
Expand Down Expand Up @@ -132,7 +376,20 @@ function mockUserStorageMessenger(

const messenger = baseMessenger.getRestricted({
name: 'UserStorageController',
allowedActions: [],
allowedActions: [
'KeyringController:getState',
'SnapController:handleRequest',
'AuthenticationController:getBearerToken',
'AuthenticationController:getSessionProfile',
'AuthenticationController:isSignedIn',
'AuthenticationController:performSignIn',
'AuthenticationController:performSignOut',
'NotificationServicesController:disableNotificationServices',
'NotificationServicesController:selectIsNotificationServicesEnabled',
'AccountsController:listAccounts',
'AccountsController:updateAccountMetadata',
'KeyringController:addNewAccount',
],
allowedEvents,
});

Expand Down
Loading

0 comments on commit 94a67ba

Please sign in to comment.