import { act, fireEvent, render, waitFor, within } from '@testing-library/react-native' import { FetchMock } from 'jest-fetch-mock/types' import MockDate from 'mockdate' import React from 'react' import * as Keychain from 'react-native-keychain' import SmsRetriever from 'react-native-sms-retriever' import { Provider } from 'react-redux' import { showError } from 'src/alert/actions' import { ErrorMessages } from 'src/app/ErrorMessages' import { navigate, popToScreen } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { goToNextOnboardingScreen } from 'src/onboarding/steps' import { sleep } from 'src/utils/sleep' import VerificationCodeInputScreen from 'src/verify/VerificationCodeInputScreen' import networkConfig from 'src/web3/networkConfig' import MockedNavigator from 'test/MockedNavigator' import { createMockStore, getMockStackScreenProps } from 'test/utils' import { mockOnboardingProps } from 'test/values' const mockOnboardingPropsSelector = jest.fn(() => mockOnboardingProps) jest.mock('src/onboarding/steps', () => ({ goToNextOnboardingScreen: jest.fn(), onboardingPropsSelector: () => mockOnboardingPropsSelector(), })) const mockFetch = fetch as FetchMock const mockedKeychain = jest.mocked(Keychain) mockedKeychain.getGenericPassword.mockResolvedValue({ username: 'some username', password: 'someSignedMessage', service: 'some service', storage: 'some string', }) const mockedSmsRetriever = jest.mocked(SmsRetriever) const e164Number = '+31619123456' const store = createMockStore({ web3: { account: '0xabc', }, app: { inviterAddress: '0xabc', }, }) const renderComponent = ({ hasOnboarded = false, }: { hasOnboarded?: boolean } = {}) => render( ) describe('VerificationCodeInputScreen', () => { beforeEach(() => { jest.clearAllMocks() mockFetch.resetMocks() store.clearActions() MockDate.reset() }) it('displays the correct components and requests for the verification code on mount', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) const { getByText, getByTestId } = renderComponent() expect(getByText('phoneVerificationInput.title')).toBeTruthy() expect( getByText(`phoneVerificationInput.description, {"phoneNumber":"${e164Number}"}`) ).toBeTruthy() expect(getByText('phoneVerificationInput.help')).toBeTruthy() expect(getByTestId('PhoneVerificationCode')).toBeTruthy() expect(getByTestId('PhoneVerificationResendSmsBtn')).toBeDisabled() await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) expect(mockFetch).toHaveBeenCalledWith(`${networkConfig.verifyPhoneNumberUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}', }) }) it('displays an error if verification code request fails', async () => { mockFetch.mockResponseOnce(JSON.stringify({ message: 'something went wrong' }), { status: 500 }) renderComponent() await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) expect(store.getActions()).toEqual( expect.arrayContaining([showError(ErrorMessages.PHONE_NUMBER_VERIFICATION_FAILURE)]) ) }) it('verifies the sms code and navigates to next onboarding screen', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), { status: 200, }) const { getByTestId } = renderComponent() await act(() => { fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456') }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy() await act(() => { jest.runOnlyPendingTimers() }) expect(goToNextOnboardingScreen).toHaveBeenCalledWith({ firstScreenInCurrentStep: Screens.VerificationStartScreen, onboardingProps: mockOnboardingProps, }) expect(navigate).not.toHaveBeenCalled() expect(popToScreen).not.toHaveBeenCalled() }) it('verifies the sms code and navigates to home if not in onboarding and no previous routes found', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), { status: 200, }) const { getByTestId } = renderComponent({ hasOnboarded: true }) await act(() => { fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456') }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy() await act(() => { jest.runOnlyPendingTimers() }) expect(navigate).toHaveBeenCalledWith(Screens.TabHome) expect(popToScreen).not.toHaveBeenCalled() expect(goToNextOnboardingScreen).not.toHaveBeenCalled() }) it('verifies the sms code and navigates to previous route if not in onboarding', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), { status: 200, }) const screenProps = getMockStackScreenProps(Screens.VerificationCodeInputScreen, { countryCallingCode: '+31', e164Number, hasOnboarded: true, }) jest.mocked(screenProps.navigation).getState = jest.fn( () => ({ routes: [ { name: Screens.ProfileSubmenu }, { name: Screens.VerificationStartScreen }, { name: Screens.VerificationCodeInputScreen }, ], }) as any ) const { getByTestId } = render( ) await act(() => { fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456') }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy() await act(() => { jest.runOnlyPendingTimers() }) expect(popToScreen).toHaveBeenCalledWith(Screens.ProfileSubmenu) expect(navigate).not.toHaveBeenCalled() expect(goToNextOnboardingScreen).not.toHaveBeenCalled() }) it('waits for the verificationId to be captured before verifying sms', async () => { mockFetch.mockImplementation(async (url?: string | Request) => { if (url === networkConfig.verifyPhoneNumberUrl) { await sleep(1000) // some arbitrary network delay return new Response(JSON.stringify({ data: { verificationId: 'someId' } })) } return new Response(JSON.stringify({ message: 'OK' })) }) const { getByTestId } = renderComponent() await act(() => { // enter the verification code before the verifyPhoneNumber fetch has resolved fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456') }) await act(() => { // handle the verification code, and then increment the timer to resolve // the network delay jest.runOnlyPendingTimers() }) expect(mockFetch).toHaveBeenCalledTimes(2) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy() }) it('reads the SMS code on Android automatically', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), { status: 200, }) const { getByTestId, getByText } = renderComponent() // Check that the SmsRetriever is started await waitFor(() => expect(mockedSmsRetriever.startSmsRetriever).toHaveBeenCalledTimes(1)) expect(mockedSmsRetriever.addSmsListener).toHaveBeenCalledTimes(1) const smsListener = mockedSmsRetriever.addSmsListener.mock.calls[0][0] await act(() => { // Simulate the SMS code being received smsListener({ message: 'Your verification code for App is: 123456 5yaJvJcZt2P' }) }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByText('123456')).toBeTruthy() expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy() await act(() => { jest.runOnlyPendingTimers() }) expect(goToNextOnboardingScreen).toHaveBeenCalledWith({ firstScreenInCurrentStep: Screens.VerificationStartScreen, onboardingProps: mockOnboardingProps, }) }) it('handles when phone number already verified', async () => { mockFetch.mockResponse(JSON.stringify({ message: 'Phone number already verified' }), { status: 400, }) renderComponent() await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) expect(mockFetch).toHaveBeenCalledWith(`${networkConfig.verifyPhoneNumberUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}', }) await act(() => { jest.runOnlyPendingTimers() }) await waitFor(() => expect(goToNextOnboardingScreen).toHaveBeenCalledWith({ firstScreenInCurrentStep: Screens.VerificationStartScreen, onboardingProps: mockOnboardingProps, }) ) }) it('shows error in verifying sms code', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) mockFetch.mockRejectedValue(JSON.stringify({ message: 'Not OK' })) const { getByTestId } = renderComponent() await act(() => { fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456') }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}', }) expect(getByTestId('PhoneVerificationCode/ErrorIcon')).toBeTruthy() expect(store.getActions()).toEqual( expect.not.arrayContaining([showError(ErrorMessages.PHONE_NUMBER_VERIFICATION_FAILURE)]) ) await act(() => { jest.runOnlyPendingTimers() }) expect(navigate).not.toHaveBeenCalled() }) it('makes a request to resend the sms code and resets the timer', async () => { mockFetch.mockResponse(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) const dateNow = Date.now() MockDate.set(dateNow) const { getByTestId } = renderComponent() await act(() => { MockDate.set(dateNow + 30000) // 30 seconds, matching default resend delay time in ResendButtonWithDelay component jest.advanceTimersByTime(1000) // 1 second, to update the timer }) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) fireEvent.press(getByTestId('PhoneVerificationResendSmsBtn')) await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifyPhoneNumberUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`, }, body: `{"phoneNumber":"${e164Number}","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}`, }) expect(getByTestId('PhoneVerificationResendSmsBtn')).toBeDisabled() }) it('shows the help dialog', async () => { mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), { status: 200, }) const { getByTestId, getByText } = renderComponent() await act(() => { fireEvent.press(getByText('phoneVerificationInput.help')) }) await waitFor(() => expect(getByTestId('PhoneVerificationInputHelpDialog')).toBeTruthy()) const HelpDialog = getByTestId('PhoneVerificationInputHelpDialog') expect(within(HelpDialog).getByText('phoneVerificationInput.helpDialog.title')).toBeTruthy() expect(within(HelpDialog).getByText('phoneVerificationInput.helpDialog.body')).toBeTruthy() }) })