diff --git a/app/scripts/migrations/082.test.js b/app/scripts/migrations/082.test.js index 6f221c5c6..0060c853c 100644 --- a/app/scripts/migrations/082.test.js +++ b/app/scripts/migrations/082.test.js @@ -1,6 +1,12 @@ import { v4 } from 'uuid'; import { migrate, version } from './082'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -472,10 +478,72 @@ describe('migration #82', () => { }, }; const newStorage = await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalled(); expect(newStorage.data).toStrictEqual(oldStorage.data); }); - it('should not change anything if there is no frequentRpcListDetail property on PreferencesController', async () => { + it('should capture an exception if any PreferencesController.frequentRpcListDetail entries are not objects', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + frequentRpcListDetail: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + 'invalid entry type', + 1, + ], + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + }, + }; + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error( + `state.PreferencesController.frequentRpcListDetail contains an element of type string`, + ), + ); + }); + + it('should not change anything, and not capture an exception, if there is no frequentRpcListDetail property on PreferencesController but there is a networkConfigurations object', async () => { const oldStorage = { meta: { version: 81, @@ -556,9 +624,60 @@ describe('migration #82', () => { }, }; const newStorage = await migrate(oldStorage); + expect(sentryCaptureExceptionMock).not.toHaveBeenCalled(); expect(newStorage.data).toStrictEqual(oldStorage.data); }); + it('should capture an exception if there is no frequentRpcListDetail property on PreferencesController and no networkConfiguration object', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + }, + }; + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error( + `typeof state.PreferencesController.frequentRpcListDetail is undefined`, + ), + ); + }); + it('should change nothing if PreferencesController is undefined', async () => { const oldStorage = { meta: { @@ -595,4 +714,61 @@ describe('migration #82', () => { const newStorage = await migrate(oldStorage); expect(newStorage.data).toStrictEqual(oldStorage.data); }); + + it('should capture an exception if PreferencesController is not an object', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + PreferencesController: false, + }, + }; + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.PreferencesController is boolean`), + ); + }); + + it('should capture an exception if NetworkController is undefined', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: {}, + }, + }; + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); }); diff --git a/app/scripts/migrations/082.ts b/app/scripts/migrations/082.ts index fdda1fb8d..29ff59eea 100644 --- a/app/scripts/migrations/082.ts +++ b/app/scripts/migrations/082.ts @@ -1,6 +1,7 @@ import { cloneDeep } from 'lodash'; import { hasProperty, isObject } from '@metamask/utils'; import { v4 } from 'uuid'; +import log from 'loglevel'; export const version = 82; @@ -25,14 +26,56 @@ export async function migrate(originalVersionedData: { } function transformState(state: Record) { + if (!hasProperty(state, 'PreferencesController')) { + log.warn(`state.PreferencesController is undefined`); + return state; + } + if (!isObject(state.PreferencesController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.PreferencesController is ${typeof state.PreferencesController}`, + ), + ); + return state; + } if ( - !hasProperty(state, 'PreferencesController') || - !isObject(state.PreferencesController) || - !isObject(state.NetworkController) || - !hasProperty(state.PreferencesController, 'frequentRpcListDetail') || - !Array.isArray(state.PreferencesController.frequentRpcListDetail) || - !state.PreferencesController.frequentRpcListDetail.every(isObject) + !hasProperty(state, 'NetworkController') || + !isObject(state.NetworkController) ) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + return state; + } + if ( + !hasProperty(state.PreferencesController, 'frequentRpcListDetail') || + !Array.isArray(state.PreferencesController.frequentRpcListDetail) + ) { + const inPost077SupplementFor082State = + state.NetworkController.networkConfigurations && + state.PreferencesController.frequentRpcListDetail === undefined; + if (!inPost077SupplementFor082State) { + global.sentry?.captureException?.( + new Error( + `typeof state.PreferencesController.frequentRpcListDetail is ${typeof state + .PreferencesController.frequentRpcListDetail}`, + ), + ); + } + return state; + } + if (!state.PreferencesController.frequentRpcListDetail.every(isObject)) { + const erroneousElement = + state.PreferencesController.frequentRpcListDetail.find( + (element) => !isObject(element), + ); + global.sentry?.captureException?.( + new Error( + `state.PreferencesController.frequentRpcListDetail contains an element of type ${typeof erroneousElement}`, + ), + ); return state; } const { PreferencesController, NetworkController } = state; diff --git a/app/scripts/migrations/083.test.js b/app/scripts/migrations/083.test.js index c59951aef..b8f91dfcd 100644 --- a/app/scripts/migrations/083.test.js +++ b/app/scripts/migrations/083.test.js @@ -1,6 +1,12 @@ import { v4 } from 'uuid'; import { migrate, version } from './083'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -165,6 +171,24 @@ describe('migration #83', () => { expect(newStorage).toStrictEqual(expectedNewStorage); }); + it('should capture an exception if state.NetworkController is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + testProperty: 'testValue', + }, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should not modify state if state.NetworkController is not an object', async () => { const oldStorage = { meta: { @@ -190,6 +214,25 @@ describe('migration #83', () => { expect(newStorage).toStrictEqual(expectedNewStorage); }); + it('should capture an exception if state.NetworkController is not an object', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: false, + testProperty: 'testValue', + }, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is boolean`), + ); + }); + it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => { const oldStorage = { meta: { @@ -221,6 +264,28 @@ describe('migration #83', () => { expect(newStorage).toStrictEqual(expectedNewStorage); }); + it('should capture an exception if state.NetworkController.networkConfigurations is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: undefined, + }, + testProperty: 'testValue', + }, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof NetworkController.networkConfigurations is undefined`), + ); + }); + it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => { const oldStorage = { meta: { diff --git a/app/scripts/migrations/083.ts b/app/scripts/migrations/083.ts index cc3e3b16b..bd7ba62e4 100644 --- a/app/scripts/migrations/083.ts +++ b/app/scripts/migrations/083.ts @@ -25,11 +25,21 @@ export async function migrate(originalVersionedData: { function transformState(state: Record) { if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); return state; } const { NetworkController } = state; if (!isObject(NetworkController.networkConfigurations)) { + global.sentry?.captureException?.( + new Error( + `typeof NetworkController.networkConfigurations is ${typeof NetworkController.networkConfigurations}`, + ), + ); return state; } diff --git a/app/scripts/migrations/084.test.js b/app/scripts/migrations/084.test.js index 138bfacb6..bd0e1b35c 100644 --- a/app/scripts/migrations/084.test.js +++ b/app/scripts/migrations/084.test.js @@ -1,6 +1,16 @@ import { migrate } from './084'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + describe('migration #84', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('updates the version metadata', async () => { const originalVersionedData = buildOriginalVersionedData({ meta: { @@ -27,6 +37,21 @@ describe('migration #84', () => { expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); }); + it('captures an exception if the network controller state does not exist', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + test: '123', + }, + }); + + await migrate(originalVersionedData); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + const nonObjects = [undefined, null, 'test', 1, ['test']]; for (const invalidState of nonObjects) { it(`does not change the state if the network controller state is ${invalidState}`, async () => { @@ -40,6 +65,21 @@ describe('migration #84', () => { expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); }); + + it(`captures an exception if the network controller state is ${invalidState}`, async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: invalidState, + }, + }); + + await migrate(originalVersionedData); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is ${typeof invalidState}`), + ); + }); } it('does not change the state if the network controller state does not include "network"', async () => { @@ -56,6 +96,38 @@ describe('migration #84', () => { expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); }); + it('captures an exception if the network controller state does not include "network" and does not include "networkId"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + test: '123', + }, + }, + }); + + await migrate(originalVersionedData); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController.network is undefined`), + ); + }); + + it('does not capture an exception if the network controller state does not include "network" but does include "networkId"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + test: '123', + networkId: 'foobar', + }, + }, + }); + + await migrate(originalVersionedData); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0); + }); + it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => { const originalVersionedData = buildOriginalVersionedData({ data: { diff --git a/app/scripts/migrations/084.ts b/app/scripts/migrations/084.ts index 66a2f45ae..7551ed2c6 100644 --- a/app/scripts/migrations/084.ts +++ b/app/scripts/migrations/084.ts @@ -25,9 +25,26 @@ export async function migrate(originalVersionedData: { function transformState(state: Record) { if ( !hasProperty(state, 'NetworkController') || - !isObject(state.NetworkController) || - !hasProperty(state.NetworkController, 'network') + !isObject(state.NetworkController) ) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + return state; + } + if (!hasProperty(state.NetworkController, 'network')) { + const thePost077SupplementFor084HasNotModifiedState = + state.NetworkController.networkId === undefined; + if (thePost077SupplementFor084HasNotModifiedState) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.network is ${typeof state + .NetworkController.network}`, + ), + ); + } return state; } diff --git a/app/scripts/migrations/085.test.js b/app/scripts/migrations/085.test.js index 6b7b4967d..cfe7ff1b0 100644 --- a/app/scripts/migrations/085.test.js +++ b/app/scripts/migrations/085.test.js @@ -1,5 +1,11 @@ import { migrate, version } from './085'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -10,6 +16,10 @@ jest.mock('uuid', () => { }); describe('migration #85', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -39,6 +49,25 @@ describe('migration #85', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller previous provider state', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/085.ts b/app/scripts/migrations/085.ts index 03499d2b2..2ba22a346 100644 --- a/app/scripts/migrations/085.ts +++ b/app/scripts/migrations/085.ts @@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: { function transformState(state: Record) { if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); return state; } diff --git a/app/scripts/migrations/086.test.js b/app/scripts/migrations/086.test.js index f38f0444d..cd6a61377 100644 --- a/app/scripts/migrations/086.test.js +++ b/app/scripts/migrations/086.test.js @@ -1,5 +1,11 @@ import { migrate, version } from './086'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -10,6 +16,10 @@ jest.mock('uuid', () => { }); describe('migration #86', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -39,6 +49,25 @@ describe('migration #86', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 85, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller provider state', async () => { const oldData = { other: 'data', @@ -59,6 +88,52 @@ describe('migration #86', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller provider state and no providerConfig state', async () => { + const oldData = { + other: 'data', + NetworkController: { + networkConfigurations: { + foo: 'bar', + }, + }, + }; + const oldStorage = { + meta: { + version: 85, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController.provider is undefined`), + ); + }); + + it('should not capture an exception if there is no network controller provider state but there is a providerConfig state', async () => { + const oldData = { + other: 'data', + NetworkController: { + networkConfigurations: { + foo: 'bar', + }, + providerConfig: {}, + }, + }; + const oldStorage = { + meta: { + version: 85, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0); + }); + it('should rename the provider config state', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/086.ts b/app/scripts/migrations/086.ts index c4b60e270..709934f50 100644 --- a/app/scripts/migrations/086.ts +++ b/app/scripts/migrations/086.ts @@ -37,5 +37,24 @@ function transformState(state: Record) { NetworkController: networkControllerState, }; } + if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + } else if (!hasProperty(state.NetworkController, 'provider')) { + const thePost077SupplementFor086HasNotModifiedState = + state.NetworkController.providerConfig === undefined; + if (thePost077SupplementFor086HasNotModifiedState) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.provider is ${typeof state + .NetworkController.provider}`, + ), + ); + } + } + return state; } diff --git a/app/scripts/migrations/087.test.js b/app/scripts/migrations/087.test.js index 1d631e926..ebd9495ad 100644 --- a/app/scripts/migrations/087.test.js +++ b/app/scripts/migrations/087.test.js @@ -1,6 +1,16 @@ import { migrate, version } from './087'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + describe('migration #87', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -53,6 +63,65 @@ describe('migration #87', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should return state unaltered if TokensController state is not an object', async () => { + const oldData = { + other: 'data', + TokensController: false, + }; + const oldStorage = { + meta: { + version: 86, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('should capture an exception if TokensController state is not an object', async () => { + const oldData = { + other: 'data', + TokensController: false, + }; + const oldStorage = { + meta: { + version: 86, + }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController is boolean`), + ); + }); + + it('should not capture an exception if TokensController state is an object', async () => { + const oldData = { + other: 'data', + TokensController: { + allDetectedTokens: {}, + allIgnoredTokens: {}, + allTokens: {}, + detectedTokens: [], + ignoredTokens: [], + suggestedAssets: [], + tokens: [], + }, + }; + const oldStorage = { + meta: { + version: 86, + }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0); + }); + it('should remove the suggested assets state', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/087.ts b/app/scripts/migrations/087.ts index 92093f967..f900faab6 100644 --- a/app/scripts/migrations/087.ts +++ b/app/scripts/migrations/087.ts @@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: { function transformState(state: Record) { if (!isObject(state.TokensController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.TokensController is ${typeof state.TokensController}`, + ), + ); return state; } diff --git a/app/scripts/migrations/088.test.ts b/app/scripts/migrations/088.test.ts index a33c30eff..ca672f982 100644 --- a/app/scripts/migrations/088.test.ts +++ b/app/scripts/migrations/088.test.ts @@ -1,6 +1,16 @@ import { migrate } from './088'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + describe('migration #88', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('updates the version metadata', async () => { const oldStorage = { meta: { version: 87 }, @@ -26,6 +36,24 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the NftController property is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: {}, + NftController: false, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NftController is boolean`), + ); + }); + it('returns the state unaltered if the NftController object has no allNftContracts property', async () => { const oldData = { NftController: { @@ -58,6 +86,26 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if it NftController.allNftContracts is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: {}, + NftController: { + allNftContracts: 'foo', + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NftController.allNftContracts is string`), + ); + }); + it('returns the state unaltered if any value of the NftController.allNftContracts object is not an object itself', async () => { const oldData = { NftController: { @@ -324,6 +372,26 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if it NftController.allNfts is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: {}, + NftController: { + allNfts: 'foo', + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NftController.allNfts is string`), + ); + }); + it('returns the state unaltered if any value of the NftController.allNfts object is not an object itself', async () => { const oldData = { NftController: { @@ -656,6 +724,91 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if it has no TokenListController property', async () => { + const oldData = { + TokensController: {}, + NftController: { + allNfts: { + '0x111': { + '0x10': [ + { + name: 'NFT 1', + description: 'Description for NFT 1', + image: 'nft1.jpg', + standard: 'ERC721', + tokenId: '1', + address: '0xaaa', + }, + ], + }, + }, + allNftContracts: { + '0x111': { + '0x10': [ + { + name: 'Contract 1', + address: '0xaaa', + }, + ], + }, + }, + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokenListController is undefined`), + ); + }); + + it('captures an exception if the TokenListController property is not an object', async () => { + const oldData = { + TokensController: {}, + NftController: { + allNfts: { + '0x111': { + '0x10': [ + { + name: 'NFT 1', + description: 'Description for NFT 1', + image: 'nft1.jpg', + standard: 'ERC721', + tokenId: '1', + address: '0xaaa', + }, + ], + }, + }, + allNftContracts: { + '0x111': { + '0x10': [ + { + name: 'Contract 1', + address: '0xaaa', + }, + ], + }, + }, + }, + TokenListController: false, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokenListController is boolean`), + ); + }); + it('returns the state unaltered if the TokenListController object has no tokensChainsCache property', async () => { const oldData = { TokenListController: { @@ -688,6 +841,25 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the TokenListController.tokensChainsCache property is not an object', async () => { + const oldData = { + TokenListController: { + tokensChainsCache: 'foo', + }, + TokensController: {}, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokenListController.tokensChainsCache is string`), + ); + }); + it('rewrites TokenListController.tokensChainsCache so that decimal chain IDs are converted to hex strings', async () => { const oldStorage = { meta: { version: 87 }, @@ -919,6 +1091,39 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if it has no TokensController property', async () => { + const oldData = { + TokenListController: {}, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController is undefined`), + ); + }); + + it('captures an exception if the TokensController property is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: false, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController is boolean`), + ); + }); + it('returns the state unaltered if the TokensController object has no allTokens property', async () => { const oldData = { TokensController: { @@ -951,6 +1156,25 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the TokensController.allTokens property is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: { + allTokens: 'foo', + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController.allTokens is string`), + ); + }); + it('rewrites TokensController.allTokens so that decimal chain IDs are converted to hex strings', async () => { const oldStorage = { meta: { version: 87 }, @@ -1163,6 +1387,25 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the TokensController.allIgnoredTokens property is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: { + allIgnoredTokens: 'foo', + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController.allIgnoredTokens is string`), + ); + }); + it('rewrites TokensController.allIgnoredTokens so that decimal chain IDs are converted to hex strings', async () => { const oldStorage = { meta: { version: 87 }, @@ -1323,6 +1566,25 @@ describe('migration #88', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the TokensController.allDetectedTokens property is not an object', async () => { + const oldData = { + TokenListController: {}, + TokensController: { + allDetectedTokens: 'foo', + }, + }; + const oldStorage = { + meta: { version: 87 }, + data: oldData, + }; + + await migrate(oldStorage); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.TokensController.allDetectedTokens is string`), + ); + }); + it('rewrites TokensController.allDetectedTokens so that decimal chain IDs are converted to hex strings', async () => { const oldStorage = { meta: { version: 87 }, diff --git a/app/scripts/migrations/088.ts b/app/scripts/migrations/088.ts index a4b874b2f..5ede1b0fa 100644 --- a/app/scripts/migrations/088.ts +++ b/app/scripts/migrations/088.ts @@ -1,6 +1,7 @@ import { hasProperty, Hex, isObject, isStrictHexString } from '@metamask/utils'; import { BN } from 'ethereumjs-util'; import { cloneDeep, mapKeys } from 'lodash'; +import log from 'loglevel'; type VersionedData = { meta: { version: number }; @@ -70,6 +71,16 @@ function migrateData(state: Record): void { } }); } + } else if (hasProperty(nftControllerState, 'allNftContracts')) { + global.sentry?.captureException?.( + new Error( + `typeof state.NftController.allNftContracts is ${typeof nftControllerState.allNftContracts}`, + ), + ); + } else { + log.warn( + `typeof state.NftController.allNftContracts is ${typeof nftControllerState.allNftContracts}`, + ); } // Migrate NftController.allNfts @@ -96,9 +107,25 @@ function migrateData(state: Record): void { } }); } + } else if (hasProperty(nftControllerState, 'allNfts')) { + global.sentry?.captureException?.( + new Error( + `typeof state.NftController.allNfts is ${typeof nftControllerState.allNfts}`, + ), + ); + } else { + log.warn( + `typeof state.NftController.allNfts is ${typeof nftControllerState.allNfts}`, + ); } state.NftController = nftControllerState; + } else if (hasProperty(state, 'NftController')) { + global.sentry?.captureException?.( + new Error(`typeof state.NftController is ${typeof state.NftController}`), + ); + } else { + log.warn(`typeof state.NftController is undefined`); } if ( @@ -124,7 +151,24 @@ function migrateData(state: Record): void { tokenListControllerState.tokensChainsCache, (_, chainId: string) => toHex(chainId), ); + } else if (hasProperty(tokenListControllerState, 'tokensChainsCache')) { + global.sentry?.captureException?.( + new Error( + `typeof state.TokenListController.tokensChainsCache is ${typeof state + .TokenListController.tokensChainsCache}`, + ), + ); + } else { + log.warn( + `typeof state.TokenListController.tokensChainsCache is undefined`, + ); } + } else { + global.sentry?.captureException?.( + new Error( + `typeof state.TokenListController is ${typeof state.TokenListController}`, + ), + ); } if ( @@ -150,6 +194,16 @@ function migrateData(state: Record): void { allTokens, (_, chainId: string) => toHex(chainId), ); + } else if (hasProperty(tokensControllerState, 'allTokens')) { + global.sentry?.captureException?.( + new Error( + `typeof state.TokensController.allTokens is ${typeof tokensControllerState.allTokens}`, + ), + ); + } else { + log.warn( + `typeof state.TokensController.allTokens is ${typeof tokensControllerState.allTokens}`, + ); } // Migrate TokensController.allIgnoredTokens @@ -169,6 +223,16 @@ function migrateData(state: Record): void { allIgnoredTokens, (_, chainId: string) => toHex(chainId), ); + } else if (hasProperty(tokensControllerState, 'allIgnoredTokens')) { + global.sentry?.captureException?.( + new Error( + `typeof state.TokensController.allIgnoredTokens is ${typeof tokensControllerState.allIgnoredTokens}`, + ), + ); + } else { + log.warn( + `typeof state.TokensController.allIgnoredTokens is ${typeof tokensControllerState.allIgnoredTokens}`, + ); } // Migrate TokensController.allDetectedTokens @@ -188,9 +252,25 @@ function migrateData(state: Record): void { allDetectedTokens, (_, chainId: string) => toHex(chainId), ); + } else if (hasProperty(tokensControllerState, 'allDetectedTokens')) { + global.sentry?.captureException?.( + new Error( + `typeof state.TokensController.allDetectedTokens is ${typeof tokensControllerState.allDetectedTokens}`, + ), + ); + } else { + log.warn( + `typeof state.TokensController.allDetectedTokens is ${typeof tokensControllerState.allDetectedTokens}`, + ); } state.TokensController = tokensControllerState; + } else { + global.sentry?.captureException?.( + new Error( + `typeof state.TokensController is ${typeof state.TokensController}`, + ), + ); } } diff --git a/app/scripts/migrations/089.test.ts b/app/scripts/migrations/089.test.ts index 00868ff74..91511d315 100644 --- a/app/scripts/migrations/089.test.ts +++ b/app/scripts/migrations/089.test.ts @@ -1,5 +1,14 @@ import { migrate, version } from './089'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + startSession: jest.fn(), + endSession: jest.fn(), + toggleSession: jest.fn(), + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -10,6 +19,10 @@ jest.mock('uuid', () => { }); describe('migration #89', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -39,6 +52,25 @@ describe('migration #89', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 88, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller providerConfig state', async () => { const oldData = { other: 'data', @@ -61,6 +93,32 @@ describe('migration #89', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller providerConfig state', async () => { + const oldData = { + other: 'data', + NetworkController: { + networkConfigurations: { + id1: { + foo: 'bar', + }, + }, + }, + }; + const oldStorage = { + meta: { + version: 88, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController.providerConfig is undefined`), + ); + }); + it('should return state unaltered if the providerConfig already has an id', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/089.ts b/app/scripts/migrations/089.ts index cc1bfa4dc..d4faebc22 100644 --- a/app/scripts/migrations/089.ts +++ b/app/scripts/migrations/089.ts @@ -66,6 +66,19 @@ function transformState(state: Record) { ...state, NetworkController: state.NetworkController, }; + } else if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + } else if (!isObject(state.NetworkController.providerConfig)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.providerConfig is ${typeof state + .NetworkController.providerConfig}`, + ), + ); } return state; } diff --git a/app/scripts/migrations/090.test.js b/app/scripts/migrations/090.test.js index 6a28c60f2..13e39e648 100644 --- a/app/scripts/migrations/090.test.js +++ b/app/scripts/migrations/090.test.js @@ -2,7 +2,17 @@ import { migrate, version } from './090'; const PREVIOUS_VERSION = version - 1; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + describe('migration #90', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('updates the version metadata', async () => { const oldStorage = { meta: { @@ -31,7 +41,7 @@ describe('migration #90', () => { expect(newStorage.data).toStrictEqual(oldStorage.data); }); - it('does not change the state if the phishing controller state is invalid', async () => { + it('captures an exception if the phishing controller state is invalid', async () => { const oldStorage = { meta: { version: PREVIOUS_VERSION, @@ -39,9 +49,12 @@ describe('migration #90', () => { data: { PhishingController: 'this is not valid' }, }; - const newStorage = await migrate(oldStorage); + await migrate(oldStorage); - expect(newStorage.data).toStrictEqual(oldStorage.data); + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.PhishingController is string`), + ); }); it('does not change the state if the listState property does not exist', async () => { diff --git a/app/scripts/migrations/090.ts b/app/scripts/migrations/090.ts index e45ec05e4..a2d50cab6 100644 --- a/app/scripts/migrations/090.ts +++ b/app/scripts/migrations/090.ts @@ -1,5 +1,6 @@ import { cloneDeep } from 'lodash'; import { hasProperty, isObject } from '@metamask/utils'; +import log from 'loglevel'; export const version = 90; @@ -23,11 +24,22 @@ export async function migrate(originalVersionedData: { } function transformState(state: Record) { - if ( - !hasProperty(state, 'PhishingController') || - !isObject(state.PhishingController) || - !hasProperty(state.PhishingController, 'listState') - ) { + if (!hasProperty(state, 'PhishingController')) { + log.warn(`typeof state.PhishingController is undefined`); + return state; + } + if (!isObject(state.PhishingController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.PhishingController is ${typeof state.PhishingController}`, + ), + ); + return state; + } + if (!hasProperty(state.PhishingController, 'listState')) { + log.warn( + `typeof state.PhishingController.listState is ${typeof state.PhishingController}`, + ); return state; } diff --git a/app/scripts/migrations/091.test.ts b/app/scripts/migrations/091.test.ts index d4836f003..6a1f14ed7 100644 --- a/app/scripts/migrations/091.test.ts +++ b/app/scripts/migrations/091.test.ts @@ -1,6 +1,15 @@ import { cloneDeep } from 'lodash'; import { migrate, version } from './091'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + startSession: jest.fn(), + endSession: jest.fn(), + toggleSession: jest.fn(), + captureException: sentryCaptureExceptionMock, +}; + jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -11,6 +20,10 @@ jest.mock('uuid', () => { }); describe('migration #91', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -40,6 +53,25 @@ describe('migration #91', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 90, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller networkConfigurations state', async () => { const oldData = { other: 'data', @@ -60,6 +92,32 @@ describe('migration #91', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller networkConfigurations state', async () => { + const oldData = { + other: 'data', + NetworkController: { + providerConfig: { + foo: 'bar', + }, + }, + }; + const oldStorage = { + meta: { + version: 90, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error( + `typeof state.NetworkController.networkConfigurations is undefined`, + ), + ); + }); + it('should return state unaltered if the networkConfigurations all have a chainId', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/091.ts b/app/scripts/migrations/091.ts index c0661746a..874f6ed9f 100644 --- a/app/scripts/migrations/091.ts +++ b/app/scripts/migrations/091.ts @@ -50,6 +50,19 @@ function transformState(state: Record) { ...state, NetworkController: state.NetworkController, }; + } else if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + } else if (!isObject(state.NetworkController.networkConfigurations)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.networkConfigurations is ${typeof state + .NetworkController.networkConfigurations}`, + ), + ); } return state; } diff --git a/app/scripts/migrations/092.test.ts b/app/scripts/migrations/092.test.ts index b44c04602..b7337c871 100644 --- a/app/scripts/migrations/092.test.ts +++ b/app/scripts/migrations/092.test.ts @@ -3,7 +3,20 @@ import { migrate, version } from './092'; const PREVIOUS_VERSION = version - 1; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + startSession: jest.fn(), + endSession: jest.fn(), + toggleSession: jest.fn(), + captureException: sentryCaptureExceptionMock, +}; + describe('migration #92', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -33,6 +46,22 @@ describe('migration #92', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('captures an exception if the phishing controller state is invalid', async () => { + const oldStorage = { + meta: { + version: PREVIOUS_VERSION, + }, + data: { PhishingController: 'this is not valid' }, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.PhishingController is string`), + ); + }); + it('should return state unaltered if there is no phishing controller last fetched state', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/092.ts b/app/scripts/migrations/092.ts index bf5469614..44ef2a6c7 100644 --- a/app/scripts/migrations/092.ts +++ b/app/scripts/migrations/092.ts @@ -1,5 +1,6 @@ import { cloneDeep } from 'lodash'; import { hasProperty, isObject } from '@metamask/utils'; +import log from 'loglevel'; export const version = 92; @@ -30,6 +31,14 @@ function transformState(state: Record) { ) { delete state.PhishingController.stalelistLastFetched; delete state.PhishingController.hotlistLastFetched; + } else if (hasProperty(state, 'PhishingController')) { + global.sentry?.captureException?.( + new Error( + `typeof state.PhishingController is ${typeof state.PhishingController}`, + ), + ); + } else { + log.warn(`typeof state.PhishingController is undefined`); } return state; } diff --git a/app/scripts/migrations/093.test.ts b/app/scripts/migrations/093.test.ts index 0d19ddae6..eff417f01 100644 --- a/app/scripts/migrations/093.test.ts +++ b/app/scripts/migrations/093.test.ts @@ -3,7 +3,20 @@ import { migrate, version } from './093'; const PREVIOUS_VERSION = version - 1; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + startSession: jest.fn(), + endSession: jest.fn(), + toggleSession: jest.fn(), + captureException: sentryCaptureExceptionMock, +}; + describe('migration #93', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -33,6 +46,25 @@ describe('migration #93', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: PREVIOUS_VERSION, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller providerConfig state', async () => { const oldData = { other: 'data', @@ -55,6 +87,32 @@ describe('migration #93', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller providerConfig state', async () => { + const oldData = { + other: 'data', + NetworkController: { + networkConfigurations: { + id1: { + foo: 'bar', + }, + }, + }, + }; + const oldStorage = { + meta: { + version: PREVIOUS_VERSION, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController.providerConfig is undefined`), + ); + }); + it('should return state unaltered if there is already a ticker in the providerConfig state', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/093.ts b/app/scripts/migrations/093.ts index 8aee8f9e8..665c2f50d 100644 --- a/app/scripts/migrations/093.ts +++ b/app/scripts/migrations/093.ts @@ -44,6 +44,22 @@ function transformState(state: Record) { ...state, NetworkController: state.NetworkController, }; + } else if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + } else if ( + isObject(state.NetworkController) && + !isObject(state.NetworkController.providerConfig) + ) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.providerConfig is ${typeof state + .NetworkController.providerConfig}`, + ), + ); } return state; } diff --git a/app/scripts/migrations/094.test.ts b/app/scripts/migrations/094.test.ts index f37d46c81..955c206d2 100644 --- a/app/scripts/migrations/094.test.ts +++ b/app/scripts/migrations/094.test.ts @@ -2,7 +2,20 @@ import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; import { migrate, version } from './094'; +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + startSession: jest.fn(), + endSession: jest.fn(), + toggleSession: jest.fn(), + captureException: sentryCaptureExceptionMock, +}; + describe('migration #94', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { const oldStorage = { meta: { @@ -32,6 +45,25 @@ describe('migration #94', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController is undefined`), + ); + }); + it('should return state unaltered if there is no network controller providerConfig state', async () => { const oldData = { other: 'data', @@ -54,6 +86,137 @@ describe('migration #94', () => { expect(newStorage.data).toStrictEqual(oldData); }); + it('should capture an exception if there is no network controller providerConfig state', async () => { + const oldData = { + other: 'data', + NetworkController: { + networkConfigurations: { + id1: { + foo: 'bar', + }, + }, + }, + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(`typeof state.NetworkController.providerConfig is undefined`), + ); + }); + + it('should capture an exception if there is no providerConfig.id and no providerConfig.type value in state', async () => { + const oldData = { + other: 'data', + NetworkController: { + providerConfig: { + ticker: 'NET', + chainId: '0x189123', + nickname: 'A Network', + }, + }, + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error( + `typeof state.NetworkController.providerConfig.id is undefined and state.NetworkController.providerConfig.type is undefined`, + ), + ); + }); + + it('should not capture an exception if there is a providerConfig.id in state', async () => { + const oldData = { + other: 'data', + NetworkController: { + providerConfig: { + ticker: 'NET', + chainId: '0x189123', + nickname: 'A Network', + id: 'foobar', + }, + }, + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0); + }); + + it(`should capture an exception if there is no providerConfig.id and the providerConfig.type value is ${NetworkType.rpc} in state`, async () => { + const oldData = { + other: 'data', + NetworkController: { + providerConfig: { + ticker: 'NET', + chainId: '0x189123', + nickname: 'A Network', + type: NetworkType.rpc, + }, + }, + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error( + `typeof state.NetworkController.providerConfig.id is undefined and state.NetworkController.providerConfig.type is ${NetworkType.rpc}`, + ), + ); + }); + + it(`should not capture an exception if there is no providerConfig.id and the providerConfig.type value is not ${NetworkType.rpc} in state`, async () => { + const oldData = { + other: 'data', + NetworkController: { + providerConfig: { + ticker: 'NET', + chainId: '0x189123', + nickname: 'A Network', + type: 'NOT_AN_RPC_TYPE', + }, + }, + }; + const oldStorage = { + meta: { + version: 93, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0); + }); + it('should return state unaltered if there is a providerConfig.id value in state but it is not a string', async () => { const oldData = { other: 'data', diff --git a/app/scripts/migrations/094.ts b/app/scripts/migrations/094.ts index 78fbe9641..12734f7e6 100644 --- a/app/scripts/migrations/094.ts +++ b/app/scripts/migrations/094.ts @@ -84,6 +84,35 @@ function transformState(state: Record) { selectedNetworkClientId, }, }; + } else if (!isObject(state.NetworkController)) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController is ${typeof state.NetworkController}`, + ), + ); + } else if ( + isObject(state.NetworkController) && + !isObject(state.NetworkController.providerConfig) + ) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.providerConfig is ${typeof state + .NetworkController.providerConfig}`, + ), + ); + } else if ( + isObject(state.NetworkController) && + isObject(state.NetworkController.providerConfig) + ) { + global.sentry?.captureException?.( + new Error( + `typeof state.NetworkController.providerConfig.id is ${typeof state + .NetworkController.providerConfig + .id} and state.NetworkController.providerConfig.type is ${ + state.NetworkController.providerConfig.type + }`, + ), + ); } return state; } diff --git a/types/global.d.ts b/types/global.d.ts index 2c9c1d126..4c6f9c226 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -9,7 +9,7 @@ declare class Platform { closeCurrentWindow: () => void; } -declare class SentryObject extends Sentry { +type SentryObject = Sentry & { // Verifies that the user has opted into metrics and then updates the sentry // instance to track sessions and begins the session. startSession: () => void; @@ -20,7 +20,7 @@ declare class SentryObject extends Sentry { // Calls either startSession or endSession based on optin status toggleSession: () => void; -} +}; export declare global { var platform: Platform;