diff --git a/app/scripts/migrations/120.5.test.ts b/app/scripts/migrations/120.5.test.ts new file mode 100644 index 000000000000..479565bd27d4 --- /dev/null +++ b/app/scripts/migrations/120.5.test.ts @@ -0,0 +1,247 @@ +import { cloneDeep } from 'lodash'; +import { migrate, version } from './120.5'; + +const oldVersion = 120.4; + +describe('migration #120.5', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('does nothing if SelectedNetworkController state is not set', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('deletes the SelectedNetworkController state if it is corrupted', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + SelectedNetworkController: 'invalid', + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + }); + }); + + it('deletes the SelectedNetworkController state if it is missing the domains state', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + SelectedNetworkController: { + somethingElse: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + }); + }); + + it('deletes the SelectedNetworkController state if the domains state is corrupted', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + SelectedNetworkController: { + domains: 'invalid', + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + }); + }); + + it('deletes the SelectedNetworkController state if NetworkController state is missing', async () => { + const oldState = { + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({}); + }); + + it('deletes the SelectedNetworkController state if NetworkController state is corrupted', async () => { + const oldState = { + NetworkController: 'invalid', + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: 'invalid', + }); + }); + + it('deletes the SelectedNetworkController state if NetworkController has no networkConfigurations', async () => { + const oldState = { + NetworkController: {}, + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: {}, + }); + }); + + it('deletes the SelectedNetworkController state if NetworkController networkConfigurations state is corrupted', async () => { + const oldState = { + NetworkController: { networkConfigurations: 'invalid' }, + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { networkConfigurations: 'invalid' }, + }); + }); + + it('does nothing if SelectedNetworkController domains state is empty', async () => { + const oldState = { + NetworkController: { networkConfigurations: {} }, + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('does nothing if SelectedNetworkController domains state is valid', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + SelectedNetworkController: { + domains: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('deletes the SelectedNetworkController state if an invalid networkConfigurationId is found', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + SelectedNetworkController: { + domains: { + 'domain.test': '456', + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { + networkConfigurations: { + 123: {}, + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/120.5.ts b/app/scripts/migrations/120.5.ts new file mode 100644 index 000000000000..7159ce2447db --- /dev/null +++ b/app/scripts/migrations/120.5.ts @@ -0,0 +1,106 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 120.5; + +/** + * This migration removes invalid network configuration IDs from the SelectedNetworkController. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +/** + * Remove invalid network configuration IDs from the SelectedNetworkController. + * + * @param state - The persisted MetaMask state, keyed by controller. + */ +function transformState(state: Record): void { + if (!hasProperty(state, 'SelectedNetworkController')) { + return; + } + if (!isObject(state.SelectedNetworkController)) { + console.error( + `Migration ${version}: Invalid SelectedNetworkController state of type '${typeof state.SelectedNetworkController}'`, + ); + delete state.SelectedNetworkController; + return; + } else if (!hasProperty(state.SelectedNetworkController, 'domains')) { + console.error( + `Migration ${version}: Missing SelectedNetworkController domains state`, + ); + delete state.SelectedNetworkController; + return; + } else if (!isObject(state.SelectedNetworkController.domains)) { + console.error( + `Migration ${version}: Invalid SelectedNetworkController domains state of type '${typeof state + .SelectedNetworkController.domains}'`, + ); + delete state.SelectedNetworkController; + return; + } + + if (!hasProperty(state, 'NetworkController')) { + delete state.SelectedNetworkController; + return; + } else if (!isObject(state.NetworkController)) { + console.error( + new Error( + `Migration ${version}: Invalid NetworkController state of type '${typeof state.NetworkController}'`, + ), + ); + delete state.SelectedNetworkController; + return; + } else if (!hasProperty(state.NetworkController, 'networkConfigurations')) { + delete state.SelectedNetworkController; + return; + } else if (!isObject(state.NetworkController.networkConfigurations)) { + console.error( + new Error( + `Migration ${version}: Invalid NetworkController networkConfigurations state of type '${typeof state.NetworkController}'`, + ), + ); + delete state.SelectedNetworkController; + return; + } + + const validNetworkConfigurationIds = Object.keys( + state.NetworkController.networkConfigurations, + ); + const domainMappedNetworkConfigurationIds = Object.values( + state.SelectedNetworkController.domains, + ); + + for (const configurationId of domainMappedNetworkConfigurationIds) { + if ( + typeof configurationId !== 'string' || + !validNetworkConfigurationIds.includes(configurationId) + ) { + console.error( + new Error( + `Migration ${version}: Invalid networkConfigurationId found in SelectedNetworkController state: '${configurationId}'`, + ), + ); + delete state.SelectedNetworkController; + return; + } + } +}