Enable forgot password functinality for secondary email.
VAN-18
This commit is contained in:
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { Button, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getQueryParameters } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
import { resetPassword, validateToken } from './data/actions';
|
||||
import { resetPasswordResultSelector } from './data/selectors';
|
||||
@@ -16,6 +17,7 @@ import Spinner from './Spinner';
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
const params = getQueryParameters();
|
||||
|
||||
const [newPasswordInput, setNewPasswordValue] = useState('');
|
||||
const [confirmPasswordInput, setConfirmPasswordValue] = useState('');
|
||||
@@ -55,7 +57,7 @@ const ResetPasswordPage = (props) => {
|
||||
new_password1: newPasswordInput,
|
||||
new_password2: confirmPasswordInput,
|
||||
};
|
||||
props.resetPassword(formPayload, props.token);
|
||||
props.resetPassword(formPayload, props.token, params);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ export const validateTokenFailure = (tokenStatus) => ({
|
||||
});
|
||||
|
||||
// Reset Password
|
||||
export const resetPassword = (formPayload, token) => ({
|
||||
export const resetPassword = (formPayload, token, params) => ({
|
||||
type: RESET_PASSWORD.BASE,
|
||||
payload: { formPayload, token },
|
||||
payload: { formPayload, token, params },
|
||||
});
|
||||
|
||||
export const resetPasswordBegin = () => ({
|
||||
|
||||
@@ -34,7 +34,7 @@ export function* handleValidateToken(action) {
|
||||
export function* handleResetPassword(action) {
|
||||
try {
|
||||
yield put(resetPasswordBegin());
|
||||
const data = yield call(resetPassword, action.payload.formPayload, action.payload.token);
|
||||
const data = yield call(resetPassword, action.payload.formPayload, action.payload.token, action.payload.params);
|
||||
const resetStatus = data.reset_status;
|
||||
const resetErrors = data.err_msg;
|
||||
|
||||
|
||||
@@ -22,15 +22,17 @@ export async function validateToken(token) {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function resetPassword(payload, token) {
|
||||
export async function resetPassword(payload, token, queryParams) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
let path = `password/reset/${token}/?track=pwreset`;
|
||||
path += queryParams.is_account_recovery ? '&is_account_recovery=true' : '';
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/password/reset/${token}/?track=pwreset`,
|
||||
`${getConfig().LMS_BASE_URL}/${path}`,
|
||||
formurlencoded(payload),
|
||||
requestConfig,
|
||||
)
|
||||
|
||||
59
src/reset-password/data/tests/sagas.test.js
Normal file
59
src/reset-password/data/tests/sagas.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { runSaga } from 'redux-saga';
|
||||
|
||||
import {
|
||||
resetPasswordBegin,
|
||||
resetPasswordSuccess,
|
||||
resetPasswordFailure,
|
||||
} from '../actions';
|
||||
import { handleResetPassword } from '../sagas';
|
||||
import * as api from '../service';
|
||||
|
||||
describe('handleResetPassword', () => {
|
||||
const params = {
|
||||
payload: {
|
||||
formPayload: {
|
||||
new_password1: 'new_password1',
|
||||
new_password2: 'new_password1',
|
||||
},
|
||||
token: 'token',
|
||||
params: {},
|
||||
},
|
||||
};
|
||||
|
||||
const responseData = {
|
||||
reset_status: true,
|
||||
err_msg: '',
|
||||
};
|
||||
|
||||
it('should call service and dispatch success action', async () => {
|
||||
const resetPassword = jest.spyOn(api, 'resetPassword')
|
||||
.mockImplementation(() => Promise.resolve(responseData));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
{ dispatch: (action) => dispatched.push(action) },
|
||||
handleResetPassword,
|
||||
params,
|
||||
);
|
||||
|
||||
expect(resetPassword).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordSuccess(true)]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
|
||||
it('should call service and dispatch error action', async () => {
|
||||
const resetPassword = jest.spyOn(api, 'resetPassword')
|
||||
.mockImplementation(() => Promise.reject());
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
{ dispatch: (action) => dispatched.push(action) },
|
||||
handleResetPassword,
|
||||
params,
|
||||
);
|
||||
|
||||
expect(resetPassword).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure()]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Provider } from 'react-redux';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { resetPassword } from '../data/actions';
|
||||
|
||||
import ResetPasswordPage from '../ResetPasswordPage';
|
||||
|
||||
@@ -15,6 +16,7 @@ jest.mock('@edx/frontend-platform/auth');
|
||||
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
|
||||
describe('ResetPasswordPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
@@ -36,6 +38,10 @@ describe('ResetPasswordPage', () => {
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should match reset password default section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
@@ -106,4 +112,46 @@ describe('ResetPasswordPage', () => {
|
||||
resetPasswordPage.update();
|
||||
expect(resetPasswordPage.find('#reset-password-input-invalid-feedback').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('with valid inputs resetPassword action is dispatch', () => {
|
||||
const newPassword = 'test-password1';
|
||||
store = mockStore({
|
||||
...store,
|
||||
});
|
||||
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
token: 'token',
|
||||
};
|
||||
|
||||
const formPayload = {
|
||||
new_password1: newPassword,
|
||||
new_password2: newPassword,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const resetPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPage.find('input#reset-password-input').simulate('blur', { target: { value: newPassword } });
|
||||
resetPage.find('input#confirm-password-input').simulate('change', { target: { value: newPassword } });
|
||||
resetPage.find('button.submit').simulate('click');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(resetPassword(formPayload, props.token, {}));
|
||||
resetPage.unmount();
|
||||
});
|
||||
|
||||
it('show spinner component during token validation', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'pending',
|
||||
match: {
|
||||
params: {
|
||||
token: 'test-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -355,3 +355,51 @@ Array [
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage show spinner component during token validation 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column align-items-start"
|
||||
>
|
||||
<h3
|
||||
className="text-center mt-3"
|
||||
>
|
||||
<span>
|
||||
Token validation in progress ..
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-spinner fa-w-16 fa-spin "
|
||||
data-icon="spinner"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user