Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error handling #1079

Merged
merged 3 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/course-checklist/CourseChecklist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<>
Expand Down
15 changes: 15 additions & 0 deletions src/course-checklist/CourseChecklist.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
6 changes: 5 additions & 1 deletion src/course-checklist/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
}
};
}
Expand Down
22 changes: 22 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const CourseOutline = ({ courseId }) => {
sectionsList,
isCustomRelativeDatesActive,
isLoading,
isLoadingDenied,
isReIndexShow,
showSuccessAlert,
isSectionsExpanded,
Expand Down Expand Up @@ -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>
Expand Down
14 changes: 14 additions & 0 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
14 changes: 10 additions & 4 deletions src/course-outline/data/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}));
}
}
};
}
Expand Down
1 change: 1 addition & 0 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ const useCourseOutline = ({ courseId }) => {
sectionsList,
isCustomRelativeDatesActive,
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
isLoadingDenied: outlineIndexLoadingStatus === RequestStatus.DENIED,
isReIndexShow: Boolean(reindexLink),
showSuccessAlert,
isDisabledReindexButton,
Expand Down
10 changes: 10 additions & 0 deletions src/course-team/CourseTeam.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -35,6 +36,7 @@ const CourseTeam = ({ courseId }) => {
courseTeamUsers,
currentUserEmail,
isLoading,
isLoadingDenied,
isSingleAdmin,
isFormVisible,
isQueryPending,
Expand All @@ -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 <></>;
Expand Down
29 changes: 28 additions & 1 deletion src/course-team/CourseTeam.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import {
render,
fireEvent,
Expand All @@ -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;
Expand Down Expand Up @@ -219,4 +219,31 @@ describe('<CourseTeam />', () => {
await executeThunk(changeRoleTeamUserQuery(courseId, '[email protected]', { 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);
});
});
});
6 changes: 5 additions & 1 deletion src/course-team/data/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
Expand Down
1 change: 1 addition & 0 deletions src/course-team/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const useCourseTeam = ({ courseId }) => {
courseTeamUsers,
currentUserEmail,
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
isSingleAdmin,
isFormVisible,
isAllowActions,
Expand Down
10 changes: 10 additions & 0 deletions src/course-updates/CourseUpdates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
Expand Down
24 changes: 22 additions & 2 deletions src/course-updates/CourseUpdates.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
30 changes: 22 additions & 8 deletions src/course-updates/data/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}));
}
}
};
}
Expand Down Expand Up @@ -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 },
}));
}
}
};
}
Expand Down
10 changes: 10 additions & 0 deletions src/export-page/CourseExportPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand All @@ -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>
Expand Down
Loading