From 8037c3f9d9142c2af1a28f65aa45e9918efb89f0 Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Wed, 20 Dec 2023 16:43:37 -0300 Subject: [PATCH] feat: error page on invalid course key --- src/CourseAuthoringPage.jsx | 9 +++++- src/CourseAuthoringPage.test.jsx | 52 ++++++++++++++++++++++++-------- src/data/constants.js | 1 + src/data/thunks.js | 6 +++- src/generic/NotFoundAlert.jsx | 14 +++++++++ 5 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 src/generic/NotFoundAlert.jsx diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index b11dcc7948..3e6b50f2e3 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -9,6 +9,7 @@ import { StudioFooter } from '@edx/frontend-component-footer'; import Header from './header'; import { fetchCourseDetail } from './data/thunks'; import { useModel } from './generic/model-store'; +import NotFoundAlert from './generic/NotFoundAlert'; import PermissionDeniedAlert from './generic/PermissionDeniedAlert'; import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors'; import { RequestStatus } from './data/constants'; @@ -50,10 +51,16 @@ const CourseAuthoringPage = ({ courseId, children }) => { const courseOrg = courseDetail ? courseDetail.org : null; const courseTitle = courseDetail ? courseDetail.name : courseId; const courseAppsApiStatus = useSelector(getCourseAppsApiStatus); - const inProgress = useSelector(state => state.courseDetail.status) === RequestStatus.IN_PROGRESS; + const courseDetailStatus = useSelector(state => state.courseDetail.status); + const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS; const { pathname } = useLocation(); const showHeader = !pathname.includes('/editor'); + if (courseDetailStatus === RequestStatus.NOT_FOUND) { + return ( + + ); + } if (courseAppsApiStatus === RequestStatus.DENIED) { return ( diff --git a/src/CourseAuthoringPage.test.jsx b/src/CourseAuthoringPage.test.jsx index 3e982c5929..5a8a8af75f 100644 --- a/src/CourseAuthoringPage.test.jsx +++ b/src/CourseAuthoringPage.test.jsx @@ -12,8 +12,10 @@ import CourseAuthoringPage from './CourseAuthoringPage'; import PagesAndResources from './pages-and-resources/PagesAndResources'; import { executeThunk } from './utils'; import { fetchCourseApps } from './pages-and-resources/data/thunks'; +import { fetchCourseDetail } from './data/thunks'; const courseId = 'course-v1:edX+TestX+Test_Course'; +const notFoundCourseId = 'course-v1:edX+TestX+Wrong_Course'; let mockPathname = '/evilguy/'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -24,6 +26,19 @@ jest.mock('react-router-dom', () => ({ let axiosMock; let store; +beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: "throwException" }); +}); + describe('Editor Pages Load no header', () => { const mockStoreSuccess = async () => { const apiBaseUrl = getConfig().STUDIO_BASE_URL; @@ -33,18 +48,6 @@ describe('Editor Pages Load no header', () => { }); await executeThunk(fetchCourseApps(courseId), store.dispatch); }; - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - }); test('renders no loading wheel on editor pages', async () => { mockPathname = '/editor/'; await mockStoreSuccess(); @@ -76,3 +79,28 @@ describe('Editor Pages Load no header', () => { expect(wrapper.queryByRole('status')).toBeInTheDocument(); }); }); + +describe('Course authoring page', () => { + const mockStoreNotFound = async () => { + const apiBaseUrl = getConfig().LMS_BASE_URL; + const courseDetailApiUrl = `${apiBaseUrl}/api/courses/v1/courses`; + axiosMock.onGet( + `${courseDetailApiUrl}/${notFoundCourseId}?username=abc123`, + ).reply(404, { + response: { status: 404 }, + }); + await executeThunk(fetchCourseDetail(notFoundCourseId), store.dispatch); + }; + test('renders not found page on non-existent course key', async () => { + await mockStoreNotFound(); + const wrapper = render( + + + + + + , + ); + expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument(); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 5191ea1dfa..bd01f09dda 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -13,6 +13,7 @@ export const RequestStatus = { PENDING: 'pending', CLEAR: 'clear', PARTIAL: 'partial', + NOT_FOUND: 'not-found', }; /** diff --git a/src/data/thunks.js b/src/data/thunks.js index 9a52d4d89d..9c797dc6a5 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -21,7 +21,11 @@ export function fetchCourseDetail(courseId) { canChangeProviders: getAuthenticatedUser().administrator || new Date(courseDetail.start) > new Date(), })); } catch (error) { - dispatch(updateStatus({ courseId, status: RequestStatus.FAILED })); + if (error.response && error.response.status === 404) { + dispatch(updateStatus({ courseId, status: RequestStatus.NOT_FOUND })); + } else { + dispatch(updateStatus({ courseId, status: RequestStatus.FAILED })); + } } }; } diff --git a/src/generic/NotFoundAlert.jsx b/src/generic/NotFoundAlert.jsx new file mode 100644 index 0000000000..8ff9cf4fff --- /dev/null +++ b/src/generic/NotFoundAlert.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Alert } from '@edx/paragon'; + +const NotFoundAlert = () => ( + + + +); + +export default NotFoundAlert;