@@ -13,6 +13,7 @@ import AriaLiveRegion from './AriaLiveRegion';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import ChecklistSection from './ChecklistSection';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseChecklist = ({
|
||||
courseId,
|
||||
@@ -34,10 +35,19 @@ const CourseChecklist = ({
|
||||
bestPracticeData,
|
||||
} = useSelector(state => state.courseChecklist);
|
||||
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus } = loadingStatus;
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus, launchChecklistStatus } = loadingStatus;
|
||||
|
||||
const isCourseLaunchChecklistLoading = bestPracticeChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isCourseBestPracticeChecklistLoading = launchChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isLoadingDenied = launchChecklistStatus === RequestStatus.DENIED;
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -149,5 +149,20 @@ describe('CourseChecklistPage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
const courseLaunchApiUrl = getCourseLaunchApiUrl({
|
||||
courseId, gradedOnly: true, validateOras: true, all: true,
|
||||
});
|
||||
axiosMock.onGet(courseLaunchApiUrl).reply(403);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
const { launchChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
expect(launchChecklistStatus).toEqual(RequestStatus.DENIED);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,11 @@ export function fetchCourseLaunchQuery({
|
||||
dispatch(fetchLaunchChecklistSuccess({ data }));
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.FAILED }));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading,
|
||||
isLoadingDenied,
|
||||
isReIndexShow,
|
||||
showSuccessAlert,
|
||||
isSectionsExpanded,
|
||||
@@ -233,6 +234,27 @@ const CourseOutline = ({ courseId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="px-4 mt-4">
|
||||
<PageAlerts
|
||||
courseId={courseId}
|
||||
notificationDismissUrl={notificationDismissUrl}
|
||||
handleDismissNotification={handleDismissNotification}
|
||||
discussionsSettings={discussionsSettings}
|
||||
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
|
||||
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
|
||||
deprecatedBlocksInfo={deprecatedBlocksInfo}
|
||||
proctoringErrors={proctoringErrors}
|
||||
mfeProctoredExamSettingsUrl={mfeProctoredExamSettingsUrl}
|
||||
advanceSettingsUrl={advanceSettingsUrl}
|
||||
savingStatus={savingStatus}
|
||||
errors={errors}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -2291,4 +2291,18 @@ describe('<CourseOutline />', () => {
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
|
||||
expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,10 +99,16 @@ export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
|
||||
dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.FAILED,
|
||||
errors: getErrorDetails(error, false),
|
||||
}));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.DENIED,
|
||||
}));
|
||||
} else {
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.FAILED,
|
||||
errors: getErrorDetails(error, false),
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,6 +302,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: outlineIndexLoadingStatus === RequestStatus.DENIED,
|
||||
isReIndexShow: Boolean(reindexLink),
|
||||
showSuccessAlert,
|
||||
isDisabledReindexButton,
|
||||
|
||||
@@ -20,6 +20,7 @@ import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||
import InfoModal from './info-modal/InfoModal';
|
||||
import { useCourseTeam } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseTeam = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -35,6 +36,7 @@ const CourseTeam = ({ courseId }) => {
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
isLoading,
|
||||
isLoadingDenied,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isQueryPending,
|
||||
@@ -55,6 +57,14 @@ const CourseTeam = ({ courseId }) => {
|
||||
handleInternetConnectionFailed,
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
@@ -18,6 +17,7 @@ import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { changeRoleTeamUserQuery, deleteCourseTeamQuery } from './data/thunk';
|
||||
|
||||
let axiosMock;
|
||||
@@ -219,4 +219,31 @@ describe('<CourseTeam />', () => {
|
||||
await executeThunk(changeRoleTeamUserQuery(courseId, 'staff@example.com', { role: USER_ROLES.admin }), store.dispatch);
|
||||
expect(getAllByText('Admin')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets loading status to FAILED upon receiving a 404 response from the API', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,11 @@ export function fetchCourseTeamQuery(courseId) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.FAILED }));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,6 +113,7 @@ const useCourseTeam = ({ courseId }) => {
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isAllowActions,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getProcessingNotification } from '../generic/processing-notification/da
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import CourseHandouts from './course-handouts/CourseHandouts';
|
||||
import CourseUpdate from './course-update/CourseUpdate';
|
||||
@@ -64,9 +65,18 @@ const CourseUpdates = ({ courseId }) => {
|
||||
const errors = useSelector(getErrors);
|
||||
|
||||
const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED);
|
||||
const anyStatusDenied = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.DENIED);
|
||||
const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS);
|
||||
const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING);
|
||||
|
||||
if (anyStatusDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||
import {
|
||||
render, waitFor, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from './data/thunk';
|
||||
import initializeStore from '../store';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
||||
import CourseUpdates from './CourseUpdates';
|
||||
import messages from './messages';
|
||||
@@ -278,6 +280,24 @@ describe('<CourseUpdates />', () => {
|
||||
expect(getByTestId('course-handouts-edit-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(403, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
const { loadingStatuses } = store.getState().courseUpdates;
|
||||
Object.values(loadingStatuses)
|
||||
.some(status => expect(status).toEqual(RequestStatus.DENIED));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('saving failure API responses', () => {
|
||||
|
||||
@@ -31,10 +31,17 @@ export function fetchCourseUpdatesQuery(courseId) {
|
||||
error: { loadingUpdates: false },
|
||||
}));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseUpdatesQuery: RequestStatus.FAILED },
|
||||
error: { loadingUpdates: true },
|
||||
}));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseUpdatesQuery: RequestStatus.DENIED },
|
||||
error: { loadingUpdates: true },
|
||||
}));
|
||||
} else {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseUpdatesQuery: RequestStatus.FAILED },
|
||||
error: { loadingUpdates: true },
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -116,10 +123,17 @@ export function fetchCourseHandoutsQuery(courseId) {
|
||||
error: { loadingHandouts: false },
|
||||
}));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseHandoutsQuery: RequestStatus.FAILED },
|
||||
error: { loadingHandouts: true },
|
||||
}));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseHandoutsQuery: RequestStatus.DENIED },
|
||||
error: { loadingHandouts: true },
|
||||
}));
|
||||
} else {
|
||||
dispatch(updateLoadingStatuses({
|
||||
status: { fetchCourseHandoutsQuery: RequestStatus.FAILED },
|
||||
error: { loadingHandouts: true },
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
@@ -37,6 +38,7 @@ const CourseExportPage = ({ intl, courseId }) => {
|
||||
const cookies = new Cookies();
|
||||
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS;
|
||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||
const isLoadingDenied = loadingStatus === RequestStatus.DENIED;
|
||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,6 +50,14 @@ const CourseExportPage = ({ intl, courseId }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -6,9 +5,10 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import stepperMessages from './export-stepper/messages';
|
||||
import modalErrorMessages from './export-modal-error/messages';
|
||||
import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api';
|
||||
@@ -137,4 +137,30 @@ describe('<CourseExportPage />', () => {
|
||||
expect(downloadButton).toBeInTheDocument();
|
||||
expect(downloadButton.getAttribute('href')).toEqual('http://test-download-path.test');
|
||||
});
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(403);
|
||||
const { getByRole, container } = render(<RootWrapper />);
|
||||
const startExportButton = container.querySelector('.btn-primary');
|
||||
fireEvent.click(startExportButton);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { loadingStatus } = store.getState().courseExport;
|
||||
expect(loadingStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
|
||||
it('sets loading status to FAILED upon receiving a 404 response from the API', async () => {
|
||||
axiosMock
|
||||
.onGet(getExportStatusApiUrl(courseId))
|
||||
.reply(404);
|
||||
const { container } = render(<RootWrapper />);
|
||||
const startExportButton = container.querySelector('.btn-primary');
|
||||
fireEvent.click(startExportButton);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
const { loadingStatus } = store.getState().courseExport;
|
||||
expect(loadingStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,11 @@ export function fetchExportStatus(courseId) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.FAILED }));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import AlertMessage from '../generic/alert-message';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import SectionSubHeader from '../generic/section-sub-header';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
@@ -32,10 +33,12 @@ const GradingSettings = ({ courseId }) => {
|
||||
const {
|
||||
data: gradingSettings,
|
||||
isLoading: isGradingSettingsLoading,
|
||||
isError: isGradingSettingsError,
|
||||
} = useGradingSettings(courseId);
|
||||
const {
|
||||
data: courseSettingsData,
|
||||
isLoading: isCourseSettingsLoading,
|
||||
isError: isCourseSettingsError,
|
||||
} = useCourseSettings(courseId);
|
||||
const {
|
||||
mutate: updateGradingSettings,
|
||||
@@ -46,7 +49,7 @@ const GradingSettings = ({ courseId }) => {
|
||||
|
||||
const courseAssignmentLists = gradingSettings?.courseAssignmentLists;
|
||||
const courseGradingDetails = gradingSettings?.courseDetails;
|
||||
|
||||
const isLoadingDenied = isGradingSettingsError || isCourseSettingsError;
|
||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading;
|
||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
||||
@@ -86,6 +89,14 @@ const GradingSettings = ({ courseId }) => {
|
||||
}
|
||||
}, [savePending]);
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
act, fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import React from 'react';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import gradingSettings from './__mocks__/gradingSettings';
|
||||
import { getCourseSettingsApiUrl, getGradingSettingsApiUrl } from './data/api';
|
||||
import * as apiHooks from './data/apiHooks';
|
||||
import GradingSettings from './GradingSettings';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -112,4 +112,10 @@ describe('<GradingSettings />', () => {
|
||||
setOnlineStatus(true);
|
||||
testSaving();
|
||||
});
|
||||
|
||||
it('should display connection error alert when loading is denied', async () => {
|
||||
jest.spyOn(apiHooks, 'useGradingSettings').mockReturnValue({ isError: true });
|
||||
render(<RootWrapper />);
|
||||
expect(screen.getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,4 +103,21 @@ describe('<GroupConfigurations />', () => {
|
||||
RequestStatus.FAILED,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getContentStoreApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
await executeThunk(fetchGroupConfigurationsQuery(courseId), store.dispatch);
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
expect(store.getState().groupConfigurations.loadingStatus).toBe(
|
||||
RequestStatus.DENIED,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,11 @@ export function fetchGroupConfigurationsQuery(courseId) {
|
||||
dispatch(fetchGroupConfigurations({ groupConfigurations }));
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.FAILED }));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ const useGroupConfigurations = (courseId) => {
|
||||
|
||||
return {
|
||||
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: loadingStatus === RequestStatus.DENIED,
|
||||
savingStatus,
|
||||
contentGroupActions,
|
||||
experimentConfigurationActions,
|
||||
|
||||
@@ -16,6 +16,7 @@ import ExperimentConfigurationsSection from './experiment-configurations-section
|
||||
import EnrollmentTrackGroupsSection from './enrollment-track-groups-section';
|
||||
import GroupConfigurationSidebar from './group-configuration-sidebar';
|
||||
import { useGroupConfigurations } from './hooks';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const GroupConfigurations = ({ courseId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -34,6 +35,7 @@ const GroupConfigurations = ({ courseId }) => {
|
||||
shouldShowExperimentGroups,
|
||||
experimentGroupConfigurations,
|
||||
},
|
||||
isLoadingDenied,
|
||||
} = useGroupConfigurations(courseId);
|
||||
|
||||
document.title = getPageHeadTitle(
|
||||
@@ -41,6 +43,14 @@ const GroupConfigurations = ({ courseId }) => {
|
||||
formatMessage(messages.headingTitle),
|
||||
);
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
|
||||
@@ -13,6 +13,7 @@ import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import {
|
||||
updateFileName, updateImportTriggered, updateSavingStatus, updateSuccessDate,
|
||||
} from './data/slice';
|
||||
@@ -31,6 +32,7 @@ const CourseImportPage = ({ intl, courseId }) => {
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||
const isLoadingDenied = loadingStatus === RequestStatus.DENIED;
|
||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,6 +45,14 @@ const CourseImportPage = ({ intl, courseId }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import Cookies from 'universal-cookie';
|
||||
import initializeStore from '../store';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import CourseImportPage from './CourseImportPage';
|
||||
import { getImportStatusApiUrl } from './data/api';
|
||||
@@ -108,4 +108,29 @@ describe('<CourseImportPage />', () => {
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
expect(getByText(stepperMessages.viewOutlineButton.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz'))
|
||||
.reply(403);
|
||||
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' });
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { loadingStatus } = store.getState().courseImport;
|
||||
expect(loadingStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
|
||||
it('sets loading status to FAILED upon receiving a 404 response from the API', async () => {
|
||||
axiosMock
|
||||
.onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz'))
|
||||
.reply(404);
|
||||
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' });
|
||||
render(<RootWrapper />);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
const { loadingStatus } = store.getState().courseImport;
|
||||
expect(loadingStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import Cookies from 'universal-cookie';
|
||||
import moment from 'moment';
|
||||
|
||||
@@ -32,7 +31,11 @@ export function fetchImportStatus(courseId, fileName) {
|
||||
dispatch(updateLoadingStatus(RequestStatus.SUCCESSFUL));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatus(RequestStatus.FAILED));
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingStatus(RequestStatus.DENIED));
|
||||
} else {
|
||||
dispatch(updateLoadingStatus(RequestStatus.FAILED));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import initializeStore from '../store';
|
||||
import { executeThunk } from '../utils';
|
||||
import { getTextbooksApiUrl } from './data/api';
|
||||
@@ -84,4 +85,19 @@ describe('<Textbooks />', () => {
|
||||
expect(queryAllByTestId('textbook-card')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to FAILED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getTextbooksApiUrl(courseId))
|
||||
.reply(403);
|
||||
await executeThunk(fetchTextbooksQuery(courseId), store.dispatch);
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
expect(store.getState().textbooks.loadingStatus).toBe(
|
||||
RequestStatus.FAILED,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getProcessingNotification } from '../generic/processing-notification/da
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import TextbookCard from './textbook-card/TextbooksCard';
|
||||
@@ -36,6 +37,7 @@ const Textbooks = ({ courseId }) => {
|
||||
const {
|
||||
textbooks,
|
||||
isLoading,
|
||||
isLoadingFailed,
|
||||
breadcrumbs,
|
||||
errorMessage,
|
||||
savingStatus,
|
||||
@@ -53,6 +55,14 @@ const Textbooks = ({ courseId }) => {
|
||||
title: processingNotificationTitle,
|
||||
} = useSelector(getProcessingNotification);
|
||||
|
||||
if (isLoadingFailed) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
|
||||
@@ -77,6 +77,7 @@ const useTextbooks = (courseId, waffleFlags) => {
|
||||
|
||||
return {
|
||||
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingFailed: loadingStatus === RequestStatus.FAILED,
|
||||
savingStatus,
|
||||
errorMessage,
|
||||
textbooks,
|
||||
|
||||
Reference in New Issue
Block a user