feat: Access for Import/Export Pages Based on Permissions (#804)

* feat: import/export page access based on permissions
This commit is contained in:
hilary sinkoff
2024-02-06 15:02:19 -06:00
committed by hsinkoff
parent 69d9ea318e
commit a518fada29
15 changed files with 257 additions and 49 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
});
});

View File

@@ -0,0 +1,4 @@
module.exports = {
exportStatus: 3,
exportOutput: '/test',
};

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as exportStepperPageMock } from './exportStepperPage';

View File

@@ -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 (

View File

@@ -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']),

View File

@@ -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);
});
});
});

View File

@@ -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>

View File

@@ -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 />);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();