diff --git a/app/store/migrations/059.test.ts b/app/store/migrations/059.test.ts new file mode 100644 index 00000000000..b0fb374d7af --- /dev/null +++ b/app/store/migrations/059.test.ts @@ -0,0 +1,81 @@ +import migrate, { DEFAULT_NOTIFICATION_SERVICES_CONTROLLER } from './059'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState, { backgroundState } from '../../util/test/initial-root-state'; +import mockedEngine from '../../core/__mocks__/MockedEngine'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +jest.mock('../../core/Engine', () => ({ + init: () => mockedEngine.init(), +})); + +describe('Migration #59 - Insert NotificationServicesController if missing', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "FATAL ERROR: Migration 59: Invalid state error: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: + "FATAL ERROR: Migration 59: Invalid engine state error: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: + "FATAL ERROR: Migration 59: Invalid engine backgroundState error: 'object'", + scenario: 'backgroundState is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`should capture exception if ${scenario}`, async () => { + const newState = await migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('should insert default NotificationServicesController if missing', async () => { + const oldState = { + ...initialRootState, + engine: { + backgroundState, + }, + }; + + const expectedState = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + NotificationServicesController: DEFAULT_NOTIFICATION_SERVICES_CONTROLLER + }, + }, + }; + + const migratedState = await migrate(oldState); + expect(migratedState).toStrictEqual(expectedState); + }); +}); diff --git a/app/store/migrations/059.ts b/app/store/migrations/059.ts new file mode 100644 index 00000000000..e4f59ecf1ad --- /dev/null +++ b/app/store/migrations/059.ts @@ -0,0 +1,29 @@ +import { ensureValidState } from './util'; +import { hasProperty, isObject } from '@metamask/utils'; + +export const DEFAULT_NOTIFICATION_SERVICES_CONTROLLER = { + isCheckingAccountsPresence: false, + isFeatureAnnouncementsEnabled: false, + isFetchingMetamaskNotifications: false, + isMetamaskNotificationsFeatureSeen: false, + isNotificationServicesEnabled: false, + isUpdatingMetamaskNotifications: false, + isUpdatingMetamaskNotificationsAccount: [], + metamaskNotificationsList: [], + metamaskNotificationsReadList: [], + subscriptionAccountsSeen: [], +}; + +export default function migrate(state: unknown) { + if (!ensureValidState(state, 59)) { + return state; + } + + if ( + !hasProperty(state.engine.backgroundState, 'NotificationServicesController') || + !isObject(state.engine.backgroundState.NotificationServicesController) + ) { + state.engine.backgroundState.NotificationServicesController = DEFAULT_NOTIFICATION_SERVICES_CONTROLLER; + } + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 91c8ba0a5d5..52ac3f83fc8 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -59,6 +59,7 @@ import migration55 from './055'; import migration56 from './056'; import migration57 from './057'; import migration58 from './058'; +import migration59 from './059'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -130,6 +131,7 @@ export const migrationList: MigrationsList = { 56: migration56, 57: migration57, 58: migration58, + 59: migration59, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/sentry/tags/index.ts b/app/util/sentry/tags/index.ts index 796bb9212fe..636663a71be 100644 --- a/app/util/sentry/tags/index.ts +++ b/app/util/sentry/tags/index.ts @@ -8,6 +8,12 @@ import { selectPendingApprovals } from '../../../selectors/approvalController'; export function getTraceTags(state: RootState) { if (!Object.keys(state?.engine?.backgroundState).length) return; + if (!state?.engine?.backgroundState?.AccountsController) return; + if (!state?.engine?.backgroundState?.NftController) return; + if (!state?.engine?.backgroundState?.NotificationServicesController) return; + if (!state?.engine?.backgroundState?.TokensController) return; + if (!state?.engine?.backgroundState?.TransactionController) return; + if (!state?.engine?.backgroundState?.NotificationServicesController) return; const unlocked = state.user.userLoggedIn; const accountCount = selectInternalAccounts(state).length; const nftCount = selectAllNftsFlat(state).length;