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

feat: error page on invalid course key #761

Merged
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
9 changes: 8 additions & 1 deletion src/CourseAuthoringPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<NotFoundAlert />
);
}
if (courseAppsApiStatus === RequestStatus.DENIED) {
return (
<PermissionDeniedAlert />
Expand Down
79 changes: 67 additions & 12 deletions src/CourseAuthoringPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId} />
</IntlProvider>
</AppProvider>
,
);
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(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
,
);
expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();
expect(wrapper.queryByTestId('notFoundAlert')).not.toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const RequestStatus = {
PENDING: 'pending',
CLEAR: 'clear',
PARTIAL: 'partial',
NOT_FOUND: 'not-found',
};

/**
Expand Down
6 changes: 5 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
}
};
}
14 changes: 14 additions & 0 deletions src/generic/NotFoundAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';

const NotFoundAlert = () => (
<Alert variant="danger" data-testid="notFoundAlert">
<FormattedMessage
id="authoring.alert.error.notfound"
defaultMessage="Not found."
/>
</Alert>
);

export default NotFoundAlert;