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
109 changes: 109 additions & 0 deletions app/views/ChangePasswordView/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { useDispatch } from 'react-redux';

import ChangePasswordView 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/twoFactor';
import handleSaveUserProfileError from '../../lib/methods/helpers/handleSaveUserProfileError';
import { TwoFactorMethods } from '../../definitions/ITotp';

jest.mock('react-redux', () => ({
useDispatch: jest.fn()
}));

jest.mock('../../lib/hooks/useAppSelector', () => ({
useAppSelector: jest.fn()
}));

jest.mock('../../lib/services/restApi', () => ({
saveUserProfile: jest.fn(),
setPassword: jest.fn()
}));

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

jest.mock('../../lib/methods/helpers/handleSaveUserProfileError', () => jest.fn());

jest.mock('../../lib/hooks/useVerifyPassword', () => () => ({ isPasswordValid: true, passwordPolicies: null }));

jest.mock('react-native-keyboard-controller', () => {
const { View } = require('react-native');
return { KeyboardAvoidingView: View };
});

const user = {
id: 'user-id',
username: 'john.doe',
emails: [{ address: 'john@rocket.chat', verified: true }]
};

const buildState = () => ({
login: { user },
server: { server: 'https://open.rocket.chat' },
settings: {
Accounts_AllowPasswordChange: true,
Accounts_RequirePasswordConfirmation: true
}
});

const navigation = {
setOptions: jest.fn(),
goBack: jest.fn(),
getState: () => ({ routes: [{ name: 'ProfileView' }] })
} as any;

const totpInvalid = { error: 'totp-invalid', details: { method: TwoFactorMethods.TOTP } };

const renderChangePassword = () => {
(useAppSelector as jest.Mock).mockImplementation((selector: (state: any) => unknown) => selector(buildState()));
return render(<ChangePasswordView navigation={navigation} />);
};

const fillAndSubmit = (getByTestId: (id: string) => any) => {
fireEvent.changeText(getByTestId('change-password-view-current-password'), 'current-password');
fireEvent.changeText(getByTestId('change-password-view-new-password'), 'new-password');
fireEvent.changeText(getByTestId('change-password-view-confirm-new-password'), 'new-password');
fireEvent.press(getByTestId('change-password-view-set-new-password-button'));
};

beforeEach(() => {
jest.clearAllMocks();
(useDispatch as jest.Mock).mockReturnValue(jest.fn());
});

describe('ChangePasswordView two-factor', () => {
it('clears the current password and the stored code when the user cancels the 2FA prompt', async () => {
(saveUserProfile as jest.Mock).mockRejectedValue(totpInvalid);
(twoFactor as jest.Mock)
.mockResolvedValueOnce({ twoFactorCode: '123456', twoFactorMethod: TwoFactorMethods.TOTP })
.mockRejectedValue(new TwoFactorCancelledError());

const { getByTestId } = renderChangePassword();
fillAndSubmit(getByTestId);

await waitFor(() => expect(twoFactor).toHaveBeenCalledTimes(2));
expect(getByTestId('change-password-view-current-password').props.value).toBe('');

fillAndSubmit(getByTestId);

await waitFor(() => expect(twoFactor).toHaveBeenCalledTimes(3));
expect((twoFactor as jest.Mock).mock.calls[2][0]).toEqual(expect.objectContaining({ invalid: false }));
expect(handleSaveUserProfileError).not.toHaveBeenCalled();
});

it('reports the two-factor failure instead of the original totp-invalid error', async () => {
const twoFactorFailure = new Error('two-factor prompt blew up');
(saveUserProfile as jest.Mock).mockRejectedValue(totpInvalid);
(twoFactor as jest.Mock).mockRejectedValue(twoFactorFailure);

const { getByTestId } = renderChangePassword();
fillAndSubmit(getByTestId);

await waitFor(() => expect(handleSaveUserProfileError).toHaveBeenCalledWith(twoFactorFailure, 'saving_profile'));
});
});
10 changes: 8 additions & 2 deletions app/views/ChangePasswordView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const ChangePasswordView = ({ navigation }: IChangePasswordViewProps) => {
}
};

const resetTwoFactorState = () => {
setValue('currentPassword', '');
setTwoFactorCode(null);
};

const changePasswordFromProfileView = async () => {
const { currentPassword, newPassword, confirmNewPassword } = inputValues;
if (newPassword !== confirmNewPassword) {
Expand Down Expand Up @@ -147,9 +152,11 @@ const ChangePasswordView = ({ navigation }: IChangePasswordViewProps) => {
setTwoFactorCode(code as any);
return handleSetNewPassword();
} catch (twoFactorError) {
resetTwoFactorState();
if (isTwoFactorCancelled(twoFactorError)) {
return;
}
return handleSaveUserProfileError(twoFactorError, 'saving_profile');
}
}

Expand All @@ -159,8 +166,7 @@ const ChangePasswordView = ({ navigation }: IChangePasswordViewProps) => {
return;
}

setValue('currentPassword', '');
setTwoFactorCode(null);
resetTwoFactorState();
handleSaveUserProfileError(e, 'saving_profile');
} finally {
setValue('saving', false);
Expand Down
Loading