Skip to content

Commit

Permalink
feat: added support to check if active enterprise is same as Enterpri…
Browse files Browse the repository at this point in the history
…seCourseEnrollment object (#967)
  • Loading branch information
HammadAhmadWaqas authored Sep 7, 2022
1 parent a7b584c commit f9806d0
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';

import { getConfig } from '@edx/frontend-platform';
import genericMessages from './messages';

function ActiveEnterpriseAlert({ intl, payload }) {
const { text } = payload;
const changeActiveEnterprise = (
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={
`${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=${encodeURIComponent(global.location.href)}`
}
>
{intl.formatMessage(genericMessages.changeActiveEnterpriseLowercase)}
</Hyperlink>
);

return (
<Alert variant="warning" icon={WarningFilled}>
{text}
<FormattedMessage
id="learning.activeEnterprise.alert"
description="Prompts the user to log-in with the correct enterprise to access the course content."
defaultMessage=" {changeActiveEnterprise}."
values={{
changeActiveEnterprise,
}}
/>
</Alert>
);
}

ActiveEnterpriseAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
text: PropTypes.string,
}).isRequired,
};

export default injectIntl(ActiveEnterpriseAlert);
26 changes: 26 additions & 0 deletions src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
initializeTestStore, render, screen,
} from '../../setupTest';
import ActiveEnterpriseAlert from './ActiveEnterpriseAlert';

describe('ActiveEnterpriseAlert', () => {
const mockData = {
payload: {
text: 'test message',
},
};
beforeAll(async () => {
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
});

it('Shows alert message and links', () => {
render(<ActiveEnterpriseAlert {...mockData} />);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('test message')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute(
'href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2F`,
);
});
});
28 changes: 28 additions & 0 deletions src/alerts/active-enteprise-alert/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useMemo } from 'react';
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
import { useModel } from '../../generic/model-store';

const ActiveEnterpriseAlert = React.lazy(() => import('./ActiveEnterpriseAlert'));

export default function useActiveEnterpriseAlert(courseId) {
const { courseAccess } = useModel('courseHomeMeta', courseId);
/**
* This alert should render if
* 1. course access code is incorrect_active_enterprise
*/
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';

const payload = {
text: courseAccess && courseAccess.userMessage,
courseId,
};
useAlert(isVisible, {
code: 'clientActiveEnterpriseAlert',
topic: 'outline',
dismissible: false,
type: ALERT_TYPES.ERROR,
payload: useMemo(() => payload, Object.values(payload).sort()),
});

return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
}
3 changes: 3 additions & 0 deletions src/alerts/active-enteprise-alert/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import useActiveEnterpriseAlert from './hooks';

export default useActiveEnterpriseAlert;
11 changes: 11 additions & 0 deletions src/alerts/active-enteprise-alert/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
changeActiveEnterpriseLowercase: {
id: 'learning.activeEnterprise.change.alert',
defaultMessage: 'change enterprise now',
description: 'Text in a link, prompting the user to change active enterprise. Used in learning.activeEnterprise.change.alert"',
},
});

export default messages;
7 changes: 7 additions & 0 deletions src/courseware/CoursewareContainer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,13 @@ describe('CoursewareContainer', () => {
expect(global.location.href).toEqual('http://localhost/redirect/consent?consentPath=data_sharing_consent_url');
});

it('should go to access denied page for a incorrect_active_enterprise error code', async () => {
const { courseMetadata } = setUpWithDeniedStatus('incorrect_active_enterprise');
await loadContainer();

expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/access-denied`);
});

it('should go to course home for an authentication_required error code', async () => {
const { courseMetadata } = setUpWithDeniedStatus('authentication_required');
await loadContainer();
Expand Down
6 changes: 6 additions & 0 deletions src/courseware/CoursewareRedirectLandingPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export default () => {
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
}}
/>
<PageRoute
path={`${path}/home/:courseId`}
render={({ match }) => {
global.location.assign(`/course/${match.params.courseId}/home`);
}}
/>
</Switch>
</div>
);
Expand Down
14 changes: 14 additions & 0 deletions src/courseware/CoursewareRedirectLandingPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@ describe('CoursewareRedirectLandingPage', () => {

expect(redirectUrl).toHaveBeenCalledWith('http://localhost:18000/grant_data_sharing_consent');
});

it('Redirects to correct consent URL', () => {
const history = createMemoryHistory({
initialEntries: ['/redirect/home/course-v1:edX+DemoX+Demo_Course'],
});

render(
<Router history={history}>
<CoursewareRedirectLandingPage />
</Router>,
);

expect(redirectUrl).toHaveBeenCalledWith('/course/course-v1:edX+DemoX+Demo_Course/home');
});
});
63 changes: 63 additions & 0 deletions src/generic/CourseAccessErrorPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useEffect } from 'react';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
import { useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { Redirect } from 'react-router';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert';
import { AlertList } from './user-messages';
import { fetchDiscussionTab } from '../course-home/data/thunks';
import { LOADED, LOADING } from '../course-home/data/slice';
import PageLoading from './PageLoading';
import messages from '../tab-page/messages';

function CourseAccessErrorPage({ intl }) {
const { courseId } = useParams();

const dispatch = useDispatch();
const activeEnterpriseAlert = useActiveEnterpriseAlert(courseId);
useEffect(() => {
dispatch(fetchDiscussionTab(courseId));
}, [courseId]);

const {
courseStatus,
} = useSelector(state => state.courseHome);

if (courseStatus === LOADING) {
return (
<>
<Header />
<PageLoading
srMessage={intl.formatMessage(messages.loading)}
/>
<Footer />
</>
);
}
if (courseStatus === LOADED) {
return (<Redirect to={`/redirect/home/${courseId}`} />);
}
return (
<>
<Header />
<main id="main-content" className="container my-5 text-center" data-testid="access-denied-main">
<AlertList
topic="outline"
className="mx-5 mt-3"
customAlerts={{
...activeEnterpriseAlert,
}}
/>
</main>
<Footer />
</>
);
}

CourseAccessErrorPage.propTypes = {
intl: intlShape.isRequired,
};

export default injectIntl(CourseAccessErrorPage);
58 changes: 58 additions & 0 deletions src/generic/CourseAccessErrorPage.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import { Route } from 'react-router';
import { initializeTestStore, render, screen } from '../setupTest';
import CourseAccessErrorPage from './CourseAccessErrorPage';

const mockDispatch = jest.fn();
let mockCourseStatus;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
useSelector: () => ({ courseStatus: mockCourseStatus }),
}));
jest.mock('./PageLoading', () => () => <div data-testid="page-loading" />);

describe('CourseAccessErrorPage', () => {
let courseId;
let accessDeniedUrl;
beforeEach(async () => {
const store = await initializeTestStore({ excludeFetchSequence: true });
courseId = store.getState().courseware.courseId;
accessDeniedUrl = `/course/${courseId}/access-denied`;
history.push(accessDeniedUrl);
});

it('Displays loading in start on page rendering', () => {
mockCourseStatus = 'loading';
render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
);
expect(screen.getByTestId('page-loading')).toBeInTheDocument();
expect(history.location.pathname).toBe(accessDeniedUrl);
});

it('Redirect user to homepage if user has access', () => {
mockCourseStatus = 'loaded';
render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
);
expect(history.location.pathname).toBe('/redirect/home/course-v1:edX+DemoX+Demo_Course');
});

it('For access denied it should render access denied page', () => {
mockCourseStatus = 'denied';

render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
);
expect(screen.getByTestId('access-denied-main')).toBeInTheDocument();
expect(history.location.pathname).toBe(accessDeniedUrl);
});
});
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import initializeStore from './store';
import NoticesProvider from './generic/notices';
import PathFixesProvider from './generic/path-fixes';
import LiveTab from './course-home/live-tab/LiveTab';
import CourseAccessErrorPage from './generic/CourseAccessErrorPage';

subscribe(APP_READY, () => {
ReactDOM.render(
Expand All @@ -44,6 +45,7 @@ subscribe(APP_READY, () => {
<Switch>
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
Expand Down
3 changes: 3 additions & 0 deletions src/shared/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export function getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess
case 'data_sharing_access_required':
url = `/redirect/consent?consentPath=${encodeURIComponent(courseAccess.developerMessage)}`;
break;
case 'incorrect_active_enterprise':
url = `/course/${courseId}/access-denied`;
break;
case 'unfulfilled_milestones':
url = '/redirect/dashboard';
break;
Expand Down

0 comments on commit f9806d0

Please sign in to comment.