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 Nov 1, 2024
1 parent 66a5d34 commit 8eb2c3a
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 21 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) {
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-customer/${enterpriseCustomerUserUUID}/unlink_self/`;
try {
await getAuthenticatedHttpClient().post(url);
} catch (error) {
logError(error);
}
}
16 changes: 16 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,18 @@ describe('fetchInProgressPathways', () => {
expect(response.status).toEqual(200);
});
});
describe('postUnlinkUserFromEnterprise', () => {
const mockEnterpriseCustomerUserUUID = 'test-enterprise-customer-user-uuid';
const UNLINK_USER_ENDPOINT = `${APP_CONFIG.LMS_BASE_URL}/enterprise/api/v1/enterprise-customer/${mockEnterpriseCustomerUserUUID}/unlink_self/`;

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

it('passes correct POST body', async () => {
axiosMock.onPost(UNLINK_USER_ENDPOINT).reply(200);
await postUnlinkUserFromEnterprise(mockEnterpriseCustomerUserUUID);
expect(axiosMock.history.post[0].data).toEqual(200);
});
});
32 changes: 27 additions & 5 deletions src/components/expired-subscription-modal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
useToggle, AlertModal, Button, ActionRow,
useToggle, AlertModal, ActionRow, StatefulButton,
} from '@openedx/paragon';
import DOMPurify from 'dompurify';
import { useSubscriptions } from '../app/data';
import { useState } from 'react';
import { postUnlinkUserFromEnterprise, useEnterpriseCustomer, useSubscriptions } from '../app/data';

const ExpiredSubscriptionModal = () => {
const [buttonState, setButtonState] = useState('default');
const { data: { customerAgreement, subscriptionLicense, subscriptionPlan } } = useSubscriptions();
const { data: enterpriseCustomer } = useEnterpriseCustomer();

const [isOpen] = useToggle(true);
const displaySubscriptionExpirationModal = (
customerAgreement?.hasCustomLicenseExpirationMessagingV2
Expand All @@ -16,16 +20,34 @@ const ExpiredSubscriptionModal = () => {
return null;
}

const onClickHandler = async (e) => {
e.preventDefault();
setButtonState('pending');

await postUnlinkUserFromEnterprise(enterpriseCustomer.uuid);

// Redirect immediately
window.location.href = customerAgreement.urlForButtonInModalV2;
setButtonState('default');
};
const props = {
labels: {
default: customerAgreement.buttonLabelInModalV2,
},
variant: 'primary',
};
return (
<AlertModal
title={<h3 className="mb-2">{customerAgreement.modalHeaderTextV2}</h3>}
isOpen={isOpen}
isBlocking
footerNode={(
<ActionRow>
<Button href={customerAgreement.urlForButtonInModalV2}>
{customerAgreement.buttonLabelInModalV2}
</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 @@ -29,10 +44,11 @@ describe('<ExpiredSubscriptionModal />', () => {
},
},
});
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
});

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

Expand All @@ -42,7 +58,7 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue Learning',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: '/renew',
},
Expand All @@ -55,7 +71,7 @@ describe('<ExpiredSubscriptionModal />', () => {
},
});

const { container } = renderWithRouter(<ExpiredSubscriptionModal />);
const { container } = renderWithRouter(<ExpiredSubscriptionModalWrapper />);
expect(container).toBeEmptyDOMElement();
});

Expand All @@ -65,7 +81,7 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue Learning',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: '/renew',
},
Expand All @@ -74,7 +90,7 @@ describe('<ExpiredSubscriptionModal />', () => {
},
});

const { container } = renderWithRouter(<ExpiredSubscriptionModal />);
const { container } = renderWithRouter(<ExpiredSubscriptionModalWrapper />);
expect(container).toBeEmptyDOMElement();
});

Expand All @@ -84,7 +100,7 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue Learning',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: '/renew',
},
Expand All @@ -97,15 +113,15 @@ describe('<ExpiredSubscriptionModal />', () => {
},
});

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 @@ -115,7 +131,7 @@ describe('<ExpiredSubscriptionModal />', () => {
customerAgreement: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue Learning',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: '/renew',
},
Expand All @@ -128,17 +144,17 @@ describe('<ExpiredSubscriptionModal />', () => {
},
});

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: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue Learning',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: 'https://example.com',
},
Expand All @@ -152,15 +168,43 @@ describe('<ExpiredSubscriptionModal />', () => {
});

// 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: {
hasCustomLicenseExpirationMessagingV2: true,
modalHeaderTextV2: 'Expired Subscription',
buttonLabelInModalV2: 'Continue learning',
expiredSubscriptionModalMessagingV2: '<p>Your subscription has expired.</p>',
urlForButtonInModalV2: 'https://example.com',
},
subscriptionLicense: {
uuid: '123',
},
subscriptionPlan: {
isCurrent: false,
},
},
});
postUnlinkUserFromEnterprise.mockResolvedValueOnce();

renderWithRouter(<ExpiredSubscriptionModalWrapper />);

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

userEvent.click(continueButton);

expect(postUnlinkUserFromEnterprise).toHaveBeenCalledWith(mockEnterpriseCustomer.uuid);
});
});

0 comments on commit 8eb2c3a

Please sign in to comment.