diff --git a/src/components/NavigationBar/data/__factories__/index.js b/src/components/NavigationBar/data/__factories__/index.js new file mode 100644 index 00000000..d199bffa --- /dev/null +++ b/src/components/NavigationBar/data/__factories__/index.js @@ -0,0 +1 @@ +import './navigationBar.factory'; diff --git a/src/components/NavigationBar/data/__factories__/navigationBar.factory.js b/src/components/NavigationBar/data/__factories__/navigationBar.factory.js new file mode 100644 index 00000000..172bcd83 --- /dev/null +++ b/src/components/NavigationBar/data/__factories__/navigationBar.factory.js @@ -0,0 +1,50 @@ +import { Factory } from 'rosie'; + +import { getApiBaseUrl } from '../../../../data/constants'; + +Factory.define('navigationBar') + .attr('can_show_upgrade_sock', null, false) + .attr('can_view_certificate', null, false) + .attr('celebrations', null, { + first_section: false, streak_discount_enabled: false, streak_length_to_celebrate: null, weekly_goal: false, + }) + .option('hasCourseAccess', null, true) + + .attr('course_access', ['hasCourseAccess'], (hasCourseAccess) => ({ + additional_context_user_message: null, + developer_message: null, + error_code: null, + has_access: hasCourseAccess, + user_fragment: null, + user_message: null, + })) + .option('course_id', null, 'course-v1:edX+DemoX+Demo_Course') + .attr('is_enrolled', null, false) + .attr('is_self_paced', null, false) + .attr('is_staff', null, true) + .attr('number', null, 'DemoX') + .attr('org', null, 'edX') + .attr('original_user_is_staff', null, true) + .attr('title', null, 'Demonstration Course') + .attr('username', null, 'edx') + .attr('tabs', ['course_id'], (idx, courseId) => [ + { + tab_id: 'courseware', + title: 'Course', + url: `${getApiBaseUrl}/course/${courseId}/home`, + }, + { + tab_id: 'progress', + title: 'Progress', + url: `${getApiBaseUrl}/course/${courseId}/progress`, + }, + { + tab_id: 'discussion', + title: 'Discussion', + url: `${getApiBaseUrl}/course/${courseId}/discussion/forum/`, + }, + { + tab_id: 'instructor', + title: 'Instructor', + url: `${getApiBaseUrl}/course/${courseId}/instructor`, + }]); diff --git a/src/components/NavigationBar/data/api.js b/src/components/NavigationBar/data/api.js index 222cbad0..3b4ce1c6 100644 --- a/src/components/NavigationBar/data/api.js +++ b/src/components/NavigationBar/data/api.js @@ -4,6 +4,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getApiBaseUrl } from '../../../data/constants'; +export const getCourseMetadataApiUrl = (courseId) => `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`; + function normalizeCourseHomeCourseMetadata(metadata, rootSlug) { const data = camelCaseObject(metadata); return { @@ -21,7 +23,7 @@ function normalizeCourseHomeCourseMetadata(metadata, rootSlug) { } export async function getCourseHomeCourseMetadata(courseId, rootSlug) { - const url = `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`; + const url = getCourseMetadataApiUrl(courseId); // don't know the context of adding timezone in url. hence omitting it // url = appendBrowserTimezoneToUrl(url); const { data } = await getAuthenticatedHttpClient().get(url); diff --git a/src/components/NavigationBar/data/api.test.js b/src/components/NavigationBar/data/api.test.js new file mode 100644 index 00000000..8de071ff --- /dev/null +++ b/src/components/NavigationBar/data/api.test.js @@ -0,0 +1,65 @@ +import MockAdapter from 'axios-mock-adapter'; +import { Factory } from 'rosie'; + +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { initializeMockApp } from '@edx/frontend-platform/testing'; + +import { initializeStore } from '../../../store'; +import { executeThunk } from '../../../test-utils'; +import { getCourseMetadataApiUrl } from './api'; +import { fetchTab } from './thunks'; + +import './__factories__'; + +const courseId = 'course-v1:edX+TestX+Test_Course'; +let axiosMock = null; +let store; + +describe('Navigation bar api tests', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + store = initializeStore(); + }); + + afterEach(() => { + axiosMock.reset(); + }); + + it('Successfully get navigation tabs', async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, (Factory.build('navigationBar', 1))); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); + + expect(store.getState().courseTabs.tabs).toHaveLength(4); + expect(store.getState().courseTabs.courseStatus).toEqual('loaded'); + }); + + it('Failed to get navigation tabs', async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(404); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); + + expect(store.getState().courseTabs.courseStatus).toEqual('failed'); + }); + + it('Denied to get navigation tabs', async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(403, {}); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); + + expect(store.getState().courseTabs.courseStatus).toEqual('denied'); + }); + + it('Denied to get navigation bar when user has no access on course', async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, + (Factory.build('navigationBar', 1, { hasCourseAccess: false }))); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); + + expect(store.getState().courseTabs.courseStatus).toEqual('denied'); + }); +}); diff --git a/src/components/NavigationBar/data/thunks.js b/src/components/NavigationBar/data/thunks.js index f1a4637c..bfff811f 100644 --- a/src/components/NavigationBar/data/thunks.js +++ b/src/components/NavigationBar/data/thunks.js @@ -1,6 +1,7 @@ /* eslint-disable import/prefer-default-export, no-unused-expressions */ import { logError } from '@edx/frontend-platform/logging'; +import { getHttpErrorStatus } from '../../../discussions/utils'; import { getCourseHomeCourseMetadata } from './api'; import { fetchTabDenied, @@ -26,7 +27,11 @@ export function fetchTab(courseId, rootSlug) { })); } } catch (e) { - dispatch(fetchTabFailure({ courseId })); + if (getHttpErrorStatus(e) === 403) { + dispatch(fetchTabDenied({ courseId })); + } else { + dispatch(fetchTabFailure({ courseId })); + } logError(e); } }; diff --git a/src/discussions/discussions-home/DiscussionsHome.test.jsx b/src/discussions/discussions-home/DiscussionsHome.test.jsx index 0302515b..1abac37a 100644 --- a/src/discussions/discussions-home/DiscussionsHome.test.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.test.jsx @@ -12,6 +12,8 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; +import { getCourseMetadataApiUrl } from '../../components/NavigationBar/data/api'; +import { fetchTab } from '../../components/NavigationBar/data/thunks'; import { getApiBaseUrl } from '../../data/constants'; import { initializeStore } from '../../store'; import { executeThunk } from '../../test-utils'; @@ -28,6 +30,7 @@ import DiscussionsHome from './DiscussionsHome'; import '../posts/data/__factories__/threads.factory'; import '../in-context-topics/data/__factories__/inContextTopics.factory'; import '../topics/data/__factories__/topics.factory'; +import '../../components/NavigationBar/data/__factories__/navigationBar.factory'; const courseConfigApiUrl = getCourseConfigApiUrl(); let axiosMock; @@ -247,4 +250,12 @@ describe('DiscussionsHome', () => { expect(screen.queryByText('No topic selected')).toBeInTheDocument(); }); + + it('should display navigation tabs', async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, (Factory.build('navigationBar', 1))); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); + renderComponent(`/${courseId}/topics`); + + expect(screen.queryByText('Discussion')).toBeInTheDocument(); + }); });