From d62195aec488b2b150b6ef04717a4445d04ba00e Mon Sep 17 00:00:00 2001 From: AliAkbar Date: Fri, 26 Aug 2022 03:00:09 +0500 Subject: [PATCH] feat: adds cancel retirement feature --- src/users/UserPage.jsx | 10 ++ .../account-actions/CancelRetirement.jsx | 79 ++++++++++++ .../account-actions/CancelRetirement.test.jsx | 120 ++++++++++++++++++ src/users/data/api.js | 31 +++++ src/users/data/api.test.js | 58 ++++++++- src/users/data/urls.js | 4 + 6 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/users/account-actions/CancelRetirement.jsx create mode 100644 src/users/account-actions/CancelRetirement.test.jsx diff --git a/src/users/UserPage.jsx b/src/users/UserPage.jsx index 2166e55a5..85dab2bc4 100644 --- a/src/users/UserPage.jsx +++ b/src/users/UserPage.jsx @@ -12,6 +12,7 @@ import { getAllUserData } from './data/api'; import UserSearch from './UserSearch'; import LearnerInformation from './LearnerInformation'; import { LEARNER_INFO_TAB, TAB_PATH_MAP } from '../SupportToolsTab/constants'; +import CancelRetirement from './account-actions/CancelRetirement'; // Supports urls such as /users/?username={username}, /users/?email={email} and /users/?lms_user_id={lms_user_id} export default function UserPage({ location }) { @@ -60,6 +61,9 @@ export default function UserPage({ location }) { function processSearchResult(searchValue, result) { if (result.errors.length > 0) { result.errors.forEach((error) => add(error)); + if (result.retirementStatus?.canCancelRetirement) { + setData(result.retirementStatus); + } history.replace(`${TAB_PATH_MAP['learner-information']}`); document.title = 'Support Tools | edX'; } else { @@ -159,6 +163,12 @@ export default function UserPage({ location }) { changeHandler={handleUserSummaryChange} /> )} + {!loading && data.canCancelRetirement && ( + + )} ); } diff --git a/src/users/account-actions/CancelRetirement.jsx b/src/users/account-actions/CancelRetirement.jsx new file mode 100644 index 000000000..ade9ffae6 --- /dev/null +++ b/src/users/account-actions/CancelRetirement.jsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Modal, Button, Alert } from '@edx/paragon'; +import { postCancelRetirement } from '../data/api'; + +export default function CancelRetirement({ + retirementId, + changeHandler, +}) { + const [cancelRetirementModalIsOpen, setCancelRetirementModalIsOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const cancelRetirement = async () => { + const resp = await postCancelRetirement(retirementId); + if (resp.errors) { + setErrorMessage(resp.errors[0].text || 'Something went wrong. Please try again later!'); + } else { + changeHandler(); + } + }; + + const closeCancelRetirementModal = () => { + setCancelRetirementModalIsOpen(false); + setErrorMessage(null); + }; + + const modalBody = ( + errorMessage ? {errorMessage} + : ( +
+ + + +
+ ) + ); + + return ( +
+ + + ) + : ( + + ), + ]} + onClose={closeCancelRetirementModal} + dialogClassName="modal-lg modal-dialog-centered justify-content-center" + title="Cancel Retirement" + body={modalBody} + /> +
+ ); +} + +CancelRetirement.propTypes = { + retirementId: PropTypes.number.isRequired, + changeHandler: PropTypes.func.isRequired, +}; diff --git a/src/users/account-actions/CancelRetirement.test.jsx b/src/users/account-actions/CancelRetirement.test.jsx new file mode 100644 index 000000000..26a2dd678 --- /dev/null +++ b/src/users/account-actions/CancelRetirement.test.jsx @@ -0,0 +1,120 @@ +import { mount } from 'enzyme'; +import React from 'react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import * as api from '../data/api'; +import CancelRetirement from './CancelRetirement'; +import { waitForComponentToPaint } from '../../setupTest'; + +const CancelRetirementWrapper = (props) => ( + + + +); + +describe('Cancel Retirement Component Tests', () => { + let wrapper; + const changeHandler = jest.fn(() => { }); + + beforeEach(() => { + const data = { + retirement_id: 1, + changeHandler, + }; + wrapper = mount(); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it('Cancel Retirement button for a User', () => { + const cancelRetirementButton = wrapper.find('#cancel-retirement').hostNodes(); + expect(cancelRetirementButton.text()).toEqual('Cancel Retirement'); + }); + + it('Cancel Retirement Modal', async () => { + const mockApiCall = jest.spyOn(api, 'postCancelRetirement').mockImplementationOnce(() => Promise.resolve({})); + const cancelRetirementButton = wrapper.find('#cancel-retirement').hostNodes(); + let cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + + expect(cancelRetirementModal.prop('open')).toEqual(false); + expect(cancelRetirementButton.text()).toEqual('Cancel Retirement'); + + cancelRetirementButton.simulate('click'); + cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + + expect(cancelRetirementModal.prop('open')).toEqual(true); + expect(cancelRetirementModal.prop('title')).toEqual('Cancel Retirement'); + const confirmAlert = cancelRetirementModal.find('.alert-warning'); + expect(confirmAlert.text()).toEqual('This will cancel retirement for the requested user. Do you wish to proceed?'); + + cancelRetirementModal.find('button.btn-danger').hostNodes().simulate('click'); + await waitForComponentToPaint(wrapper); + expect(changeHandler).toHaveBeenCalled(); + cancelRetirementModal.find('button.btn-link').simulate('click'); + cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + expect(cancelRetirementModal.prop('open')).toEqual(false); + + mockApiCall.mockRestore(); + }); + + it('Display Error on Cancel RetirementModal', async () => { + const cancelRetirementErrors = { + errors: [ + { + code: null, + dismissible: true, + text: 'Retirement does not exist!', + type: 'error', + topic: 'cancelRetirement', + }, + ], + }; + const mockApiCall = jest.spyOn(api, 'postCancelRetirement').mockImplementationOnce(() => Promise.resolve(cancelRetirementErrors)); + const cancelRetirementButton = wrapper.find('#cancel-retirement').hostNodes(); + cancelRetirementButton.simulate('click'); + let cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + expect(cancelRetirementModal.prop('open')).toEqual(true); + const confirmAlert = cancelRetirementModal.find('.alert-warning'); + expect(confirmAlert.text()).toEqual( + 'This will cancel retirement for the requested user. Do you wish to proceed?', + ); + + cancelRetirementModal.find('button.btn-danger').hostNodes().simulate('click'); + await waitForComponentToPaint(wrapper); + cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + const errorAlert = cancelRetirementModal.find('.alert-danger'); + expect(errorAlert.text()).toEqual('Retirement does not exist!'); + + cancelRetirementModal.find('button.btn-link').simulate('click'); + cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + expect(cancelRetirementModal.prop('open')).toEqual(false); + mockApiCall.mockRestore(); + }); + + it('Display Unknown Error on Cancel Retirement Modal', async () => { + const cancelRetirementErrors = { + errors: [ + { + code: null, + dismissible: true, + text: null, + type: 'error', + topic: 'cancelRetirement', + }, + ], + }; + const mockApiCall = jest.spyOn(api, 'postCancelRetirement').mockImplementationOnce(() => Promise.resolve(cancelRetirementErrors)); + const cancelRetirementButton = wrapper.find('#cancel-retirement').hostNodes(); + cancelRetirementButton.simulate('click'); + let cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + cancelRetirementModal.find('button.btn-danger').hostNodes().simulate('click'); + await waitForComponentToPaint(wrapper); + cancelRetirementModal = wrapper.find('Modal#user-account-cancel-retirement'); + const errorAlert = cancelRetirementModal.find('.alert-danger'); + expect(errorAlert.text()).toEqual( + 'Something went wrong. Please try again later!', + ); + mockApiCall.mockRestore(); + }); +}); diff --git a/src/users/data/api.js b/src/users/data/api.js index 211e807ae..70f1aa0a5 100644 --- a/src/users/data/api.js +++ b/src/users/data/api.js @@ -351,11 +351,20 @@ export async function getOnboardingStatus(username) { export async function getAllUserData(userIdentifier) { const errors = []; let user = null; + let retirementStatus = null; + let errorResponse = null; try { user = await getUser(userIdentifier); } catch (error) { if (error.userError) { errors.push(error.userError); + errorResponse = JSON.parse(error.customAttributes.httpErrorResponseData); + if (errorResponse?.can_cancel_retirement) { + retirementStatus = { + canCancelRetirement: errorResponse.can_cancel_retirement, + retirementId: errorResponse.retirement_id, + }; + } } else { throw error; } @@ -367,6 +376,7 @@ export async function getAllUserData(userIdentifier) { return { errors, user, + retirementStatus, }; } @@ -571,6 +581,27 @@ export async function postResetPassword(email) { } } +export async function postCancelRetirement(retirementId) { + try { + const { data } = await getAuthenticatedHttpClient().post( + AppUrls.CancelRetirementUrl(), `retirement_id=${retirementId}`, + ); + return data; + } catch (error) { + return { + errors: [ + { + code: null, + dismissible: true, + text: error.message, + type: 'error', + topic: 'cancelRetirement', + }, + ], + }; + } +} + export async function getCertificate(username, courseKey) { try { const { data } = await getAuthenticatedHttpClient().get( diff --git a/src/users/data/api.test.js b/src/users/data/api.test.js index 68c710412..9e6e53211 100644 --- a/src/users/data/api.test.js +++ b/src/users/data/api.test.js @@ -520,9 +520,37 @@ describe('API', () => { const response = await api.getAllUserData(testUsername); expect(response).toEqual({ errors: [], + retirementStatus: null, user: { ...successDictResponse, passwordStatus: {} }, }); }); + + it('Retired User Data Retrieval', async () => { + const UserApiResponse = { + can_cancel_retirement: true, + retirement_id: 1, + error_msg: 'This email is associated to a retired account.', + }; + const expectedError = [{ + code: null, + dismissible: true, + text: 'This email is associated to a retired account.', + topic: 'general', + type: 'error', + }]; + const retirementStatus = { + canCancelRetirement: true, + retirementId: 1, + }; + mockAdapter.onGet(`${userAccountApiBaseUrl}?email=${encodeURIComponent(testEmail)}`).reply(() => throwError(404, UserApiResponse)); + + const response = await api.getAllUserData(testEmail); + expect(response).toEqual({ + errors: expectedError, + retirementStatus, + user: null, + }); + }); }); describe('Toggle Password Status', () => { @@ -542,13 +570,39 @@ describe('API', () => { const resetPasswordApiUrl = `${getConfig().LMS_BASE_URL}/account/password`; it('Reset Password Response', async () => { - const expectedResponse = { }; + const expectedResponse = {}; mockAdapter.onPost(resetPasswordApiUrl, `email_from_support_tools=${testEmail}`).reply(200, expectedResponse); const response = await api.postResetPassword(testEmail); expect(response).toEqual(expectedResponse); }); }); + describe('Cancel Retirement', () => { + const CancelRetirementUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/cancel_retirement/`; + + it('Successful Cancel Retirement Response', async () => { + const expectedResponse = {}; + mockAdapter.onPost(CancelRetirementUrl, 'retirement_id=3').reply(200, expectedResponse); + const response = await api.postCancelRetirement(3); + expect(response).toEqual(expectedResponse); + }); + + it('Unsuccessful Cancel Retirement Response', async () => { + const error = new Error(); + error.message = 'Retirement does not exist!'; + const expectedResponse = { + code: null, + dismissible: true, + text: 'Retirement does not exist!', + type: 'error', + topic: 'cancelRetirement', + }; + mockAdapter.onPost(CancelRetirementUrl, 'retirement_id=3').reply(() => { throw error; }); + const response = await api.postCancelRetirement(3); + expect(...response.errors).toEqual(expectedResponse); + }); + }); + describe('Get Course Data', () => { const courseUUID = 'uuid'; @@ -803,7 +857,7 @@ describe('API', () => { code: null, dismissible: true, text: - 'User already enrolled', + 'User already enrolled', type: 'danger', topic: 'enrollments', }; diff --git a/src/users/data/urls.js b/src/users/data/urls.js index 7072b769f..0297fffda 100644 --- a/src/users/data/urls.js +++ b/src/users/data/urls.js @@ -79,6 +79,10 @@ export const getResetPasswordUrl = () => `${ LMS_BASE_URL }/account/password`; +export const CancelRetirementUrl = () => `${ + LMS_BASE_URL +}/api/user/v1/accounts/cancel_retirement/`; + export const getAccountActivationUrl = (activationKey) => `${ LMS_BASE_URL }/activate/${activationKey}`;