Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/views/ProfileView/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProfileView from './index';
import { useAppSelector } from '../../lib/hooks/useAppSelector';
import { saveUserProfile } from '../../lib/services/restApi';
import { twoFactor } from '../../lib/services/twoFactor';
import { TwoFactorCancelledError } from '../../lib/services/twoFactorCancelled';
import handleSaveUserProfileError from '../../lib/methods/helpers/handleSaveUserProfileError';
import EventEmitter from '../../lib/methods/helpers/events';
import { setUser } from '../../actions/login';
Expand All @@ -28,6 +29,7 @@ jest.mock('../../lib/services/restApi', () => ({
}));

jest.mock('../../lib/services/twoFactor', () => ({
...jest.requireActual('../../lib/services/twoFactor'),
twoFactor: jest.fn()
}));

Expand Down Expand Up @@ -134,6 +136,28 @@ describe('ProfileView submit', () => {
expect(handleSaveUserProfileError).not.toHaveBeenCalled();
});

it('stays silent when the user cancels the 2FA challenge', async () => {
(saveUserProfile as jest.Mock).mockRejectedValue({ error: 'totp-invalid', details: { method: 'totp' } });
(twoFactor as jest.Mock).mockRejectedValue(new TwoFactorCancelledError());

const { getByTestId } = renderProfile();
changeNameAndSubmit(getByTestId);

await waitFor(() => expect(twoFactor).toHaveBeenCalled());
expect(handleSaveUserProfileError).not.toHaveBeenCalled();
});
Comment thread
diegolmello marked this conversation as resolved.

it('reports the 2FA error itself when the challenge fails for a reason other than cancelling', async () => {
const twoFactorError = { error: 'totp-required' };
(saveUserProfile as jest.Mock).mockRejectedValue({ error: 'totp-invalid', details: { method: 'totp' } });
(twoFactor as jest.Mock).mockRejectedValue(twoFactorError);

const { getByTestId } = renderProfile();
changeNameAndSubmit(getByTestId);

await waitFor(() => expect(handleSaveUserProfileError).toHaveBeenCalledWith(twoFactorError, 'saving_profile'));
});

it('handles the save error after a cancelled/non-2FA failure', async () => {
const error = { error: 'some-other-error' };
(saveUserProfile as jest.Mock).mockRejectedValue(error);
Expand Down
23 changes: 13 additions & 10 deletions app/views/ProfileView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const MAX_NICKNAME_LENGTH = 120;
interface IProfileViewProps {
navigation: NativeStackNavigationProp<ProfileStackParamList, 'ProfileView'>;
}
type TwoFactorChallengeOutcome = { status: 'retried' } | { status: 'cancelled' } | { status: 'failed'; error: unknown };

const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => {
const validationSchema = yup.object().shape({
name: yup.string().required(I18n.t('Name_required')),
Expand Down Expand Up @@ -205,21 +207,20 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => {
}
};

const handleTwoFactorChallenge = async (e: any): Promise<boolean> => {
const handleTwoFactorChallenge = async (e: any): Promise<TwoFactorChallengeOutcome> => {
if (e?.error !== 'totp-invalid' || e?.details.method === TwoFactorMethods.PASSWORD) {
return false;
return { status: 'failed', error: e };
}
try {
const code = await twoFactor({ method: e.details.method, invalid: e?.error === 'totp-invalid' && !!twoFactorCode });
setTwoFactorCode(code as any);
await submit();
return true;
return { status: 'retried' };
} catch (twoFactorError) {
if (isTwoFactorCancelled(twoFactorError)) {
resetSavingState();
return true;
return { status: 'cancelled' };
}
return false;
return { status: 'failed', error: twoFactorError };
}
};

Expand Down Expand Up @@ -251,12 +252,14 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => {
const { email } = getValues();
setFieldErrorsFromResponse(e, email);

const handled = await handleTwoFactorChallenge(e);
if (handled) return;
const twoFactorOutcome = await handleTwoFactorChallenge(e);
if (twoFactorOutcome.status === 'retried') return;

logEvent(events.PROFILE_SAVE_CHANGES_F);
resetSavingState();
handleSaveUserProfileError(e, 'saving_profile');
if (twoFactorOutcome.status === 'cancelled') return;

logEvent(events.PROFILE_SAVE_CHANGES_F);
handleSaveUserProfileError(twoFactorOutcome.error, 'saving_profile');
}
};

Expand Down
Loading