From dcc90bc99a554ff47b994367da28edbb2af0522b Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 4 Apr 2022 11:54:00 -0400 Subject: [PATCH] Add registration & verification tests --- .../verification/__tests__/index.test.tsx | 100 +++++++++++++++++ app/soapbox/features/verification/index.js | 50 --------- app/soapbox/features/verification/index.tsx | 58 ++++++++++ .../features/verification/registration.js | 4 +- .../steps/__tests__/age-verification.test.js | 51 +++++++++ .../__tests__/email-verification.test.js | 67 ++++++++++++ .../steps/__tests__/sms-verification.test.js | 103 ++++++++++++++++++ ...ge_verification.js => age-verification.js} | 8 +- ..._verification.js => email-verification.js} | 4 +- ...ms_verification.js => sms-verification.js} | 4 +- 10 files changed, 389 insertions(+), 60 deletions(-) create mode 100644 app/soapbox/features/verification/__tests__/index.test.tsx delete mode 100644 app/soapbox/features/verification/index.js create mode 100644 app/soapbox/features/verification/index.tsx create mode 100644 app/soapbox/features/verification/steps/__tests__/age-verification.test.js create mode 100644 app/soapbox/features/verification/steps/__tests__/email-verification.test.js create mode 100644 app/soapbox/features/verification/steps/__tests__/sms-verification.test.js rename app/soapbox/features/verification/steps/{age_verification.js => age-verification.js} (90%) rename app/soapbox/features/verification/steps/{email_verification.js => email-verification.js} (96%) rename app/soapbox/features/verification/steps/{sms_verification.js => sms-verification.js} (97%) diff --git a/app/soapbox/features/verification/__tests__/index.test.tsx b/app/soapbox/features/verification/__tests__/index.test.tsx new file mode 100644 index 000000000..b4c28509e --- /dev/null +++ b/app/soapbox/features/verification/__tests__/index.test.tsx @@ -0,0 +1,100 @@ +import { Map as ImmutableMap } from 'immutable'; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { __stub } from '../../../__mocks__/api'; +import { render, screen } from '../../../jest/test-helpers'; +import Verification from '../index'; + +const TestableComponent = () => ( + + + Homepage + +); + +const renderComponent = (store) => render( + , + {}, + store, + { initialEntries: ['/auth/verify'] }, +); + +describe('', () => { + let store; + + beforeEach(() => { + store = { + verification: ImmutableMap({ + instance: { + isReady: true, + registrations: true, + }, + isComplete: false, + }), + }; + + __stub(mock => { + mock.onGet('/api/v1/pepe/instance') + .reply(200, { + age_minimum: 18, + approval_required: true, + challenges: ['age', 'email', 'sms'], + }); + + mock.onPost('/api/v1/pepe/registrations') + .reply(200, { + access_token: 'N-dZmNqNSmTutJLsGjZ5AnJL4sLw_y-N3pn2acSqJY8', + }); + }); + }); + + describe('When registration is closed', () => { + it('successfully redirects to the homepage', () => { + const verification = store.verification.setIn(['instance', 'registrations'], false); + store.verification = verification; + + renderComponent(store); + expect(screen.getByTestId('home')).toHaveTextContent('Homepage'); + }); + }); + + describe('When verification is complete', () => { + it('successfully renders the Registration component', () => { + const verification = store.verification.set('isComplete', true); + store.verification = verification; + + renderComponent(store); + expect(screen.getByRole('heading')).toHaveTextContent('Register your account'); + }); + }); + + describe('Switching verification steps', () => { + it('successfully renders the Birthday step', () => { + const verification = store.verification.set('currentChallenge', 'age'); + store.verification = verification; + + renderComponent(store); + + expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date'); + }); + + it('successfully renders the Email step', () => { + const verification = store.verification.set('currentChallenge', 'email'); + store.verification = verification; + + renderComponent(store); + + expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address'); + }); + + it('successfully renders the SMS step', () => { + const verification = store.verification.set('currentChallenge', 'sms'); + store.verification = verification; + + renderComponent(store); + + expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number'); + }); + }); +}); diff --git a/app/soapbox/features/verification/index.js b/app/soapbox/features/verification/index.js deleted file mode 100644 index 63ad1b07c..000000000 --- a/app/soapbox/features/verification/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { fetchVerificationConfig } from 'soapbox/actions/verification'; - -import Registration from './registration'; -import AgeVerification from './steps/age_verification'; -import EmailVerification from './steps/email_verification'; -import SmsVerification from './steps/sms_verification'; - -const verificationSteps = { - email: EmailVerification, - sms: SmsVerification, - age: AgeVerification, -}; - -const Verification = () => { - const dispatch = useDispatch(); - - const isInstanceReady = useSelector((state) => state.getIn(['verification', 'instance', 'isReady'], false) === true); - const isRegistrationOpen = useSelector(state => state.getIn(['verification', 'instance', 'registrations'], false) === true); - const currentChallenge = useSelector((state) => state.getIn(['verification', 'currentChallenge'])); - const isVerificationComplete = useSelector((state) => state.getIn(['verification', 'isComplete'])); - const StepToRender = verificationSteps[currentChallenge]; - - React.useEffect(() => { - dispatch(fetchVerificationConfig()); - }, []); - - if (isInstanceReady && !isRegistrationOpen) { - return ; - } - - if (isVerificationComplete) { - return ( - - ); - } - - if (!currentChallenge) { - return null; - } - - return ( - - ); -}; - -export default Verification; diff --git a/app/soapbox/features/verification/index.tsx b/app/soapbox/features/verification/index.tsx new file mode 100644 index 000000000..db2dcaaf6 --- /dev/null +++ b/app/soapbox/features/verification/index.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { useDispatch } from 'react-redux'; +import { Redirect } from 'react-router-dom'; + +import { fetchVerificationConfig } from 'soapbox/actions/verification'; +import { useAppSelector } from 'soapbox/hooks'; + +import Registration from './registration'; +import AgeVerification from './steps/age-verification'; +import EmailVerification from './steps/email-verification'; +import SmsVerification from './steps/sms-verification'; + +// eslint-disable-next-line no-unused-vars +enum ChallengeTypes { + EMAIL = 'email', // eslint-disable-line no-unused-vars + SMS = 'sms', // eslint-disable-line no-unused-vars + AGE = 'age', // eslint-disable-line no-unused-vars +} + +const verificationSteps = { + email: EmailVerification, + sms: SmsVerification, + age: AgeVerification, +}; + +const Verification = () => { + const dispatch = useDispatch(); + + const isInstanceReady = useAppSelector((state) => state.verification.getIn(['instance', 'isReady'], false) === true); + const isRegistrationOpen = useAppSelector(state => state.verification.getIn(['instance', 'registrations'], false) === true); + const currentChallenge = useAppSelector((state) => state.verification.getIn(['currentChallenge']) as ChallengeTypes); + const isVerificationComplete = useAppSelector((state) => state.verification.get('isComplete')); + const StepToRender = verificationSteps[currentChallenge]; + + React.useEffect(() => { + dispatch(fetchVerificationConfig()); + }, []); + + if (isInstanceReady && !isRegistrationOpen) { + return ; + } + + if (isVerificationComplete) { + return ( + + ); + } + + if (!currentChallenge) { + return null; + } + + return ( + + ); +}; + +export default Verification; diff --git a/app/soapbox/features/verification/registration.js b/app/soapbox/features/verification/registration.js index fb874b597..b00be7ad0 100644 --- a/app/soapbox/features/verification/registration.js +++ b/app/soapbox/features/verification/registration.js @@ -20,7 +20,7 @@ const Registration = () => { const dispatch = useDispatch(); const intl = useIntl(); - const isLoading = useSelector((state) => state.getIn(['verification', 'isLoading'])); + const isLoading = useSelector((state) => state.verification.get('isLoading')); const siteTitle = useSelector((state) => state.instance.title); const [state, setState] = React.useState(initialState); @@ -89,7 +89,7 @@ const Registration = () => {
-
+ ', () => { + let store; + + beforeEach(() => { + store = { + verification: ImmutableMap({ + ageMinimum: 13, + }), + }; + + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_age/confirm') + .reply(200, {}); + }); + }); + + it('successfully renders the Birthday step', async() => { + render( + , + {}, + store, + ); + expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date'); + }); + + it('selects a date', async() => { + render( + , + {}, + store, + ); + + await userEvent.type(screen.getByLabelText('Birth Date'), '{enter}'); + + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); +}); diff --git a/app/soapbox/features/verification/steps/__tests__/email-verification.test.js b/app/soapbox/features/verification/steps/__tests__/email-verification.test.js new file mode 100644 index 000000000..604b8b536 --- /dev/null +++ b/app/soapbox/features/verification/steps/__tests__/email-verification.test.js @@ -0,0 +1,67 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import '@testing-library/jest-dom'; + +import { __stub } from 'soapbox/api'; +import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers'; + +import EmailVerification from '../email-verification'; + +describe('', () => { + it('successfully renders the Email step', async() => { + render(); + expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address'); + }); + + describe('with valid data', () => { + beforeEach(() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_email/request') + .reply(200, {}); + }); + }); + + it('successfully submits', async() => { + render(); + + await userEvent.type(screen.getByLabelText('Email Address'), 'foo@bar.com{enter}'); + + await waitFor(() => { + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); + + expect(screen.getByRole('button')).toHaveTextContent('Resend verification email'); + }); + }); + + describe('with invalid data', () => { + beforeEach(() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_email/request') + .reply(422, { + error: 'email_taken', + }); + }); + }); + + it('renders errors', async() => { + render(); + + await userEvent.type(screen.getByLabelText('Email Address'), 'foo@bar.com{enter}'); + + await waitFor(() => { + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); + + expect(screen.getByTestId('form-group-error')).toHaveTextContent('is taken'); + }); + }); +}); diff --git a/app/soapbox/features/verification/steps/__tests__/sms-verification.test.js b/app/soapbox/features/verification/steps/__tests__/sms-verification.test.js new file mode 100644 index 000000000..f6a997e2b --- /dev/null +++ b/app/soapbox/features/verification/steps/__tests__/sms-verification.test.js @@ -0,0 +1,103 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import '@testing-library/jest-dom'; + +import { __stub } from 'soapbox/api'; +import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers'; + +import SmsVerification from '../sms-verification'; + +describe('', () => { + it('successfully renders the SMS step', async() => { + render(); + expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number'); + }); + + describe('with valid data', () => { + beforeEach(() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_sms/request').reply(200, {}); + }); + }); + + it('successfully submits', async() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(200, {}); + }); + + render(); + + await userEvent.type(screen.getByLabelText('Phone Number'), '+1 (555) 555-5555'); + await waitFor(() => { + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); + + expect(screen.getByRole('heading')).toHaveTextContent('Verification code'); + expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.'); + + await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1'); + await userEvent.type(screen.getByLabelText('Digit 2'), '2'); + await userEvent.type(screen.getByLabelText('Digit 3'), '3'); + await userEvent.type(screen.getByLabelText('Digit 4'), '4'); + await userEvent.type(screen.getByLabelText('Digit 5'), '5'); + await userEvent.type(screen.getByLabelText('Digit 6'), '6'); + }); + + it('handle expired tokens', async() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(422, {}); + }); + + render(); + + await userEvent.type(screen.getByLabelText('Phone Number'), '+1 (555) 555-5555'); + await waitFor(() => { + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); + + expect(screen.getByRole('heading')).toHaveTextContent('Verification code'); + expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.'); + + await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1'); + await userEvent.type(screen.getByLabelText('Digit 2'), '2'); + await userEvent.type(screen.getByLabelText('Digit 3'), '3'); + await userEvent.type(screen.getByLabelText('Digit 4'), '4'); + await userEvent.type(screen.getByLabelText('Digit 5'), '5'); + await userEvent.type(screen.getByLabelText('Digit 6'), '6'); + + expect(screen.getByTestId('toast')).toHaveTextContent('Your SMS token has expired.'); + }); + }); + + describe('with invalid data', () => { + beforeEach(() => { + __stub(mock => { + mock.onPost('/api/v1/pepe/verify_sms/request') + .reply(422, {}); + }); + }); + + it('renders errors', async() => { + render(); + + await userEvent.type(screen.getByLabelText('Phone Number'), '+1 (555) 555-5555'); + await waitFor(() => { + fireEvent.submit( + screen.getByRole('button'), { + preventDefault: () => {}, + }, + ); + }); + + expect(screen.getByTestId('toast')).toHaveTextContent('Failed to send SMS message to your phone number.'); + }); + }); +}); diff --git a/app/soapbox/features/verification/steps/age_verification.js b/app/soapbox/features/verification/steps/age-verification.js similarity index 90% rename from app/soapbox/features/verification/steps/age_verification.js rename to app/soapbox/features/verification/steps/age-verification.js index 450c5a459..64d2297ce 100644 --- a/app/soapbox/features/verification/steps/age_verification.js +++ b/app/soapbox/features/verification/steps/age-verification.js @@ -28,9 +28,9 @@ const AgeVerification = () => { const intl = useIntl(); const dispatch = useDispatch(); - const isLoading = useSelector((state) => state.getIn(['verification', 'isLoading'])); - const ageMinimum = useSelector((state) => state.getIn(['verification', 'ageMinimum'])); - const siteTitle = useSelector((state) => state.instance.title); + const isLoading = useSelector((state) => state.verification.get('isLoading')); + const ageMinimum = useSelector((state) => state.verification.get('ageMinimum')); + const siteTitle = useSelector((state) => state.instance.get('title')); const [date, setDate] = React.useState(''); const isValid = typeof date === 'object'; @@ -65,7 +65,7 @@ const AgeVerification = () => {
- + { const intl = useIntl(); const dispatch = useDispatch(); - const isLoading = useSelector((state) => state.getIn(['verification', 'isLoading'])); + const isLoading = useSelector((state) => state.verification.get('isLoading')); const [email, setEmail] = React.useState(''); const [status, setStatus] = React.useState(Statuses.IDLE); @@ -110,7 +110,7 @@ const EmailVerification = () => {
- + { const intl = useIntl(); const dispatch = useDispatch(); - const isLoading = useSelector((state) => state.getIn(['verification', 'isLoading'])); + const isLoading = useSelector((state) => state.verification.get('isLoading')); const [phone, setPhone] = React.useState(''); const [status, setStatus] = React.useState(Statuses.IDLE); @@ -147,7 +147,7 @@ const SmsVerification = () => {
- +