diff --git a/app/store/migrations/030.test.ts b/app/store/migrations/030.test.ts new file mode 100644 index 00000000000..8f404f0a8f6 --- /dev/null +++ b/app/store/migrations/030.test.ts @@ -0,0 +1,92 @@ +import migrate from './030'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState from '../../util/test/initial-root-state'; + +const expectedState = { + engine: { + backgroundState: { + PreferencesController: { + securityAlertsEnabled: true, + }, + }, + }, +}; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration #30', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "Migration 30: Invalid root state: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: "Migration 30: Invalid root engine state: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: + "Migration 30: Invalid root engine backgroundState: 'object'", + scenario: 'backgroundState is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`should capture exception if ${scenario}`, () => { + const newState = migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('should not change anything if security alert is already enabled', () => { + const oldState = { + engine: { + backgroundState: { + PreferencesController: { + securityAlertsEnabled: true, + }, + }, + }, + }; + + const migratedState = migrate(oldState); + expect(migratedState).toStrictEqual(expectedState); + }); + + it('should enable security alert if it is not enabled', () => { + const oldState = { + engine: { + backgroundState: { + PreferencesController: { + securityAlertsEnabled: false, + }, + }, + }, + }; + const migratedState = migrate(oldState); + expect(migratedState).toStrictEqual(expectedState); + }); +}); diff --git a/app/store/migrations/030.ts b/app/store/migrations/030.ts new file mode 100644 index 00000000000..dab6dcd4c01 --- /dev/null +++ b/app/store/migrations/030.ts @@ -0,0 +1,54 @@ +import { PreferencesState } from '@metamask/preferences-controller'; +import { captureException } from '@sentry/react-native'; +import { isObject } from '@metamask/utils'; + +/** + * Enable security alerts by default. + * @param {any} state - Redux state. + * @returns Migrated Redux state. + */ +export default function migrate(state: unknown) { + if (!isObject(state)) { + captureException( + new Error(`Migration 30: Invalid root state: '${typeof state}'`), + ); + return state; + } + + if (!isObject(state.engine)) { + captureException( + new Error( + `Migration 30: Invalid root engine state: '${typeof state.engine}'`, + ), + ); + return state; + } + + if (!isObject(state.engine.backgroundState)) { + captureException( + new Error( + `Migration 30: Invalid root engine backgroundState: '${typeof state + .engine.backgroundState}'`, + ), + ); + return state; + } + + const preferencesControllerState = state.engine.backgroundState + .PreferencesController as PreferencesState; + + if (!isObject(preferencesControllerState)) { + captureException( + new Error( + `Migration 30: Invalid PreferencesController state: '${typeof preferencesControllerState}'`, + ), + ); + return state; + } + + if (!preferencesControllerState.securityAlertsEnabled) { + preferencesControllerState.securityAlertsEnabled = true; + } + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 9e599fa3e50..de194ea95db 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -30,6 +30,7 @@ import migration26 from './026'; import migration27 from './027'; import migration28 from './028'; import migration29 from './029'; +import migration30 from './030'; // We do not keep track of the old state // We create this type for better readability @@ -66,6 +67,7 @@ export const migrations: MigrationManifest = { 27: migration27, 28: migration28 as unknown as (state: PersistedState) => PersistedState, 29: migration29 as unknown as (state: OldState) => PersistedState, + 30: migration30 as unknown as (state: OldState) => PersistedState, }; // The latest (i.e. highest) version number.