Skip to content

Commit f9806d0

Browse files
feat: added support to check if active enterprise is same as EnterpriseCourseEnrollment object (#967)
1 parent a7b584c commit f9806d0

File tree

12 files changed

+266
-0
lines changed

12 files changed

+266
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
3+
import PropTypes from 'prop-types';
4+
import { Alert, Hyperlink } from '@edx/paragon';
5+
import { WarningFilled } from '@edx/paragon/icons';
6+
7+
import { getConfig } from '@edx/frontend-platform';
8+
import genericMessages from './messages';
9+
10+
function ActiveEnterpriseAlert({ intl, payload }) {
11+
const { text } = payload;
12+
const changeActiveEnterprise = (
13+
<Hyperlink
14+
style={{ textDecoration: 'underline' }}
15+
destination={
16+
`${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=${encodeURIComponent(global.location.href)}`
17+
}
18+
>
19+
{intl.formatMessage(genericMessages.changeActiveEnterpriseLowercase)}
20+
</Hyperlink>
21+
);
22+
23+
return (
24+
<Alert variant="warning" icon={WarningFilled}>
25+
{text}
26+
<FormattedMessage
27+
id="learning.activeEnterprise.alert"
28+
description="Prompts the user to log-in with the correct enterprise to access the course content."
29+
defaultMessage=" {changeActiveEnterprise}."
30+
values={{
31+
changeActiveEnterprise,
32+
}}
33+
/>
34+
</Alert>
35+
);
36+
}
37+
38+
ActiveEnterpriseAlert.propTypes = {
39+
intl: intlShape.isRequired,
40+
payload: PropTypes.shape({
41+
text: PropTypes.string,
42+
}).isRequired,
43+
};
44+
45+
export default injectIntl(ActiveEnterpriseAlert);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { getConfig } from '@edx/frontend-platform';
3+
import {
4+
initializeTestStore, render, screen,
5+
} from '../../setupTest';
6+
import ActiveEnterpriseAlert from './ActiveEnterpriseAlert';
7+
8+
describe('ActiveEnterpriseAlert', () => {
9+
const mockData = {
10+
payload: {
11+
text: 'test message',
12+
},
13+
};
14+
beforeAll(async () => {
15+
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
16+
});
17+
18+
it('Shows alert message and links', () => {
19+
render(<ActiveEnterpriseAlert {...mockData} />);
20+
expect(screen.getByRole('alert')).toBeInTheDocument();
21+
expect(screen.getByText('test message')).toBeInTheDocument();
22+
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute(
23+
'href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2F`,
24+
);
25+
});
26+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React, { useMemo } from 'react';
2+
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
3+
import { useModel } from '../../generic/model-store';
4+
5+
const ActiveEnterpriseAlert = React.lazy(() => import('./ActiveEnterpriseAlert'));
6+
7+
export default function useActiveEnterpriseAlert(courseId) {
8+
const { courseAccess } = useModel('courseHomeMeta', courseId);
9+
/**
10+
* This alert should render if
11+
* 1. course access code is incorrect_active_enterprise
12+
*/
13+
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
14+
15+
const payload = {
16+
text: courseAccess && courseAccess.userMessage,
17+
courseId,
18+
};
19+
useAlert(isVisible, {
20+
code: 'clientActiveEnterpriseAlert',
21+
topic: 'outline',
22+
dismissible: false,
23+
type: ALERT_TYPES.ERROR,
24+
payload: useMemo(() => payload, Object.values(payload).sort()),
25+
});
26+
27+
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
28+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import useActiveEnterpriseAlert from './hooks';
2+
3+
export default useActiveEnterpriseAlert;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
changeActiveEnterpriseLowercase: {
5+
id: 'learning.activeEnterprise.change.alert',
6+
defaultMessage: 'change enterprise now',
7+
description: 'Text in a link, prompting the user to change active enterprise. Used in learning.activeEnterprise.change.alert"',
8+
},
9+
});
10+
11+
export default messages;

src/courseware/CoursewareContainer.test.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,13 @@ describe('CoursewareContainer', () => {
483483
expect(global.location.href).toEqual('http://localhost/redirect/consent?consentPath=data_sharing_consent_url');
484484
});
485485

486+
it('should go to access denied page for a incorrect_active_enterprise error code', async () => {
487+
const { courseMetadata } = setUpWithDeniedStatus('incorrect_active_enterprise');
488+
await loadContainer();
489+
490+
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/access-denied`);
491+
});
492+
486493
it('should go to course home for an authentication_required error code', async () => {
487494
const { courseMetadata } = setUpWithDeniedStatus('authentication_required');
488495
await loadContainer();

src/courseware/CoursewareRedirectLandingPage.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export default () => {
4040
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
4141
}}
4242
/>
43+
<PageRoute
44+
path={`${path}/home/:courseId`}
45+
render={({ match }) => {
46+
global.location.assign(`/course/${match.params.courseId}/home`);
47+
}}
48+
/>
4349
</Switch>
4450
</div>
4551
);

src/courseware/CoursewareRedirectLandingPage.test.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,18 @@ describe('CoursewareRedirectLandingPage', () => {
3434

3535
expect(redirectUrl).toHaveBeenCalledWith('http://localhost:18000/grant_data_sharing_consent');
3636
});
37+
38+
it('Redirects to correct consent URL', () => {
39+
const history = createMemoryHistory({
40+
initialEntries: ['/redirect/home/course-v1:edX+DemoX+Demo_Course'],
41+
});
42+
43+
render(
44+
<Router history={history}>
45+
<CoursewareRedirectLandingPage />
46+
</Router>,
47+
);
48+
49+
expect(redirectUrl).toHaveBeenCalledWith('/course/course-v1:edX+DemoX+Demo_Course/home');
50+
});
3751
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useEffect } from 'react';
2+
import { LearningHeader as Header } from '@edx/frontend-component-header';
3+
import Footer from '@edx/frontend-component-footer';
4+
import { useParams } from 'react-router-dom';
5+
import { useDispatch, useSelector } from 'react-redux';
6+
import { Redirect } from 'react-router';
7+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
8+
import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert';
9+
import { AlertList } from './user-messages';
10+
import { fetchDiscussionTab } from '../course-home/data/thunks';
11+
import { LOADED, LOADING } from '../course-home/data/slice';
12+
import PageLoading from './PageLoading';
13+
import messages from '../tab-page/messages';
14+
15+
function CourseAccessErrorPage({ intl }) {
16+
const { courseId } = useParams();
17+
18+
const dispatch = useDispatch();
19+
const activeEnterpriseAlert = useActiveEnterpriseAlert(courseId);
20+
useEffect(() => {
21+
dispatch(fetchDiscussionTab(courseId));
22+
}, [courseId]);
23+
24+
const {
25+
courseStatus,
26+
} = useSelector(state => state.courseHome);
27+
28+
if (courseStatus === LOADING) {
29+
return (
30+
<>
31+
<Header />
32+
<PageLoading
33+
srMessage={intl.formatMessage(messages.loading)}
34+
/>
35+
<Footer />
36+
</>
37+
);
38+
}
39+
if (courseStatus === LOADED) {
40+
return (<Redirect to={`/redirect/home/${courseId}`} />);
41+
}
42+
return (
43+
<>
44+
<Header />
45+
<main id="main-content" className="container my-5 text-center" data-testid="access-denied-main">
46+
<AlertList
47+
topic="outline"
48+
className="mx-5 mt-3"
49+
customAlerts={{
50+
...activeEnterpriseAlert,
51+
}}
52+
/>
53+
</main>
54+
<Footer />
55+
</>
56+
);
57+
}
58+
59+
CourseAccessErrorPage.propTypes = {
60+
intl: intlShape.isRequired,
61+
};
62+
63+
export default injectIntl(CourseAccessErrorPage);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { history } from '@edx/frontend-platform';
3+
import { Route } from 'react-router';
4+
import { initializeTestStore, render, screen } from '../setupTest';
5+
import CourseAccessErrorPage from './CourseAccessErrorPage';
6+
7+
const mockDispatch = jest.fn();
8+
let mockCourseStatus;
9+
jest.mock('react-redux', () => ({
10+
...jest.requireActual('react-redux'),
11+
useDispatch: () => mockDispatch,
12+
useSelector: () => ({ courseStatus: mockCourseStatus }),
13+
}));
14+
jest.mock('./PageLoading', () => () => <div data-testid="page-loading" />);
15+
16+
describe('CourseAccessErrorPage', () => {
17+
let courseId;
18+
let accessDeniedUrl;
19+
beforeEach(async () => {
20+
const store = await initializeTestStore({ excludeFetchSequence: true });
21+
courseId = store.getState().courseware.courseId;
22+
accessDeniedUrl = `/course/${courseId}/access-denied`;
23+
history.push(accessDeniedUrl);
24+
});
25+
26+
it('Displays loading in start on page rendering', () => {
27+
mockCourseStatus = 'loading';
28+
render(
29+
<Route path="/course/:courseId/access-denied">
30+
<CourseAccessErrorPage />
31+
</Route>,
32+
);
33+
expect(screen.getByTestId('page-loading')).toBeInTheDocument();
34+
expect(history.location.pathname).toBe(accessDeniedUrl);
35+
});
36+
37+
it('Redirect user to homepage if user has access', () => {
38+
mockCourseStatus = 'loaded';
39+
render(
40+
<Route path="/course/:courseId/access-denied">
41+
<CourseAccessErrorPage />
42+
</Route>,
43+
);
44+
expect(history.location.pathname).toBe('/redirect/home/course-v1:edX+DemoX+Demo_Course');
45+
});
46+
47+
it('For access denied it should render access denied page', () => {
48+
mockCourseStatus = 'denied';
49+
50+
render(
51+
<Route path="/course/:courseId/access-denied">
52+
<CourseAccessErrorPage />
53+
</Route>,
54+
);
55+
expect(screen.getByTestId('access-denied-main')).toBeInTheDocument();
56+
expect(history.location.pathname).toBe(accessDeniedUrl);
57+
});
58+
});

0 commit comments

Comments
 (0)