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..c7eeeb9be8 100644
--- a/src/CourseAuthoringPage.test.jsx
+++ b/src/CourseAuthoringPage.test.jsx
@@ -12,6 +12,7 @@ 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';
let mockPathname = '/evilguy/';
@@ -24,6 +25,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());
+});
+
describe('Editor Pages Load no header', () => {
const mockStoreSuccess = async () => {
const apiBaseUrl = getConfig().STUDIO_BASE_URL;
@@ -33,18 +47,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 +78,56 @@ describe('Editor Pages Load no header', () => {
expect(wrapper.queryByRole('status')).toBeInTheDocument();
});
});
+
+describe('Course authoring page', () => {
+ const lmsApiBaseUrl = getConfig().LMS_BASE_URL;
+ const courseDetailApiUrl = `${lmsApiBaseUrl}/api/courses/v1/courses`;
+ const mockStoreNotFound = async () => {
+ axiosMock.onGet(
+ `${courseDetailApiUrl}/${courseId}?username=abc123`,
+ ).reply(404, {
+ response: { status: 404 },
+ });
+ await executeThunk(fetchCourseDetail(courseId), store.dispatch);
+ };
+ const mockStoreError = async () => {
+ axiosMock.onGet(
+ `${courseDetailApiUrl}/${courseId}?username=abc123`,
+ ).reply(500, {
+ response: { status: 500 },
+ });
+ await executeThunk(fetchCourseDetail(courseId), store.dispatch);
+ };
+ test('renders not found page on non-existent course key', async () => {
+ await mockStoreNotFound();
+ const wrapper = render(
+
+
+
+
+
+ ,
+ );
+ expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
+ });
+ test('does not render not found page on other kinds of error', async () => {
+ await mockStoreError();
+ // Currently, loading errors are not handled, so we wait for the child
+ // content to be rendered -which happens when request status is no longer
+ // IN_PROGRESS but also not NOT_FOUND or DENIED- then check that the not
+ // found alert is not present.
+ const contentTestId = 'courseAuthoringPageContent';
+ const wrapper = render(
+
+
+
+
+
+
+
+ ,
+ );
+ expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();
+ expect(wrapper.queryByTestId('notFoundAlert')).not.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;