diff --git a/src/course-checklist/CourseChecklist.jsx b/src/course-checklist/CourseChecklist.jsx
index 8c15a11dc0..a220cff8fa 100644
--- a/src/course-checklist/CourseChecklist.jsx
+++ b/src/course-checklist/CourseChecklist.jsx
@@ -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 (
+
+
+
+ );
+ }
return (
<>
diff --git a/src/course-checklist/CourseChecklist.test.jsx b/src/course-checklist/CourseChecklist.test.jsx
index 53d52af77e..69057abf70 100644
--- a/src/course-checklist/CourseChecklist.test.jsx
+++ b/src/course-checklist/CourseChecklist.test.jsx
@@ -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();
+ });
+ });
});
});
diff --git a/src/course-checklist/data/thunks.js b/src/course-checklist/data/thunks.js
index 20be7648a1..74547dab7b 100644
--- a/src/course-checklist/data/thunks.js
+++ b/src/course-checklist/data/thunks.js
@@ -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 }));
+ }
}
};
}
diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx
index fe428c1c74..21d35831df 100644
--- a/src/course-outline/CourseOutline.jsx
+++ b/src/course-outline/CourseOutline.jsx
@@ -68,6 +68,7 @@ const CourseOutline = ({ courseId }) => {
sectionsList,
isCustomRelativeDatesActive,
isLoading,
+ isLoadingDenied,
isReIndexShow,
showSuccessAlert,
isSectionsExpanded,
@@ -233,6 +234,27 @@ const CourseOutline = ({ courseId }) => {
);
}
+ if (isLoadingDenied) {
+ return (
+
+
+
+ );
+ }
+
return (
<>
diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx
index b7f8332eeb..bfe6c07705 100644
--- a/src/course-outline/CourseOutline.test.jsx
+++ b/src/course-outline/CourseOutline.test.jsx
@@ -2291,4 +2291,18 @@ describe('', () => {
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();
+
+ await waitFor(() => {
+ expect(getByRole('alert')).toBeInTheDocument();
+ const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
+ expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
+ });
+ });
});
diff --git a/src/course-outline/data/thunk.js b/src/course-outline/data/thunk.js
index 315c5846c0..c555fcb978 100644
--- a/src/course-outline/data/thunk.js
+++ b/src/course-outline/data/thunk.js
@@ -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),
+ }));
+ }
}
};
}
diff --git a/src/course-outline/hooks.jsx b/src/course-outline/hooks.jsx
index 739d599432..c3b43a9fd3 100644
--- a/src/course-outline/hooks.jsx
+++ b/src/course-outline/hooks.jsx
@@ -302,6 +302,7 @@ const useCourseOutline = ({ courseId }) => {
sectionsList,
isCustomRelativeDatesActive,
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
+ isLoadingDenied: outlineIndexLoadingStatus === RequestStatus.DENIED,
isReIndexShow: Boolean(reindexLink),
showSuccessAlert,
isDisabledReindexButton,
diff --git a/src/course-team/CourseTeam.jsx b/src/course-team/CourseTeam.jsx
index 10360460a3..29ad5b80f9 100644
--- a/src/course-team/CourseTeam.jsx
+++ b/src/course-team/CourseTeam.jsx
@@ -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 (
+
+
+
+ );
+ }
+
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>>;
diff --git a/src/course-team/CourseTeam.test.jsx b/src/course-team/CourseTeam.test.jsx
index 4f33788744..046e8ef95f 100644
--- a/src/course-team/CourseTeam.test.jsx
+++ b/src/course-team/CourseTeam.test.jsx
@@ -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('', () => {
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();
+
+ 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();
+
+ await waitFor(() => {
+ const { loadingCourseTeamStatus } = store.getState().courseTeam;
+ expect(loadingCourseTeamStatus).toEqual(RequestStatus.FAILED);
+ });
+ });
});
diff --git a/src/course-team/data/thunk.js b/src/course-team/data/thunk.js
index 78012870f6..bfc3db193e 100644
--- a/src/course-team/data/thunk.js
+++ b/src/course-team/data/thunk.js
@@ -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;
}
};
diff --git a/src/course-team/hooks.jsx b/src/course-team/hooks.jsx
index 1b3778fecf..7c6bc09a48 100644
--- a/src/course-team/hooks.jsx
+++ b/src/course-team/hooks.jsx
@@ -113,6 +113,7 @@ const useCourseTeam = ({ courseId }) => {
courseTeamUsers,
currentUserEmail,
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
+ isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
isSingleAdmin,
isFormVisible,
isAllowActions,
diff --git a/src/course-updates/CourseUpdates.jsx b/src/course-updates/CourseUpdates.jsx
index a6b7af677b..0d82f3bf8d 100644
--- a/src/course-updates/CourseUpdates.jsx
+++ b/src/course-updates/CourseUpdates.jsx
@@ -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 (
+
+
+
+ );
+ }
+
return (
<>
diff --git a/src/course-updates/CourseUpdates.test.jsx b/src/course-updates/CourseUpdates.test.jsx
index 387d3b3c26..ab8e1a8022 100644
--- a/src/course-updates/CourseUpdates.test.jsx
+++ b/src/course-updates/CourseUpdates.test.jsx
@@ -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('', () => {
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();
+
+ 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', () => {
diff --git a/src/course-updates/data/thunk.js b/src/course-updates/data/thunk.js
index 88b3a0578a..225ca9e461 100644
--- a/src/course-updates/data/thunk.js
+++ b/src/course-updates/data/thunk.js
@@ -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 },
+ }));
+ }
}
};
}
diff --git a/src/export-page/CourseExportPage.jsx b/src/export-page/CourseExportPage.jsx
index b332499732..36f96f2513 100644
--- a/src/export-page/CourseExportPage.jsx
+++ b/src/export-page/CourseExportPage.jsx
@@ -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 (
+
+
+
+ );
+ }
+
return (
<>
diff --git a/src/export-page/CourseExportPage.test.jsx b/src/export-page/CourseExportPage.test.jsx
index 05db07d5bf..4c0a01d5e6 100644
--- a/src/export-page/CourseExportPage.test.jsx
+++ b/src/export-page/CourseExportPage.test.jsx
@@ -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('', () => {
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();
+ 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();
+ 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);
+ });
});
diff --git a/src/export-page/data/thunks.js b/src/export-page/data/thunks.js
index f5fdcae07c..dd86ba84ef 100644
--- a/src/export-page/data/thunks.js
+++ b/src/export-page/data/thunks.js
@@ -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;
}
};
diff --git a/src/grading-settings/GradingSettings.jsx b/src/grading-settings/GradingSettings.jsx
index cda1a6278c..bcb3a8f089 100644
--- a/src/grading-settings/GradingSettings.jsx
+++ b/src/grading-settings/GradingSettings.jsx
@@ -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 (
+
+
+
+ );
+ }
+
if (isLoading) {
return null;
}
diff --git a/src/grading-settings/GradingSettings.test.jsx b/src/grading-settings/GradingSettings.test.jsx
index 84b574367f..686cf9acd1 100644
--- a/src/grading-settings/GradingSettings.test.jsx
+++ b/src/grading-settings/GradingSettings.test.jsx
@@ -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('', () => {
setOnlineStatus(true);
testSaving();
});
+
+ it('should display connection error alert when loading is denied', async () => {
+ jest.spyOn(apiHooks, 'useGradingSettings').mockReturnValue({ isError: true });
+ render();
+ expect(screen.getByTestId('connectionErrorAlert')).toBeInTheDocument();
+ });
});
diff --git a/src/group-configurations/GroupConfigurations.test.jsx b/src/group-configurations/GroupConfigurations.test.jsx
index 34486c368b..303b557f2c 100644
--- a/src/group-configurations/GroupConfigurations.test.jsx
+++ b/src/group-configurations/GroupConfigurations.test.jsx
@@ -103,4 +103,21 @@ describe('', () => {
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,
+ );
+ });
+ });
});
diff --git a/src/group-configurations/data/thunk.js b/src/group-configurations/data/thunk.js
index 16b961d96d..30ae354730 100644
--- a/src/group-configurations/data/thunk.js
+++ b/src/group-configurations/data/thunk.js
@@ -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 }));
+ }
}
};
}
diff --git a/src/group-configurations/hooks.jsx b/src/group-configurations/hooks.jsx
index 01187f1d2c..cbc3cb7b0e 100644
--- a/src/group-configurations/hooks.jsx
+++ b/src/group-configurations/hooks.jsx
@@ -85,6 +85,7 @@ const useGroupConfigurations = (courseId) => {
return {
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
+ isLoadingDenied: loadingStatus === RequestStatus.DENIED,
savingStatus,
contentGroupActions,
experimentConfigurationActions,
diff --git a/src/group-configurations/index.jsx b/src/group-configurations/index.jsx
index ad7900946d..1f150c8288 100644
--- a/src/group-configurations/index.jsx
+++ b/src/group-configurations/index.jsx
@@ -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 (
+
+
+
+ );
+ }
+
if (isLoading) {
return (
diff --git a/src/import-page/CourseImportPage.jsx b/src/import-page/CourseImportPage.jsx
index 368d44f741..2daea3fdb2 100644
--- a/src/import-page/CourseImportPage.jsx
+++ b/src/import-page/CourseImportPage.jsx
@@ -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 (
+
+
+
+ );
+ }
+
return (
<>
diff --git a/src/import-page/CourseImportPage.test.jsx b/src/import-page/CourseImportPage.test.jsx
index 8359312692..646eadbe6e 100644
--- a/src/import-page/CourseImportPage.test.jsx
+++ b/src/import-page/CourseImportPage.test.jsx
@@ -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('', () => {
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();
+ // 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();
+ // 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);
+ });
});
diff --git a/src/import-page/data/thunks.js b/src/import-page/data/thunks.js
index 95acb291ec..028fea2da2 100644
--- a/src/import-page/data/thunks.js
+++ b/src/import-page/data/thunks.js
@@ -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;
}
};
diff --git a/src/textbooks/Textbook.test.jsx b/src/textbooks/Textbook.test.jsx
index b8437f114f..60af1e3026 100644
--- a/src/textbooks/Textbook.test.jsx
+++ b/src/textbooks/Textbook.test.jsx
@@ -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('', () => {
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,
+ );
+ });
+ });
});
diff --git a/src/textbooks/Textbooks.jsx b/src/textbooks/Textbooks.jsx
index e796ac2ed5..b2c0607976 100644
--- a/src/textbooks/Textbooks.jsx
+++ b/src/textbooks/Textbooks.jsx
@@ -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 (
+
+
+
+ );
+ }
+
if (isLoading) {
return (
diff --git a/src/textbooks/hooks.jsx b/src/textbooks/hooks.jsx
index 74d33ceb0b..00a3dcf6de 100644
--- a/src/textbooks/hooks.jsx
+++ b/src/textbooks/hooks.jsx
@@ -77,6 +77,7 @@ const useTextbooks = (courseId, waffleFlags) => {
return {
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
+ isLoadingFailed: loadingStatus === RequestStatus.FAILED,
savingStatus,
errorMessage,
textbooks,