From 7cfe30a1461283cded729deb6716511a160761c7 Mon Sep 17 00:00:00 2001 From: Ejaz Ahmad <86868918+jajjibhai008@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:37:03 +0500 Subject: [PATCH] feat: unlink the enterprise learner in non blocking manner (#1215) --- .../data/services/enterpriseCustomerUser.js | 14 ++++ .../services/enterpriseCustomerUser.test.js | 16 ++++ .../expired-subscription-modal/index.jsx | 32 ++++++-- .../tests/ExpiredSubscriptionModal.test.jsx | 76 +++++++++++++++---- 4 files changed, 117 insertions(+), 21 deletions(-) diff --git a/src/components/app/data/services/enterpriseCustomerUser.js b/src/components/app/data/services/enterpriseCustomerUser.js index 70608ae4d3..3c629fe54a 100644 --- a/src/components/app/data/services/enterpriseCustomerUser.js +++ b/src/components/app/data/services/enterpriseCustomerUser.js @@ -204,3 +204,17 @@ 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. + * @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); + } +} diff --git a/src/components/app/data/services/enterpriseCustomerUser.test.js b/src/components/app/data/services/enterpriseCustomerUser.test.js index 88f32ae6e3..9d80bcd34f 100644 --- a/src/components/app/data/services/enterpriseCustomerUser.test.js +++ b/src/components/app/data/services/enterpriseCustomerUser.test.js @@ -8,6 +8,7 @@ import { fetchInProgressPathways, fetchLearnerProgramsList, postLinkEnterpriseLearner, + postUnlinkUserFromEnterprise, updateUserActiveEnterprise, updateUserCsodParams, } from './enterpriseCustomerUser'; @@ -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(undefined); + }); +}); diff --git a/src/components/expired-subscription-modal/index.jsx b/src/components/expired-subscription-modal/index.jsx index 86c38d5e50..c92eef80f8 100644 --- a/src/components/expired-subscription-modal/index.jsx +++ b/src/components/expired-subscription-modal/index.jsx @@ -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 @@ -16,6 +20,22 @@ 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 ( {customerAgreement.modalHeaderTextV2}} @@ -23,9 +43,11 @@ const ExpiredSubscriptionModal = () => { isBlocking footerNode={( - + )} > diff --git a/src/components/expired-subscription-modal/tests/ExpiredSubscriptionModal.test.jsx b/src/components/expired-subscription-modal/tests/ExpiredSubscriptionModal.test.jsx index 2f43005532..987683010a 100644 --- a/src/components/expired-subscription-modal/tests/ExpiredSubscriptionModal.test.jsx +++ b/src/components/expired-subscription-modal/tests/ExpiredSubscriptionModal.test.jsx @@ -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 }) => ( + + + {children} + + +); describe('', () => { beforeEach(() => { @@ -29,10 +44,11 @@ describe('', () => { }, }, }); + useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer }); }); test('does not renderwithrouter if `hasCustomLicenseExpirationMessagingV2` is false', () => { - const { container } = renderWithRouter(); + const { container } = renderWithRouter(); expect(container).toBeEmptyDOMElement(); }); @@ -42,7 +58,7 @@ describe('', () => { customerAgreement: { hasCustomLicenseExpirationMessagingV2: true, modalHeaderTextV2: 'Expired Subscription', - buttonLabelInModalV2: 'Continue Learning', + buttonLabelInModalV2: 'Continue learning', expiredSubscriptionModalMessagingV2: '

Your subscription has expired.

', urlForButtonInModalV2: '/renew', }, @@ -55,7 +71,7 @@ describe('', () => { }, }); - const { container } = renderWithRouter(); + const { container } = renderWithRouter(); expect(container).toBeEmptyDOMElement(); }); @@ -65,7 +81,7 @@ describe('', () => { customerAgreement: { hasCustomLicenseExpirationMessagingV2: true, modalHeaderTextV2: 'Expired Subscription', - buttonLabelInModalV2: 'Continue Learning', + buttonLabelInModalV2: 'Continue learning', expiredSubscriptionModalMessagingV2: '

Your subscription has expired.

', urlForButtonInModalV2: '/renew', }, @@ -74,7 +90,7 @@ describe('', () => { }, }); - const { container } = renderWithRouter(); + const { container } = renderWithRouter(); expect(container).toBeEmptyDOMElement(); }); @@ -84,7 +100,7 @@ describe('', () => { customerAgreement: { hasCustomLicenseExpirationMessagingV2: true, modalHeaderTextV2: 'Expired Subscription', - buttonLabelInModalV2: 'Continue Learning', + buttonLabelInModalV2: 'Continue learning', expiredSubscriptionModalMessagingV2: '

Your subscription has expired.

', urlForButtonInModalV2: '/renew', }, @@ -97,15 +113,15 @@ describe('', () => { }, }); - renderWithRouter(); + renderWithRouter(); 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(); + const { container } = renderWithRouter(); expect(container).toBeEmptyDOMElement(); }); @@ -115,7 +131,7 @@ describe('', () => { customerAgreement: { hasCustomLicenseExpirationMessagingV2: true, modalHeaderTextV2: 'Expired Subscription', - buttonLabelInModalV2: 'Continue Learning', + buttonLabelInModalV2: 'Continue learning', expiredSubscriptionModalMessagingV2: '

Your subscription has expired.

', urlForButtonInModalV2: '/renew', }, @@ -128,17 +144,17 @@ describe('', () => { }, }); - renderWithRouter(); + renderWithRouter(); 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: '

Your subscription has expired.

', urlForButtonInModalV2: 'https://example.com', }, @@ -152,10 +168,10 @@ describe('', () => { }); // Render the component - renderWithRouter(); + renderWithRouter(); // 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); @@ -163,4 +179,32 @@ describe('', () => { // 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: '

Your subscription has expired.

', + urlForButtonInModalV2: 'https://example.com', + }, + subscriptionLicense: { + uuid: '123', + }, + subscriptionPlan: { + isCurrent: false, + }, + }, + }); + postUnlinkUserFromEnterprise.mockResolvedValueOnce(); + + renderWithRouter(); + + const continueButton = screen.getByText('Continue learning'); + + userEvent.click(continueButton); + + expect(postUnlinkUserFromEnterprise).toHaveBeenCalledWith(mockEnterpriseCustomer.uuid); + }); });