Skip to content

Commit

Permalink
Merge pull request #270 from openedx/aakbar/PROD-2521-cancel-retirement
Browse files Browse the repository at this point in the history
feat: adds cancel retirement feature
  • Loading branch information
Ali-D-Akbar authored Sep 13, 2022
2 parents c785f52 + d62195a commit 92d57ba
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 2 deletions.
10 changes: 10 additions & 0 deletions src/users/UserPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -159,6 +163,12 @@ export default function UserPage({ location }) {
changeHandler={handleUserSummaryChange}
/>
)}
{!loading && data.canCancelRetirement && (
<CancelRetirement
retirementId={data.retirementId}
changeHandler={handleUserSummaryChange}
/>
)}
</main>
);
}
Expand Down
79 changes: 79 additions & 0 deletions src/users/account-actions/CancelRetirement.jsx
Original file line number Diff line number Diff line change
@@ -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 ? <Alert variant="danger">{errorMessage}</Alert>
: (
<div>
<Alert variant="warning">
<FormattedMessage
id="supportTools.accountActions.cancelRetirement"
tagName="p"
description="Cancel Retirement instructions prompt"
defaultMessage="This will cancel retirement for the requested user. Do you wish to proceed?"
/>
</Alert>
</div>
)
);

return (
<div>
<Button
id="cancel-retirement"
variant="btn btn-danger"
onClick={() => setCancelRetirementModalIsOpen(true)}
className="mr-1 mb-2"
>Cancel Retirement
</Button>

<Modal
open={cancelRetirementModalIsOpen}
id="user-account-cancel-retirement"
buttons={[errorMessage ? (<></>)
: (
<Button
variant="danger"
onClick={cancelRetirement}
>
Confirm
</Button>
),
]}
onClose={closeCancelRetirementModal}
dialogClassName="modal-lg modal-dialog-centered justify-content-center"
title="Cancel Retirement"
body={modalBody}
/>
</div>
);
}

CancelRetirement.propTypes = {
retirementId: PropTypes.number.isRequired,
changeHandler: PropTypes.func.isRequired,
};
120 changes: 120 additions & 0 deletions src/users/account-actions/CancelRetirement.test.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<IntlProvider locale="en">
<CancelRetirement {...props} />
</IntlProvider>
);

describe('Cancel Retirement Component Tests', () => {
let wrapper;
const changeHandler = jest.fn(() => { });

beforeEach(() => {
const data = {
retirement_id: 1,
changeHandler,
};
wrapper = mount(<CancelRetirementWrapper {...data} />);
});

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();
});
});
31 changes: 31 additions & 0 deletions src/users/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -367,6 +376,7 @@ export async function getAllUserData(userIdentifier) {
return {
errors,
user,
retirementStatus,
};
}

Expand Down Expand Up @@ -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(
Expand Down
58 changes: 56 additions & 2 deletions src/users/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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';

Expand Down Expand Up @@ -803,7 +857,7 @@ describe('API', () => {
code: null,
dismissible: true,
text:
'User already enrolled',
'User already enrolled',
type: 'danger',
topic: 'enrollments',
};
Expand Down
4 changes: 4 additions & 0 deletions src/users/data/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down

0 comments on commit 92d57ba

Please sign in to comment.