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-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-team/CourseTeam.test.jsx b/src/course-team/CourseTeam.test.jsx index 4f33788744..31f9c78fdc 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,18 @@ 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); + }); + }); }); 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/export-page/CourseExportPage.test.jsx b/src/export-page/CourseExportPage.test.jsx index 05db07d5bf..debfe72762 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,17 @@ 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); + }); }); diff --git a/src/grading-settings/GradingSettings.jsx b/src/grading-settings/GradingSettings.jsx index 8ddd7a9bb5..bcb3a8f089 100644 --- a/src/grading-settings/GradingSettings.jsx +++ b/src/grading-settings/GradingSettings.jsx @@ -8,7 +8,6 @@ import { useGradingSettings, useGradingSettingUpdater, } from 'CourseAuthoring/grading-settings/data/apiHooks'; -import { RequestStatus } from 'CourseAuthoring/data/constants'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; @@ -34,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, @@ -48,7 +49,7 @@ const GradingSettings = ({ courseId }) => { const courseAssignmentLists = gradingSettings?.courseAssignmentLists; const courseGradingDetails = gradingSettings?.courseDetails; - const isLoadingDenied = isCourseSettingsLoading === RequestStatus.DENIED; + const isLoadingDenied = isGradingSettingsError || isCourseSettingsError; const [showSuccessAlert, setShowSuccessAlert] = useState(false); const isLoading = isCourseSettingsLoading || isGradingSettingsLoading; const [isQueryPending, setIsQueryPending] = useState(false); 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/import-page/CourseImportPage.test.jsx b/src/import-page/CourseImportPage.test.jsx index 8359312692..a72363171a 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,17 @@ 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); + }); }); 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, + ); + }); + }); });