feat: Access for Import/Export Pages Based on Permissions (#804)
* feat: import/export page access based on permissions
This commit is contained in:
@@ -25,6 +25,9 @@ import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './
|
||||
import ExportModalError from './export-modal-error/ExportModalError';
|
||||
import ExportFooter from './export-footer/ExportFooter';
|
||||
import ExportStepper from './export-stepper/ExportStepper';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const CourseExportPage = ({ intl, courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -38,6 +41,14 @@ const CourseExportPage = ({ intl, courseId }) => {
|
||||
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS;
|
||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasExportPermissions = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))
|
||||
);
|
||||
const viewOnly = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
|
||||
@@ -48,6 +59,12 @@ const CourseExportPage = ({ intl, courseId }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!hasExportPermissions) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -89,13 +106,14 @@ const CourseExportPage = ({ intl, courseId }) => {
|
||||
className="mb-4"
|
||||
onClick={() => dispatch(startExportingCourse(courseId))}
|
||||
iconBefore={ArrowCircleDownIcon}
|
||||
disabled={viewOnly}
|
||||
>
|
||||
{intl.formatMessage(messages.buttonTitle)}
|
||||
</Button>
|
||||
</Card.Section>
|
||||
)}
|
||||
</Card>
|
||||
{exportTriggered && <ExportStepper courseId={courseId} />}
|
||||
{exportTriggered && <ExportStepper courseId={courseId} viewOnly={viewOnly} />}
|
||||
<ExportFooter />
|
||||
</article>
|
||||
</Layout.Element>
|
||||
|
||||
@@ -12,6 +12,9 @@ import initializeStore from '../store';
|
||||
import stepperMessages from './export-stepper/messages';
|
||||
import modalErrorMessages from './export-modal-error/messages';
|
||||
import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import { executeThunk } from '../utils';
|
||||
import { EXPORT_STAGES } from './data/constants';
|
||||
import { exportPageMock } from './__mocks__';
|
||||
import messages from './messages';
|
||||
@@ -22,6 +25,8 @@ let axiosMock;
|
||||
let cookies;
|
||||
const courseId = '123';
|
||||
const courseName = 'About Node JS';
|
||||
const userId = 3;
|
||||
let userPermissionsData = { permissions: [] };
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
@@ -49,7 +54,7 @@ describe('<CourseExportPage />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -60,6 +65,14 @@ describe('<CourseExportPage />', () => {
|
||||
axiosMock
|
||||
.onGet(postExportCourseApiUrl(courseId))
|
||||
.reply(200, exportPageMock);
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
cookies = new Cookies();
|
||||
cookies.get.mockReturnValue(null);
|
||||
});
|
||||
@@ -85,8 +98,57 @@ describe('<CourseExportPage />', () => {
|
||||
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should render permissionDenied if incorrect permissions', async () => {
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||
});
|
||||
it('should render without errors if correct permissions', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
userPermissionsData = { permissions: ['manage_course_settings'] };
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
const exportPageElement = getByText(messages.headingTitle.defaultMessage, {
|
||||
selector: 'h2.sub-header-title',
|
||||
});
|
||||
expect(exportPageElement).toBeInTheDocument();
|
||||
expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should render without errors if viewOnly permissions', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
userPermissionsData = { permissions: ['view_course_settings'] };
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
const exportPageElement = getByText(messages.headingTitle.defaultMessage, {
|
||||
selector: 'h2.sub-header-title',
|
||||
});
|
||||
const buttonElement = getByText(messages.buttonTitle.defaultMessage);
|
||||
expect(exportPageElement).toBeInTheDocument();
|
||||
expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument();
|
||||
expect(buttonElement).toBeInTheDocument();
|
||||
expect(buttonElement.disabled).toEqual(true);
|
||||
});
|
||||
userPermissionsData = { permissions: ['manage_course_settings'] };
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
});
|
||||
it('should start exporting on click', async () => {
|
||||
const { getByText, container } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
const button = container.querySelector('.btn-primary');
|
||||
fireEvent.click(button);
|
||||
expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument();
|
||||
@@ -96,6 +158,8 @@ describe('<CourseExportPage />', () => {
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } });
|
||||
const { getByText, queryByText, container } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
const startExportButton = container.querySelector('.btn-primary');
|
||||
fireEvent.click(startExportButton);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
@@ -116,6 +180,8 @@ describe('<CourseExportPage />', () => {
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' });
|
||||
const { getByText, container } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
const startExportButton = container.querySelector('.btn-primary');
|
||||
fireEvent.click(startExportButton);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
@@ -129,6 +195,8 @@ describe('<CourseExportPage />', () => {
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: 'http://test-download-path.test' });
|
||||
const { getByText, container } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
const startExportButton = container.querySelector('.btn-primary');
|
||||
fireEvent.click(startExportButton);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
|
||||
@@ -17,7 +17,7 @@ import { EXPORT_STAGES } from '../data/constants';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const ExportStepper = ({ intl, courseId }) => {
|
||||
const ExportStepper = ({ intl, courseId, viewOnly }) => {
|
||||
const currentStage = useSelector(getCurrentStage);
|
||||
const downloadPath = useSelector(getDownloadPath);
|
||||
const successDate = useSelector(getSuccessDate);
|
||||
@@ -90,7 +90,7 @@ const ExportStepper = ({ intl, courseId }) => {
|
||||
errorMessage={errorMessage}
|
||||
hasError={!!errorMessage}
|
||||
/>
|
||||
{downloadPath && currentStage === EXPORT_STAGES.SUCCESS && <Button className="ml-5.5 mt-n2.5" href={downloadPath} download>{intl.formatMessage(messages.downloadCourseButtonTitle)}</Button>}
|
||||
{downloadPath && currentStage === EXPORT_STAGES.SUCCESS && <Button className="ml-5.5 mt-n2.5" href={downloadPath} download disabled={viewOnly}>{intl.formatMessage(messages.downloadCourseButtonTitle)}</Button>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -98,6 +98,7 @@ const ExportStepper = ({ intl, courseId }) => {
|
||||
ExportStepper.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ExportStepper);
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
|
||||
import messages from './messages';
|
||||
import ExportStepper from './ExportStepper';
|
||||
import { exportStepperPageMock } from './__mocks__';
|
||||
import { fetchExportStatus } from '../data/thunks';
|
||||
import { getExportStatusApiUrl } from '../data/api';
|
||||
|
||||
const courseId = 'course-123';
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} />
|
||||
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} viewOnly={false} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const ViewOnlyRootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} viewOnly />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -30,9 +46,22 @@ describe('<ExportStepper />', () => {
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(200, exportStepperPageMock);
|
||||
executeThunk(fetchExportStatus(courseId), store.dispatch);
|
||||
});
|
||||
it('render stepper correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('render stepper correctly if button is disabled', () => {
|
||||
const { getByText } = render(<ViewOnlyRootWrapper />);
|
||||
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
||||
const buttonElement = getByText(messages.downloadCourseButtonTitle.defaultMessage, {
|
||||
selector: '.disabled',
|
||||
});
|
||||
expect(buttonElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
exportStatus: 3,
|
||||
exportOutput: '/test',
|
||||
};
|
||||
2
src/export-page/export-stepper/__mocks__/index.js
Normal file
2
src/export-page/export-stepper/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as exportStepperPageMock } from './exportStepperPage';
|
||||
@@ -28,7 +28,7 @@ const Header = ({
|
||||
const hasSettingsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_advanced_settings') || checkPermission('view_course_settings')));
|
||||
const hasToolsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
|
||||
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const contentMenu = getContentMenuItems({
|
||||
studioBaseUrl,
|
||||
@@ -37,6 +37,12 @@ const Header = ({
|
||||
hasContentPermissions,
|
||||
});
|
||||
const mainMenuDropdowns = [];
|
||||
const toolsMenu = getToolsMenuItems({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasToolsPermissions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUserPermissionsEnabledFlag());
|
||||
@@ -65,17 +71,16 @@ const Header = ({
|
||||
hasSettingsPermissions,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.tools']),
|
||||
items: getToolsMenuItems({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasToolsPermissions,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (toolsMenu.length > 0) {
|
||||
mainMenuDropdowns.push(
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.tools']),
|
||||
items: toolsMenu,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const outlineLink = `${studioBaseUrl}/course/${courseId}`;
|
||||
return (
|
||||
|
||||
@@ -91,18 +91,16 @@ export const getToolsMenuItems = ({
|
||||
hasToolsPermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/import/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.import']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/export/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.export']),
|
||||
},
|
||||
);
|
||||
if (hasToolsPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/import/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.import']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/export/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.export']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/checklists/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.checklists']),
|
||||
|
||||
@@ -51,9 +51,9 @@ describe('header utils', () => {
|
||||
const actualItems = getToolsMenuItems(toolsProps);
|
||||
expect(actualItems).toHaveLength(3);
|
||||
});
|
||||
it('should not include Checklist option', () => {
|
||||
it('should not include any items if there are no permissions', () => {
|
||||
const actualItems = getToolsMenuItems({ ...baseProps, hasToolsPermissions: false });
|
||||
expect(actualItems).toHaveLength(2);
|
||||
expect(actualItems).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,9 @@ import { LAST_IMPORT_COOKIE_NAME } from './data/constants';
|
||||
import ImportSidebar from './import-sidebar/ImportSidebar';
|
||||
import FileSection from './file-section/FileSection';
|
||||
import messages from './messages';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const CourseImportPage = ({ intl, courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -32,6 +35,14 @@ const CourseImportPage = ({ intl, courseId }) => {
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasImportPermissions = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))
|
||||
);
|
||||
const viewOnly = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME);
|
||||
@@ -43,6 +54,12 @@ const CourseImportPage = ({ intl, courseId }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!hasImportPermissions) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -72,7 +89,7 @@ const CourseImportPage = ({ intl, courseId }) => {
|
||||
<p className="small">{intl.formatMessage(messages.description1)}</p>
|
||||
<p className="small">{intl.formatMessage(messages.description2)}</p>
|
||||
<p className="small">{intl.formatMessage(messages.description3)}</p>
|
||||
<FileSection courseId={courseId} />
|
||||
<FileSection courseId={courseId} viewOnly={viewOnly} />
|
||||
{importTriggered && <ImportStepper courseId={courseId} />}
|
||||
</article>
|
||||
</Layout.Element>
|
||||
|
||||
@@ -14,12 +14,17 @@ import CourseImportPage from './CourseImportPage';
|
||||
import { getImportStatusApiUrl } from './data/api';
|
||||
import { IMPORT_STAGES } from './data/constants';
|
||||
import stepperMessages from './import-stepper/messages';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import { executeThunk } from '../utils';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
let cookies;
|
||||
const courseId = '123';
|
||||
const courseName = 'About Node JS';
|
||||
const userId = 3;
|
||||
let userPermissionsData = { permissions: [] };
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
@@ -47,7 +52,7 @@ describe('<CourseImportPage />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -58,6 +63,12 @@ describe('<CourseImportPage />', () => {
|
||||
axiosMock
|
||||
.onGet(getImportStatusApiUrl(courseId, 'testFileName.test'))
|
||||
.reply(200, { importStatus: 1, message: '' });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
cookies = new Cookies();
|
||||
cookies.get.mockReturnValue(null);
|
||||
});
|
||||
@@ -83,6 +94,30 @@ describe('<CourseImportPage />', () => {
|
||||
expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should render permissionDenied if incorrect permissions', async () => {
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||
});
|
||||
it('should render without errors if correct permissions', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
userPermissionsData = { permissions: ['manage_course_settings'] };
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
const importPageElement = getByText(messages.headingTitle.defaultMessage, {
|
||||
selector: 'h2.sub-header-title',
|
||||
});
|
||||
expect(importPageElement).toBeInTheDocument();
|
||||
expect(getByText(messages.description1.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should fetch status without clicking when cookies has', async () => {
|
||||
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' });
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Dropzone } from '@edx/paragon';
|
||||
|
||||
import { Alert, Card, Dropzone } from '@edx/paragon';
|
||||
import { IMPORT_STAGES } from '../data/constants';
|
||||
import {
|
||||
getCurrentStage, getError, getFileName, getImportTriggered,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import messages from './messages';
|
||||
import { handleProcessUpload } from '../data/thunks';
|
||||
|
||||
const FileSection = ({ intl, courseId }) => {
|
||||
const FileSection = ({ intl, courseId, viewOnly }) => {
|
||||
const dispatch = useDispatch();
|
||||
const importTriggered = useSelector(getImportTriggered);
|
||||
const currentStage = useSelector(getCurrentStage);
|
||||
@@ -30,21 +30,25 @@ const FileSection = ({ intl, courseId }) => {
|
||||
subtitle={fileName && intl.formatMessage(messages.fileChosen, { fileName })}
|
||||
/>
|
||||
<Card.Section className="px-3 pt-2 pb-4">
|
||||
{isShowedDropzone
|
||||
&& (
|
||||
<Dropzone
|
||||
onProcessUpload={
|
||||
({ fileData, requestConfig, handleError }) => dispatch(handleProcessUpload(
|
||||
courseId,
|
||||
fileData,
|
||||
requestConfig,
|
||||
handleError,
|
||||
))
|
||||
}
|
||||
accept={{ 'application/gzip': ['.tar.gz'] }}
|
||||
data-testid="dropzone"
|
||||
/>
|
||||
)}
|
||||
{!viewOnly && isShowedDropzone && (
|
||||
<Dropzone
|
||||
onProcessUpload={
|
||||
({ fileData, requestConfig, handleError }) => dispatch(handleProcessUpload(
|
||||
courseId,
|
||||
fileData,
|
||||
requestConfig,
|
||||
handleError,
|
||||
))
|
||||
}
|
||||
accept={{ 'application/gzip': ['.tar.gz'] }}
|
||||
data-testid="dropzone"
|
||||
/>
|
||||
)}
|
||||
{viewOnly && (
|
||||
<Alert variant="info">
|
||||
<FormattedMessage {...messages.viewOnlyAlert} />
|
||||
</Alert>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
@@ -53,6 +57,7 @@ const FileSection = ({ intl, courseId }) => {
|
||||
FileSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(FileSection);
|
||||
|
||||
@@ -14,7 +14,15 @@ const courseId = '123';
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<FileSection intl={injectIntl} courseId={courseId} />
|
||||
<FileSection intl={injectIntl} courseId={courseId} viewOnly={false} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const RootWrapperViewOnly = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<FileSection intl={injectIntl} courseId={courseId} viewOnly />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -27,6 +35,7 @@ describe('<FileSection />', () => {
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
permisions: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
@@ -43,6 +52,13 @@ describe('<FileSection />', () => {
|
||||
expect(getByTestId('dropzone')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should not render Dropzone when view is viewOnly', async () => {
|
||||
const { getByText, queryByTestId, container } = render(<RootWrapperViewOnly />);
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument();
|
||||
expect(getByText(messages.viewOnlyAlert.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should work Dropzone', async () => {
|
||||
const {
|
||||
getByText, getByTestId, queryByTestId, container,
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.import.file-section.chosen-file',
|
||||
defaultMessage: 'File chosen: {fileName}',
|
||||
},
|
||||
viewOnlyAlert: {
|
||||
id: 'course-authoring.import.file-section.view-only-alert',
|
||||
defaultMessage: 'You have view only access to this page. If you feel you should have full access, please reach out to your course team admin to be given access.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -8,6 +8,12 @@ import * as xpertUnitSummaryApi from './xpert-unit-summary/data/api';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
|
||||
jest.mock('../generic/hooks', () => ({
|
||||
useUserPermissions: jest.fn(() => ({
|
||||
checkPermission: jest.fn(() => true),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('PagesAndResources', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
Reference in New Issue
Block a user