diff --git a/src/context/course-info-context.js b/src/context/course-info-context.js
index 107dd0df..081b173f 100644
--- a/src/context/course-info-context.js
+++ b/src/context/course-info-context.js
@@ -1,6 +1,6 @@
import { createContext } from 'react';
-export const CourseInfoContext = createContext('course-info', {
+export const CourseInfoContext = createContext({
courseId: null,
unitId: null,
isUpgradeEligible: false,
diff --git a/src/hooks/index.js b/src/hooks/index.js
index 22f1a63c..bcc999ad 100644
--- a/src/hooks/index.js
+++ b/src/hooks/index.js
@@ -1,3 +1,2 @@
-/* eslint-disable import/prefer-default-export */
export { default as useCourseUpgrade } from './use-course-upgrade';
export { default as useTrackEvent } from './use-track-event';
diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js
index f5544890..8870dc5a 100644
--- a/src/hooks/use-course-upgrade.js
+++ b/src/hooks/use-course-upgrade.js
@@ -23,6 +23,7 @@ export default function useCourseUpgrade() {
if (auditTrial?.expirationDate) {
const auditTrialExpirationDate = new Date(auditTrial.expirationDate);
+
auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay);
auditTrialExpired = auditTrialDaysRemaining < 0;
diff --git a/src/hooks/use-course-upgrade.test.jsx b/src/hooks/use-course-upgrade.test.jsx
new file mode 100644
index 00000000..2bb4aeb7
--- /dev/null
+++ b/src/hooks/use-course-upgrade.test.jsx
@@ -0,0 +1,159 @@
+import { renderHook as rtlRenderHook } from '@testing-library/react-hooks';
+import { useSelector } from 'react-redux';
+import { useModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved
+import { CourseInfoProvider } from '../context';
+import useCourseUpgrade from './use-course-upgrade';
+
+jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }), { virtual: true });
+jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
+
+const mockedUpgradeUrl = 'https://upgrade.edx/course/test';
+const mockedAuditTrialLengthDays = 7;
+
+const contextWrapper = ({ courseInfo }) => function Wrapper({ children }) { // eslint-disable-line react/prop-types
+ return (
+
+ {children}
+
+ );
+};
+
+const renderHook = ({
+ courseInfo, offer = {}, verifiedMode = {}, state = { learningAssistant: {} },
+}) => {
+ useModel.mockImplementation((model) => {
+ switch (model) {
+ case 'coursewareMeta': return { offer };
+ case 'courseHomeMeta': return { verifiedMode };
+ default: {
+ throw new Error('Model not mocked');
+ }
+ }
+ });
+
+ useSelector.mockReturnValue(state.learningAssistant);
+
+ return rtlRenderHook(
+ () => useCourseUpgrade(),
+ { wrapper: contextWrapper({ courseInfo }) },
+ );
+};
+
+describe('useCourseUpgrade()', () => {
+ beforeAll(() => jest.useFakeTimers().setSystemTime(new Date('2024-01-10 09:00:00')));
+ afterAll(() => jest.useRealTimers());
+ afterEach(() => jest.resetAllMocks());
+
+ it('should return { upgradeable: false } if not eligible', () => {
+ const { result } = renderHook({ courseInfo: { isUpgradeEligible: false } });
+
+ expect(result.current).toEqual({ upgradeable: false });
+ });
+
+ it('should return { upgradeable: false } if missing upgradeUrl', () => {
+ const { result } = renderHook({ courseInfo: { isUpgradeEligible: true } });
+
+ expect(result.current).toEqual({ upgradeable: false });
+ });
+
+ it('should return { upgradeable: true } if eligible and upgradeable and no trial info for both offer and verifiedMode urls', () => {
+ const expected = {
+ upgradeable: true,
+ auditTrial: undefined,
+ auditTrialDaysRemaining: undefined,
+ auditTrialExpired: false,
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ upgradeUrl: mockedUpgradeUrl,
+ };
+
+ const { result: resultOffer } = renderHook({
+ courseInfo: { isUpgradeEligible: true },
+ offer: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ state: {
+ learningAssistant: {
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ },
+ },
+ });
+
+ expect(resultOffer.current).toEqual(expected);
+
+ const { result: resultVerified } = renderHook({
+ courseInfo: { isUpgradeEligible: true },
+ verifiedMode: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ state: {
+ learningAssistant: {
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ },
+ },
+ });
+
+ expect(resultVerified.current).toEqual(expected);
+ });
+
+ it('should return trial info if enabled and not expired', () => {
+ const { result } = renderHook({
+ courseInfo: { isUpgradeEligible: true },
+ offer: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ verifiedMode: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ state: {
+ learningAssistant: {
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ auditTrial: {
+ expirationDate: '2024-01-15 09:00:00',
+ },
+ },
+ },
+ });
+
+ expect(result.current).toEqual({
+ auditTrial: {
+ expirationDate: '2024-01-15 09:00:00',
+ },
+ auditTrialDaysRemaining: 5,
+ auditTrialExpired: false,
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ upgradeUrl: 'https://upgrade.edx/course/test',
+ upgradeable: true,
+ });
+ });
+
+ it('should return trial info if expired', () => {
+ const { result } = renderHook({
+ courseInfo: { isUpgradeEligible: true },
+ offer: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ verifiedMode: {
+ upgradeUrl: mockedUpgradeUrl,
+ },
+ state: {
+ learningAssistant: {
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ auditTrial: {
+ expirationDate: '2024-01-05 09:00:00',
+ },
+ },
+ },
+ });
+
+ expect(result.current).toEqual({
+ auditTrial: {
+ expirationDate: '2024-01-05 09:00:00',
+ },
+ auditTrialDaysRemaining: -5,
+ auditTrialExpired: true,
+ auditTrialLengthDays: mockedAuditTrialLengthDays,
+ upgradeUrl: 'https://upgrade.edx/course/test',
+ upgradeable: true,
+ });
+ });
+});
diff --git a/src/hooks/use-track-event.test.jsx b/src/hooks/use-track-event.test.jsx
new file mode 100644
index 00000000..846f356f
--- /dev/null
+++ b/src/hooks/use-track-event.test.jsx
@@ -0,0 +1,54 @@
+import { renderHook as rtlRenderHook } from '@testing-library/react-hooks';
+import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+import { CourseInfoProvider } from '../context';
+
+import useTrackEvent from './use-track-event';
+
+const mockedUserId = 123;
+const mockedCourseId = 'some-course-id';
+const mockedModuleId = 'some-module-id';
+
+jest.mock('@edx/frontend-platform/analytics', () => ({
+ sendTrackEvent: jest.fn(),
+}));
+
+const mockedAuthenticatedUser = { userId: mockedUserId };
+jest.mock('@edx/frontend-platform/auth', () => ({
+ getAuthenticatedUser: () => mockedAuthenticatedUser,
+}));
+
+const contextWrapper = ({ courseInfo }) => function Wrapper({ children }) { // eslint-disable-line react/prop-types
+ return (
+
+ {children}
+
+ );
+};
+
+const renderHook = ({
+ courseInfo,
+}) => rtlRenderHook(
+ () => useTrackEvent(),
+ { wrapper: contextWrapper({ courseInfo }) },
+);
+
+describe('useCourseUpgrade()', () => {
+ afterEach(() => jest.resetAllMocks());
+
+ it('should return a track method that calls sendTrackEvent with the contextual information', () => {
+ const { result } = renderHook({ courseInfo: { courseId: mockedCourseId, moduleId: mockedModuleId } });
+
+ const { track } = result.current;
+
+ const eventLabel = 'some-cool-event-to-track';
+
+ track(eventLabel, { some_extra_prop: 42 });
+
+ expect(sendTrackEvent).toHaveBeenCalledWith(eventLabel, {
+ course_id: mockedCourseId,
+ user_id: mockedUserId,
+ module_id: mockedModuleId,
+ some_extra_prop: 42,
+ });
+ });
+});