import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' import SecuritySubmenu from 'src/account/SecuritySubmenu' import { Screens } from 'src/navigator/Screens' import MockedNavigator from 'test/MockedNavigator' import { createMockStore } from 'test/utils' import { fireEvent, render, waitFor, act } from '@testing-library/react-native' import { ensurePincode, navigate } from 'src/navigator/NavigationService' import { FetchMock } from 'jest-fetch-mock/types' import * as Keychain from 'react-native-keychain' import { setAnalyticsEnabled } from 'src/app/actions' import { showError } from 'src/alert/actions' import { ErrorMessages } from 'src/app/ErrorMessages' import { setPincodeSuccess } from 'src/account/actions' import { mockE164Number } from 'test/values' import { BIOMETRY_TYPE } from 'react-native-keychain' import { PincodeType } from 'src/account/reducer' import { removeStoredPin, setPincodeWithBiometry } from 'src/pincode/authentication' import AppAnalytics from 'src/analytics/AppAnalytics' import { SettingsEvents } from 'src/analytics/Events' import { getFeatureGate } from 'src/statsig/index' import { deleteKeylessBackupStarted, hideDeleteKeylessBackupError } from 'src/keylessBackup/slice' import { KeylessBackupDeleteStatus } from 'src/keylessBackup/types' import networkConfig from 'src/web3/networkConfig' const mockedEnsurePincode = jest.mocked(ensurePincode) const mockFetch = fetch as FetchMock const mockedKeychain = jest.mocked(Keychain) mockedKeychain.getGenericPassword.mockResolvedValue({ username: 'some username', password: 'someSignedMessage', service: 'some service', storage: 'some string', }) jest.mock('src/analytics/AppAnalytics') jest.mock('src/statsig') describe('SecuritySubmenu', () => { beforeEach(() => { jest.mocked(getFeatureGate).mockReturnValue(false) jest.clearAllMocks() }) it('shows the expected menu items', () => { const store = createMockStore() const { getByText } = render( ) expect(getByText('accountKey')).toBeTruthy() // recovery phrase expect(getByText('changePin')).toBeTruthy() expect(getByText('requirePinOnAppOpen')).toBeTruthy() expect(getByText('shareAnalytics')).toBeTruthy() expect(getByText('removeAccountTitle')).toBeTruthy() expect(getByText('deleteAccountTitle')).toBeTruthy() }) it('triggers the correct actions on change data preferences', () => { const store = createMockStore({}) const { getByText } = render( ) store.clearActions() fireEvent(getByText('shareAnalytics'), 'valueChange', false) expect(store.getActions()).toEqual([setAnalyticsEnabled(false)]) }) it('navigates to PincodeSet screen if entered PIN is correct', async () => { const { getByText } = render( ) mockedEnsurePincode.mockImplementation(() => Promise.resolve(true)) await act(() => { fireEvent.press(getByText('changePin')) }) expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet, { changePin: true, }) }) it('does not navigate to PincodeSet screen if entered PIN is incorrect', async () => { const { getByText } = render( ) mockedEnsurePincode.mockImplementation(() => Promise.resolve(false)) await act(() => { fireEvent.press(getByText('changePin')) }) expect(navigate).not.toHaveBeenCalled() }) it('toggle the biometry option correctly', async () => { const store = createMockStore({ app: { supportedBiometryType: BIOMETRY_TYPE.FACE_ID, }, account: { pincodeType: PincodeType.CustomPin, }, }) const { getByText, getByTestId } = render( ) await act(() => { fireEvent(getByTestId('useBiometryToggle'), 'valueChange', true) }) expect(getByText('useBiometryType, {"biometryType":"biometryType.FaceID"}')).toBeTruthy() expect(setPincodeWithBiometry).toHaveBeenCalledTimes(1) expect(store.getActions()).toEqual( expect.arrayContaining([setPincodeSuccess(PincodeType.PhoneAuth)]) ) expect(AppAnalytics.track).toHaveBeenCalledWith(SettingsEvents.settings_biometry_opt_in_enable) expect(AppAnalytics.track).toHaveBeenCalledWith( SettingsEvents.settings_biometry_opt_in_complete ) await act(() => { fireEvent(getByTestId('useBiometryToggle'), 'valueChange', false) }) expect(removeStoredPin).toHaveBeenCalledTimes(1) expect(store.getActions()).toEqual( expect.arrayContaining([setPincodeSuccess(PincodeType.CustomPin)]) ) expect(AppAnalytics.track).toHaveBeenCalledWith(SettingsEvents.settings_biometry_opt_in_disable) }) it('navigates to recovery phrase if entered PIN is correct', async () => { const store = createMockStore() const { getByText } = render( ) mockedEnsurePincode.mockImplementation(() => Promise.resolve(true)) await act(() => { fireEvent.press(getByText('accountKey')) }) expect(AppAnalytics.track).toHaveBeenCalledWith(SettingsEvents.settings_recovery_phrase) expect(navigate).toHaveBeenCalledWith(Screens.BackupIntroduction) }) it('does not navigate to recovery phrase if entered PIN is incorrect', async () => { const store = createMockStore() const { getByText } = render( ) mockedEnsurePincode.mockImplementation(() => Promise.resolve(false)) await act(() => { fireEvent.press(getByText('accountKey')) }) expect(navigate).not.toHaveBeenCalled() }) it('does not show keyless backup', () => { const store = createMockStore() const { queryByText } = render( ) expect(queryByText('keylessBackSettingsTitle')).toBeNull() }) it('shows keyless backup setup when flag is enabled and not already backed up', async () => { jest.mocked(getFeatureGate).mockReturnValue(true) mockedEnsurePincode.mockImplementation(() => Promise.resolve(true)) const store = createMockStore({ account: { cloudBackupCompleted: false } }) const { getByText } = render( ) expect(getByText('keylessBackupSettingsTitle')).toBeTruthy() expect(getByText('setup')).toBeTruthy() await act(() => { fireEvent.press(getByText('keylessBackupSettingsTitle')) }) expect(navigate).toHaveBeenCalledWith(Screens.WalletSecurityPrimer) expect(AppAnalytics.track).toHaveBeenCalledTimes(1) expect(AppAnalytics.track).toHaveBeenLastCalledWith( SettingsEvents.settings_set_up_keyless_backup ) }) it('shows keyless backup delete when flag is enabled and already backed up', () => { jest.mocked(getFeatureGate).mockReturnValue(true) const store = createMockStore({ account: { cloudBackupCompleted: true } }) const { getByText } = render( ) expect(getByText('keylessBackupSettingsTitle')).toBeTruthy() expect(getByText('delete')).toBeTruthy() fireEvent.press(getByText('keylessBackupSettingsTitle')) expect(navigate).not.toHaveBeenCalled() expect(AppAnalytics.track).toHaveBeenCalledTimes(1) expect(AppAnalytics.track).toHaveBeenLastCalledWith( SettingsEvents.settings_delete_keyless_backup ) expect(store.getActions()).toContainEqual(deleteKeylessBackupStarted()) }) it('shows keyless backup in progress when flag is enabled and backup is in progress', () => { jest.mocked(getFeatureGate).mockReturnValue(true) const store = createMockStore({ account: { cloudBackupCompleted: true }, keylessBackup: { deleteBackupStatus: KeylessBackupDeleteStatus.InProgress }, }) const { getByText } = render( ) expect(getByText('keylessBackupSettingsTitle')).toBeTruthy() expect(getByText('pleaseWait')).toBeTruthy() fireEvent.press(getByText('keylessBackupSettingsTitle')) expect(navigate).not.toHaveBeenCalled() expect(AppAnalytics.track).not.toHaveBeenCalled() }) it('shows error banner when keyless backup delete fails', async () => { jest.mocked(getFeatureGate).mockReturnValue(true) const store = createMockStore({ account: { cloudBackupCompleted: true }, keylessBackup: { showDeleteBackupError: true }, }) const { getByText, getByTestId } = render( ) expect(getByText('keylessBackupSettingsDeleteError')).toBeTruthy() await act(() => { fireEvent.press(getByTestId('KeylessBackupDeleteError/dismiss')) }) expect(store.getActions()).toContainEqual(hideDeleteKeylessBackupError()) }) it('fails the delete account request if phone number revoke fails', async () => { mockFetch.mockResponseOnce(JSON.stringify({ message: 'something went wrong' }), { status: 500, }) const store = createMockStore({ app: { phoneNumberVerified: true }, account: { e164PhoneNumber: mockE164Number, }, }) const { getByText } = render( ) // ignore dispatched actions on initial render store.clearActions() fireEvent.press(getByText('deleteAccountTitle')) fireEvent.press(getByText('deleteAccountWarning.buttonLabel')) await waitFor(() => expect(getByText('deleteAccountWarning.buttonLabelRevokingPhoneNumber')).toBeTruthy() ) expect(mockFetch).toHaveBeenNthCalledWith(1, `${networkConfig.revokePhoneNumberUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0x0000000000000000000000000000000000007e57:someSignedMessage`, }, body: '{"phoneNumber":"+14155550000","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(store.getActions()).toEqual([ showError('revokePhoneNumber.revokeError' as ErrorMessages), ]) expect(navigate).not.toHaveBeenCalled() }) })