Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unlink the enterprise learner in non blocking manner #1215

Merged
merged 1 commit into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 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,17 @@
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);

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L218 was not covered by tests
}
}
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(undefined);
});
});
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);
});
});
Loading