fix: error handling (#1079)

Added alert message if 403 error occurs.
This commit is contained in:
Peter Kulko
2024-11-18 16:27:36 +02:00
committed by GitHub
parent 771c5d3e19
commit 0706a09acb
29 changed files with 340 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -302,6 +302,7 @@ const useCourseOutline = ({ courseId }) => {
sectionsList,
isCustomRelativeDatesActive,
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
isLoadingDenied: outlineIndexLoadingStatus === RequestStatus.DENIED,
isReIndexShow: Boolean(reindexLink),
showSuccessAlert,
isDisabledReindexButton,

View File

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

View File

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

View File

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

View File

@@ -113,6 +113,7 @@ const useCourseTeam = ({ courseId }) => {
courseTeamUsers,
currentUserEmail,
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
isSingleAdmin,
isFormVisible,
isAllowActions,

View File

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

View File

@@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,6 +85,7 @@ const useGroupConfigurations = (courseId) => {
return {
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
isLoadingDenied: loadingStatus === RequestStatus.DENIED,
savingStatus,
contentGroupActions,
experimentConfigurationActions,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,7 @@ const useTextbooks = (courseId, waffleFlags) => {
return {
isLoading: loadingStatus === RequestStatus.IN_PROGRESS,
isLoadingFailed: loadingStatus === RequestStatus.FAILED,
savingStatus,
errorMessage,
textbooks,