Skip to content

Commit

Permalink
feat: unlink the enterprise learner in non blocking manner
Browse files Browse the repository at this point in the history
  • Loading branch information
jajjibhai008 committed Oct 30, 2024
1 parent 96970cf commit a69d684
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 17 deletions.
15 changes: 15 additions & 0 deletions src/components/app/data/services/enterpriseCustomerUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,18 @@ export async function updateUserCsodParams({ data }) {
const url = `${getConfig().LMS_BASE_URL}/integrated_channels/api/v1/cornerstone/save-learner-information`;
return getAuthenticatedHttpClient().post(url, data);
}

/**
* Helper function to unlink an enterprise customer user by making a POST API request.
* @param {string} enterpriseCustomerUserUUID - The UUID of the enterprise customer user to be unlinked.
* @param {string} userEmail - The email of the user that should be unlinked.
* @returns {Promise} - A promise that resolves when the user is successfully unlinked from the enterprise customer.
*/
export async function postUnlinkUserFromEnterprise(enterpriseCustomerUserUUID, userEmail) {
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-customer/${enterpriseCustomerUserUUID}/unlink_users/`;
try {
await getAuthenticatedHttpClient().post(url, { user_emails: [userEmail], is_relinkable: true });
} catch (error) {
logError(error);

Check warning on line 219 in src/components/app/data/services/enterpriseCustomerUser.js

View check run for this annotation

Codecov / codecov/patch

src/components/app/data/services/enterpriseCustomerUser.js#L219

Added line #L219 was not covered by tests
}
}
20 changes: 20 additions & 0 deletions src/components/app/data/services/enterpriseCustomerUser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
fetchInProgressPathways,
fetchLearnerProgramsList,
postLinkEnterpriseLearner,
postUnlinkUserFromEnterprise,
updateUserActiveEnterprise,
updateUserCsodParams,
} from './enterpriseCustomerUser';
Expand Down Expand Up @@ -309,3 +310,22 @@ describe('fetchInProgressPathways', () => {
expect(response.status).toEqual(200);
});
});
describe('postUnlinkUserFromEnterprise', () => {
const mockEnterpriseCustomerUserUUID = 'test-enterprise-customer-user-uuid';
const mockUserEmail = '[email protected]';
const UNLINK_USER_ENDPOINT = `${APP_CONFIG.LMS_BASE_URL}/enterprise/api/v1/enterprise-customer/${mockEnterpriseCustomerUserUUID}/unlink_users/`;

beforeEach(() => {
jest.clearAllMocks();
axiosMock.reset();
});

it('passes correct POST body', async () => {
axiosMock.onPost(UNLINK_USER_ENDPOINT).reply(200);
await postUnlinkUserFromEnterprise(mockEnterpriseCustomerUserUUID, mockUserEmail);
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({
user_emails: [mockUserEmail],
is_relinkable: true,
}));
});
});
36 changes: 31 additions & 5 deletions src/components/expired-subscription-modal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
import {
useToggle, AlertModal, Button, ActionRow,
useToggle, AlertModal, ActionRow, StatefulButton,
} from '@openedx/paragon';
import DOMPurify from 'dompurify';
import { useSubscriptions } from '../app/data';
import { useContext, useState } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import { postUnlinkUserFromEnterprise, useEnterpriseCustomer, useSubscriptions } from '../app/data';

const ExpiredSubscriptionModal = () => {
const [buttonState, setButtonState] = useState('default');
const { data: { customerAgreement } } = useSubscriptions();
const {
authenticatedUser: { email: userEmail },
} = useContext(AppContext);
const { data: enterpriseCustomer } = useEnterpriseCustomer();

const [isOpen] = useToggle(true);
if (!customerAgreement?.hasCustomLicenseExpirationMessaging) {
return null;
}
const onClickHandler = async (e) => {
e.preventDefault();
setButtonState('pending');

await postUnlinkUserFromEnterprise(enterpriseCustomer.uuid, userEmail);

// Redirect immediately
window.location.href = customerAgreement.urlForButtonInModal;
setButtonState('default');
};
const props = {
labels: {
default: customerAgreement.buttonLabelInModal,
},
variant: 'primary',
};
return (
<AlertModal
title={<h3 className="mb-2">{customerAgreement.modalHeaderText}</h3>}
isOpen={isOpen}
isBlocking
footerNode={(
<ActionRow>
<Button href={customerAgreement.urlForButtonInModal}>
{customerAgreement.buttonLabelInModal}
</Button>
<StatefulButton
state={buttonState}
onClick={onClickHandler}
{...props}
/>
</ActionRow>
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import { AppContext } from '@edx/frontend-platform/react';
import ExpiredSubscriptionModal from '../index';
import { useSubscriptions } from '../../app/data';
import { postUnlinkUserFromEnterprise, useEnterpriseCustomer, useSubscriptions } from '../../app/data';
import { renderWithRouter } from '../../../utils/tests';
import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';

jest.mock('../../app/data', () => ({
...jest.requireActual('../../app/data'),
useSubscriptions: jest.fn(),
useEnterpriseCustomer: jest.fn(),
postUnlinkUserFromEnterprise: jest.fn(),
}));
const mockAuthenticatedUser = authenticatedUserFactory();
const mockEnterpriseCustomer = enterpriseCustomerFactory();

const defaultAppContextValue = { authenticatedUser: mockAuthenticatedUser };
const ExpiredSubscriptionModalWrapper = ({ children, appContextValue = defaultAppContextValue }) => (
<AppContext.Provider value={appContextValue}>
<ExpiredSubscriptionModal>
{children}
</ExpiredSubscriptionModal>
</AppContext.Provider>
);

describe('<ExpiredSubscriptionModal />', () => {
beforeEach(() => {
Expand All @@ -23,10 +38,11 @@ describe('<ExpiredSubscriptionModal />', () => {
},
},
});
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
});

test('does not renderwithrouter if `hasCustomLicenseExpirationMessaging` is false', () => {
const { container } = renderWithRouter(<ExpiredSubscriptionModal />);
const { container } = renderWithRouter(<ExpiredSubscriptionModalWrapper />);
expect(container).toBeEmptyDOMElement();
});

Expand All @@ -36,22 +52,22 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessaging: true,
modalHeaderText: 'Expired Subscription',
buttonLabelInModal: 'Continue Learning',
buttonLabelInModal: 'Continue learning',
expiredSubscriptionModalMessaging: '<p>Your subscription has expired.</p>',
urlForButtonInModal: '/renew',
},
},
});

renderWithRouter(<ExpiredSubscriptionModal />);
renderWithRouter(<ExpiredSubscriptionModalWrapper />);

expect(screen.getByText('Expired Subscription')).toBeInTheDocument();
expect(screen.getByText('Continue Learning')).toBeInTheDocument();
expect(screen.getByText('Continue learning')).toBeInTheDocument();
});

test('does not renderwithrouter modal if no customer agreement data is present', () => {
useSubscriptions.mockReturnValue({ data: { customerAgreement: null } });
const { container } = renderWithRouter(<ExpiredSubscriptionModal />);
const { container } = renderWithRouter(<ExpiredSubscriptionModalWrapper />);
expect(container).toBeEmptyDOMElement();
});

Expand All @@ -61,40 +77,62 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessaging: true,
modalHeaderText: 'Expired Subscription',
buttonLabelInModal: 'Continue Learning',
buttonLabelInModal: 'Continue learning',
expiredSubscriptionModalMessaging: '<p>Your subscription has expired.</p>',
urlForButtonInModal: '/renew',
},
},
});

renderWithRouter(<ExpiredSubscriptionModal />);
renderWithRouter(<ExpiredSubscriptionModalWrapper />);
expect(screen.queryByLabelText(/close/i)).not.toBeInTheDocument();
});
test('clicks on Continue Learning button', () => {
test('clicks on Continue learning button', () => {
// Mock useSubscriptions
useSubscriptions.mockReturnValue({
data: {
customerAgreement: {
hasCustomLicenseExpirationMessaging: true,
modalHeaderText: 'Expired Subscription',
buttonLabelInModal: 'Continue Learning',
buttonLabelInModal: 'Continue learning',
expiredSubscriptionModalMessaging: '<p>Your subscription has expired.</p>',
urlForButtonInModal: 'https://example.com',
},
},
});

// Render the component
renderWithRouter(<ExpiredSubscriptionModal />);
renderWithRouter(<ExpiredSubscriptionModalWrapper />);

// Find the Continue Learning button
const continueButton = screen.getByText('Continue Learning');
const continueButton = screen.getByText('Continue learning');

// Simulate a click on the button
userEvent.click(continueButton);

// Check that the button was rendered and clicked
expect(continueButton).toBeInTheDocument();
});
test('calls postUnlinkUserFromEnterprise and redirects on button click', async () => {
useSubscriptions.mockReturnValue({
data: {
customerAgreement: {
hasCustomLicenseExpirationMessaging: true,
modalHeaderText: 'Expired Subscription',
buttonLabelInModal: 'Continue learning',
expiredSubscriptionModalMessaging: '<p>Your subscription has expired.</p>',
urlForButtonInModal: 'https://example.com',
},
},
});
postUnlinkUserFromEnterprise.mockResolvedValueOnce();

renderWithRouter(<ExpiredSubscriptionModalWrapper />);

const continueButton = screen.getByText('Continue learning');

userEvent.click(continueButton);

expect(postUnlinkUserFromEnterprise).toHaveBeenCalledWith(mockEnterpriseCustomer.uuid, mockAuthenticatedUser.email);
});
});

0 comments on commit a69d684

Please sign in to comment.