diff --git a/src/users/UserPage.jsx b/src/users/UserPage.jsx index e74ed2553..e5ee2018e 100644 --- a/src/users/UserPage.jsx +++ b/src/users/UserPage.jsx @@ -172,7 +172,6 @@ export default function UserPage({ location }) { /> diff --git a/src/users/data/api.js b/src/users/data/api.js index 5cb8d5551..4335d09f7 100644 --- a/src/users/data/api.js +++ b/src/users/data/api.js @@ -5,17 +5,31 @@ import * as AppUrls from './urls'; import { isEmail, sortedCompareDates } from '../../utils'; export async function getEntitlements(username, page = 1) { - const baseURL = AppUrls.getEntitlementUrl(); - const queryString = `user=${username}&page=${page}`; - const { data } = await getAuthenticatedHttpClient().get( - `${baseURL}?${queryString}`, - ); - if (data.next !== null) { - const nextPageData = await getEntitlements(username, data.current_page + 1); - data.results = data.results.concat(nextPageData.results); + try { + const baseURL = AppUrls.getEntitlementUrl(); + const queryString = `user=${username}&page=${page}`; + const { data } = await getAuthenticatedHttpClient().get( + `${baseURL}?${queryString}`, + ); + if (data.next !== null) { + const nextPageData = await getEntitlements(username, data.current_page + 1); + data.results = data.results.concat(nextPageData.results); + return data; + } return data; + } catch (error) { + return { + errors: [ + { + code: null, + dismissible: true, + text: JSON.parse(error.customAttributes.httpErrorResponseData), + type: 'danger', + topic: 'entitlements', + }, + ], + }; } - return data; } export async function getEnrollments(username) { @@ -221,7 +235,6 @@ export async function getOnboardingStatus(enrollments, username) { export async function getAllUserData(userIdentifier) { const errors = []; let user = null; - let entitlements = []; let enrollments = []; let verificationStatus = null; let ssoRecords = null; @@ -236,7 +249,6 @@ export async function getAllUserData(userIdentifier) { } } if (user !== null) { - entitlements = await getEntitlements(user.username); enrollments = await getEnrollments(user.username); verificationStatus = await getUserVerificationStatus(user.username); ssoRecords = await getSsoRecords(user.username); @@ -247,7 +259,6 @@ export async function getAllUserData(userIdentifier) { return { errors, user, - entitlements, enrollments, verificationStatus, ssoRecords, diff --git a/src/users/data/api.test.js b/src/users/data/api.test.js index 0504fdaee..0a4b2302d 100644 --- a/src/users/data/api.test.js +++ b/src/users/data/api.test.js @@ -348,7 +348,6 @@ describe('API', () => { it('Successful User Data Retrieval', async () => { mockAdapter.onGet(`${userAccountApiBaseUrl}/${testUsername}`).reply(200, successDictResponse); - mockAdapter.onGet(`${entitlementsApiBaseUrl}&page=1`).reply(200, { results: [], next: null }); mockAdapter.onGet(enrollmentsApiUrl).reply(200, []); mockAdapter.onGet(ssoRecordsApiUrl).reply(200, []); mockAdapter.onGet(verificationDetailsApiUrl).reply(200, {}); @@ -363,7 +362,6 @@ describe('API', () => { ssoRecords: [], verificationStatus: { extraData: {} }, enrollments: [], - entitlements: { results: [], next: null }, onboardingStatus: { ...onboardingDefaultResponse, onboardingStatus: 'No Paid Enrollment' }, }); }); @@ -474,6 +472,11 @@ describe('API', () => { user: testUsername, uuid: 'uuid', }; + it('Unsuccessful fetch', async () => { + mockAdapter.onGet(`${entitlementsApiBaseUrl}&page=1`).reply(() => throwError(400, 'There was an error fetching entitlements.')); + const response = await api.getEntitlements(testUsername); + expect(...response.errors).toEqual({ ...expectedError, text: 'There was an error fetching entitlements.' }); + }); it('Single page result', async () => { const expectedData = { count: 1, diff --git a/src/users/data/test/entitlements.js b/src/users/data/test/entitlements.js index c070d4f09..30f5389fb 100644 --- a/src/users/data/test/entitlements.js +++ b/src/users/data/test/entitlements.js @@ -1,49 +1,54 @@ -const entitlementsData = { - data: { - results: [ - { - user: 'edX', - uuid: 'entitlement-uuid', - courseUuid: 'course-uuid', - enrollmentCourseRun: 'course-v1:testX+test123+2030', - created: Date().toLocaleString(), - modified: Date().toLocaleString(), - expiredAt: Date().toLocaleString(), - mode: 'verified', - orderNumber: '123edX456', - supportDetails: [{ - supportUser: 'admin', - action: 'CREATE', - actionCreated: Date().toLocaleString(), - comments: 'creating entitlement', - unenrolledRun: null, - }, - { - supportUser: 'admin', - action: 'EXPIRE', - actionCreated: Date().toLocaleString(), - comments: 'expiring entitlement', - unenrolledRun: null, - }, - ], +export const entitlementsData = { + results: [ + { + user: 'edX', + uuid: 'entitlement-uuid', + courseUuid: 'course-uuid', + enrollmentCourseRun: 'course-v1:testX+test123+2030', + created: Date().toLocaleString(), + modified: Date().toLocaleString(), + expiredAt: Date().toLocaleString(), + mode: 'verified', + orderNumber: '123edX456', + supportDetails: [{ + supportUser: 'admin', + action: 'CREATE', + actionCreated: Date().toLocaleString(), + comments: 'creating entitlement', + unenrolledRun: null, }, { - user: 'edX', - uuid: 'entitlement-1-uuid', - courseUuid: 'course-1-uuid', - enrollmentCourseRun: 'course-v1:testX+test123+2040', - created: Date().toLocaleString(), - modified: Date().toLocaleString(), - expiredAt: Date().toLocaleString(), - mode: 'professional', - orderNumber: '123edX456789', - supportDetails: [], + supportUser: 'admin', + action: 'EXPIRE', + actionCreated: Date().toLocaleString(), + comments: 'expiring entitlement', + unenrolledRun: null, }, - ], - }, - user: 'edX', - expanded: true, - changeHandler: jest.fn(() => {}), + ], + }, + { + user: 'edX', + uuid: 'entitlement-1-uuid', + courseUuid: 'course-1-uuid', + enrollmentCourseRun: null, + created: Date().toLocaleString(), + modified: Date().toLocaleString(), + expiredAt: null, + mode: 'professional', + orderNumber: '123edX456789', + supportDetails: [], + }, + ], }; -export default entitlementsData; +export const entitlementsErrors = { + errors: [ + { + code: null, + dismissible: true, + text: 'Test Error', + type: 'danger', + topic: 'entitlements', + }, + ], +}; diff --git a/src/users/entitlements/Entitlements.jsx b/src/users/entitlements/Entitlements.jsx index 67813f881..c351ff330 100644 --- a/src/users/entitlements/Entitlements.jsx +++ b/src/users/entitlements/Entitlements.jsx @@ -1,5 +1,5 @@ import React, { - useMemo, useState, useCallback, useRef, useLayoutEffect, useContext, + useMemo, useState, useCallback, useRef, useLayoutEffect, useContext, useEffect, } from 'react'; import PropTypes from 'prop-types'; @@ -9,14 +9,16 @@ import { import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import EntitlementForm from './EntitlementForm'; import { CREATE, REISSUE, EXPIRE } from './EntitlementActions'; +import PageLoading from '../../components/common/PageLoading'; import Table from '../../Table'; import CourseSummary from '../courseSummary/CourseSummary'; -import { getCourseData } from '../data/api'; +import { getCourseData, getEntitlements } from '../data/api'; import UserMessagesContext from '../../userMessages/UserMessagesContext'; import { formatDate, sort } from '../../utils'; +import AlertList from '../../userMessages/AlertList'; export default function Entitlements({ - data, changeHandler, user, expanded, + changeHandler, user, expanded, }) { const { add, clear } = useContext(UserMessagesContext); const [sortColumn, setSortColumn] = useState('created'); @@ -26,12 +28,25 @@ export default function Entitlements({ const [courseSummaryUUID, setCourseSummaryUUID] = useState(null); const [courseSummaryData, setCourseSummaryData] = useState(null); const [courseSummaryErrors, setCourseSummaryErrors] = useState(false); + const [entitlementData, setEntitlementData] = useState(null); const [entitlementDetailModalIsOpen, setEntitlementDetailModalIsOpen] = useState(false); const [entitlementSupportDetailsTitle, setEntitlementSupportDetailsTitle] = useState(''); const [entitlementSupportDetails, setEntitlementSupportDetails] = useState([]); const formRef = useRef(null); const summaryRef = useRef(null); + useEffect(() => { + getEntitlements(user).then((result) => { + const camelCaseResult = camelCaseObject(result); + if (camelCaseResult.errors) { + camelCaseResult.errors.forEach(error => add(error)); + setEntitlementData({ results: [] }); + } else { + setEntitlementData(camelCaseResult); + } + }); + }, [user]); + useLayoutEffect(() => { if (formType != null) { formRef.current.focus(); @@ -79,10 +94,10 @@ export default function Entitlements({ }); const tableData = useMemo(() => { - if (data === null) { + if (entitlementData === null) { return []; } - return data.results.map(entitlement => ({ + return entitlementData.results.map(entitlement => ({ courseUuid: { displayValue: ( )} + {formType !== null ? ( - sort(firstElement, secondElement, sortColumn, sortDirection), + {!entitlementData + ? + : ( +
sort(firstElement, secondElement, sortColumn, sortDirection), + )} + columns={columns} + tableSortable + defaultSortedColumn="created" + defaultSortDirection="desc" + /> )} - columns={columns} - tableSortable - defaultSortedColumn="created" - defaultSortDirection="desc" - /> + ); } Entitlements.propTypes = { - data: PropTypes.shape({ - results: PropTypes.arrayOf(PropTypes.object), - }), changeHandler: PropTypes.func.isRequired, user: PropTypes.string.isRequired, expanded: PropTypes.bool, }; Entitlements.defaultProps = { - data: null, expanded: false, }; diff --git a/src/users/entitlements/Entitlements.test.jsx b/src/users/entitlements/Entitlements.test.jsx index 48bd11ccb..533fe2f2d 100644 --- a/src/users/entitlements/Entitlements.test.jsx +++ b/src/users/entitlements/Entitlements.test.jsx @@ -3,7 +3,7 @@ import { mount } from 'enzyme'; import { waitForComponentToPaint } from '../../setupTest'; import Entitlements from './Entitlements'; -import entitlementsData from '../data/test/entitlements'; +import { entitlementsData, entitlementsErrors } from '../data/test/entitlements'; import CourseSummaryData from '../data/test/courseSummary'; import UserMessageProvider from '../../userMessages/UserMessagesProvider'; import * as api from '../data/api'; @@ -16,9 +16,16 @@ const EntitlementsPageWrapper = (props) => ( describe('Entitlements Listing', () => { let wrapper; - - beforeEach(() => { - wrapper = mount(); + const props = { + user: 'edX', + expanded: true, + changeHandler: jest.fn(() => {}), + }; + + beforeEach(async () => { + jest.spyOn(api, 'getEntitlements').mockImplementationOnce(() => Promise.resolve(entitlementsData)); + wrapper = mount(); + await waitForComponentToPaint(wrapper); }); afterEach(() => { @@ -42,12 +49,24 @@ describe('Entitlements Listing', () => { expect(collapsible.text()).toEqual('Entitlements (2)'); }); - it('No entitlements data', () => { - wrapper = mount(); + it('No entitlements data', async () => { + jest.spyOn(api, 'getEntitlements').mockImplementationOnce(() => Promise.resolve({ results: [] })); + wrapper = mount(); + await waitForComponentToPaint(wrapper); const collapsible = wrapper.find('CollapsibleAdvanced').find('.collapsible-trigger').hostNodes(); expect(collapsible.text()).toEqual('Entitlements (0)'); }); + it('Error fetching entitlements', async () => { + jest.spyOn(api, 'getEntitlements').mockImplementationOnce(() => Promise.resolve(entitlementsErrors)); + wrapper = mount(); + await waitForComponentToPaint(wrapper); + + const alert = wrapper.find('div.alert'); + console.log(alert); + expect(alert.test).toEqual(entitlementsErrors.errors.text); + }); + it('Sorting Columns Button Enabled by default', () => { const dataTable = wrapper.find('table.table'); const tableHeaders = dataTable.find('thead tr th'); @@ -71,7 +90,6 @@ describe('Entitlements Listing', () => { }); it('Support Details data', () => { - wrapper = mount(); const tableRowsLengths = [2, 0]; const tableData = wrapper.find('table.table'); @@ -94,86 +112,61 @@ describe('Entitlements Listing', () => { }); }); - describe('Expire and Reissue entitlement buttons', () => { - describe('Expire Entitlement button', () => { - it('Disabled Expire entitlement button', () => { - const tableData = wrapper.find('table.table'); - tableData.find('tbody tr').forEach(row => { - const expireButton = row.find('button.btn-outline-danger'); - - expect(expireButton.text()).toEqual('Expire'); - expect(expireButton.prop('disabled')).toBeTruthy(); - }); - }); - - it('Enabled Expire entitlement button', () => { - let data = [...entitlementsData.data.results]; - data = data.map(item => ( - { - ...item, - expiredAt: null, - } - )); - wrapper = mount(); - const tableData = wrapper.find('table.table'); - tableData.find('tbody tr').forEach(row => { - const expireButton = row.find('button.btn-outline-danger'); - - expect(expireButton.text()).toEqual('Expire'); - expect(expireButton.prop('disabled')).toBeFalsy(); - expireButton.simulate('click'); - - const expireEntitlementForm = wrapper.find('ExpireEntitlementForm'); - expect(expireEntitlementForm.html()).toEqual(expect.stringContaining('Expire Entitlement')); - expireEntitlementForm.find('button.btn-outline-secondary').simulate('click'); - expect(wrapper.find('ExpireEntitlementForm')).toEqual({}); - }); - }); + describe('Expire Entitlement button', () => { + it('Disabled Expire entitlement button', () => { + const tableData = wrapper.find('table.table'); + // We're only checking row 0 of the table since it has the button Expire Button disabled + const row = tableData.find('tbody tr').at(0); + const expireButton = row.find('button.btn-outline-danger'); + + expect(expireButton.text()).toEqual('Expire'); + expect(expireButton.prop('disabled')).toBeTruthy(); }); - describe('Reissue entitlement button', () => { - it('Disabled Reissue entitlement button', () => { - let data = [...entitlementsData.data.results]; - data = data.map(item => ( - { - ...item, - enrollmentCourseRun: null, - } - )); - wrapper = mount(); - const tableData = wrapper.find('table.table'); - - tableData.find('tbody tr').forEach(row => { - const reissueButton = row.find('button#reissue').last(); - expect(reissueButton.text()).toEqual('Reissue'); - expect(reissueButton.prop('disabled')).toBeTruthy(); - }); - }); - it('Enabled Reissue entitlement button', () => { - const tableData = wrapper.find('table.table'); - tableData.find('tbody tr').forEach((row) => { - const reissueButton = row.find('button.btn-outline-primary').last(); - - expect(reissueButton.text()).toEqual('Reissue'); - expect(reissueButton.prop('disabled')).toBeFalsy(); - reissueButton.simulate('click'); - - const reissueEntitlementForm = wrapper.find('ReissueEntitlementForm'); - expect(reissueEntitlementForm.html()).toEqual(expect.stringContaining('Reissue Entitlement')); - reissueEntitlementForm.find('button.btn-outline-secondary').simulate('click'); - expect(wrapper.find('ReissueEntitlementForm')).toEqual({}); - }); - }); + it('Enabled Expire entitlement button', () => { + const tableData = wrapper.find('table.table'); + // We're only checking row 1 of the table since the expire button is not disabled + const row = tableData.find('tbody tr').at(1); + const expireButton = row.find('button.btn-outline-danger'); + + expect(expireButton.text()).toEqual('Expire'); + expect(expireButton.prop('disabled')).toBeFalsy(); + expireButton.simulate('click'); + + const expireEntitlementForm = wrapper.find('ExpireEntitlementForm'); + expect(expireEntitlementForm.html()).toEqual(expect.stringContaining('Expire Entitlement')); + expireEntitlementForm.find('button.btn-outline-secondary').simulate('click'); + expect(wrapper.find('ExpireEntitlementForm')).toEqual({}); }); }); - describe('Course Summary button', () => { - beforeEach(() => { - // Having only one element in the table to avoid unexpected behavior on async operations per row - const data = [entitlementsData.data.results[0]]; - wrapper = mount(); + describe('Reissue entitlement button', () => { + it('Enabled Reissue entitlement button', () => { + const tableData = wrapper.find('table.table'); + // We're only checking row 0 of the table since the Reissue button is not disabled + const row = tableData.find('tbody tr').at(0); + const reissueButton = row.find('button#reissue').last(); + + expect(reissueButton.text()).toEqual('Reissue'); + expect(reissueButton.prop('disabled')).toBeFalsy(); + reissueButton.simulate('click'); + + const reissueEntitlementForm = wrapper.find('ReissueEntitlementForm'); + expect(reissueEntitlementForm.html()).toEqual(expect.stringContaining('Reissue Entitlement')); + reissueEntitlementForm.find('button.btn-outline-secondary').simulate('click'); + expect(wrapper.find('ReissueEntitlementForm')).toEqual({}); }); + it('Disabled Reissue entitlement button', () => { + const tableData = wrapper.find('table.table'); + // We're only checking row 1 of the table since it has the button Reissue Button disabled + const row = tableData.find('tbody tr').at(1); + const reissueButton = row.find('button#reissue').last(); + expect(reissueButton.text()).toEqual('Reissue'); + expect(reissueButton.prop('disabled')).toBeTruthy(); + }); + }); + describe('Course Summary button', () => { it('Successful course summary fetch', async () => { const apiMock = jest.spyOn(api, 'getCourseData').mockImplementationOnce(() => Promise.resolve(CourseSummaryData.courseData)); const row = wrapper.find('table.table').find('tbody tr').first();