From 71e99a3f6896acee6aeaa78c1f6b60bdf77adf2e Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 18 Dec 2024 14:38:13 -0500 Subject: [PATCH] fix: use correct enterpriseFeatures in loaders (#1241) --- .../queries/extractEnterpriseFeatures.test.js | 90 +++++++++++++++++++ .../data/queries/extractEnterpriseFeatures.ts | 27 ++++++ src/components/app/data/queries/index.js | 1 + .../app/routes/loaders/rootLoader.ts | 4 +- .../dashboard/data/dashboardLoader.test.jsx | 2 +- .../dashboard/data/dashboardLoader.ts | 15 +++- src/types.d.ts | 10 ++- 7 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 src/components/app/data/queries/extractEnterpriseFeatures.test.js create mode 100644 src/components/app/data/queries/extractEnterpriseFeatures.ts diff --git a/src/components/app/data/queries/extractEnterpriseFeatures.test.js b/src/components/app/data/queries/extractEnterpriseFeatures.test.js new file mode 100644 index 000000000..6b4d41013 --- /dev/null +++ b/src/components/app/data/queries/extractEnterpriseFeatures.test.js @@ -0,0 +1,90 @@ +import { when, resetAllWhenMocks } from 'jest-when'; +import { authenticatedUserFactory, enterpriseCustomerFactory } from '../services/data/__factories__'; +import extractEnterpriseFeatures from './extractEnterpriseFeatures'; +import { queryEnterpriseLearner } from './queries'; + +const mockEnsureQueryData = jest.fn(); +const mockQueryClient = { + ensureQueryData: mockEnsureQueryData, +}; +const mockAuthenticatedUser = authenticatedUserFactory(); +const mockEnterpriseCustomer = enterpriseCustomerFactory(); + +const getQueryEnterpriseLearner = ({ hasEnterpriseSlug = true } = {}) => queryEnterpriseLearner( + mockAuthenticatedUser.username, + hasEnterpriseSlug ? mockEnterpriseCustomer.slug : undefined, +); + +describe('extractEnterpriseFeatures', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetAllWhenMocks(); + }); + + it.each([ + { + routeEnterpriseSlug: mockEnterpriseCustomer.slug, + enterpriseFeatures: { featureA: true, featureB: false }, + }, + { + routeEnterpriseSlug: undefined, + enterpriseFeatures: { featureA: true, featureB: false }, + }, + { + routeEnterpriseSlug: mockEnterpriseCustomer.slug, + enterpriseFeatures: { featureA: true, featureB: false }, + }, + ])('should return the correct enterprise features (%s)', async ({ + routeEnterpriseSlug, + enterpriseFeatures, + }) => { + const args = { + queryClient: mockQueryClient, + authenticatedUser: mockAuthenticatedUser, + enterpriseSlug: routeEnterpriseSlug, + }; + + const queryEnterpriseLearnerResult = { + enterpriseFeatures, + }; + const queryEnterpriseLearnerQueryKey = getQueryEnterpriseLearner({ + hasEnterpriseSlug: !!routeEnterpriseSlug, + }).queryKey; + + when(mockEnsureQueryData) + .calledWith( + expect.objectContaining({ + queryKey: queryEnterpriseLearnerQueryKey, + }), + ) + .mockResolvedValue(queryEnterpriseLearnerResult); + + const features = await extractEnterpriseFeatures(args); + expect(features).toEqual(enterpriseFeatures); + }); + + it('should throw an error if enterprise features cannot be found', async () => { + const routeEnterpriseSlug = mockEnterpriseCustomer.slug; + const args = { + queryClient: mockQueryClient, + authenticatedUser: mockAuthenticatedUser, + enterpriseSlug: routeEnterpriseSlug, + }; + + const queryEnterpriseLearnerQueryKey = getQueryEnterpriseLearner({ + hasEnterpriseSlug: !!routeEnterpriseSlug, + }).queryKey; + + when(mockEnsureQueryData) + .calledWith( + expect.objectContaining({ + queryKey: queryEnterpriseLearnerQueryKey, + }), + ) + .mockRejectedValue(new Error('Could not retrieve enterprise features')); + + await expect(extractEnterpriseFeatures(args)).rejects.toThrow( + 'Could not retrieve enterprise features', + ); + }); +}); diff --git a/src/components/app/data/queries/extractEnterpriseFeatures.ts b/src/components/app/data/queries/extractEnterpriseFeatures.ts new file mode 100644 index 000000000..f783b7ebe --- /dev/null +++ b/src/components/app/data/queries/extractEnterpriseFeatures.ts @@ -0,0 +1,27 @@ +import { queryEnterpriseLearner } from './queries'; + +interface ExtractEnterpriseCustomerArgs { + queryClient: Types.QueryClient; + authenticatedUser: Types.AuthenticatedUser; + enterpriseSlug?: string; +} + +/** + * Extracts the enterpriseFeatures from the enterpriseLearnerData for the current user and enterprise slug. + */ +async function extractEnterpriseFeatures({ + queryClient, + authenticatedUser, + enterpriseSlug, +} : ExtractEnterpriseCustomerArgs) : Promise { + // Retrieve linked enterprise customers for the current user from query cache, or + // fetch from the server if not available. + const linkedEnterpriseCustomersQuery = queryEnterpriseLearner(authenticatedUser.username, enterpriseSlug); + const enterpriseLearnerData = await queryClient.ensureQueryData( + linkedEnterpriseCustomersQuery, + ); + const { enterpriseFeatures } = enterpriseLearnerData; + return enterpriseFeatures; +} + +export default extractEnterpriseFeatures; diff --git a/src/components/app/data/queries/index.js b/src/components/app/data/queries/index.js index 196689ee7..e9128f727 100644 --- a/src/components/app/data/queries/index.js +++ b/src/components/app/data/queries/index.js @@ -1,4 +1,5 @@ export { default as extractEnterpriseCustomer } from './extractEnterpriseCustomer'; +export { default as extractEnterpriseFeatures } from './extractEnterpriseFeatures'; export { default as queries } from './queryKeyFactory'; export * from './queries'; diff --git a/src/components/app/routes/loaders/rootLoader.ts b/src/components/app/routes/loaders/rootLoader.ts index 25d430c2a..cf792b6d0 100644 --- a/src/components/app/routes/loaders/rootLoader.ts +++ b/src/components/app/routes/loaders/rootLoader.ts @@ -31,7 +31,7 @@ const makeRootLoader: Types.MakeRouteLoaderFunctionWithQueryClient = function ma activeEnterpriseCustomer, allLinkedEnterpriseCustomerUsers, } = enterpriseLearnerData; - const { staffEnterpriseCustomer } = enterpriseLearnerData; + const { staffEnterpriseCustomer, enterpriseFeatures } = enterpriseLearnerData; // User has no active, linked enterprise customer and no staff-only customer metadata exists; return early. if (!enterpriseCustomer) { @@ -68,7 +68,7 @@ const makeRootLoader: Types.MakeRouteLoaderFunctionWithQueryClient = function ma userEmail, queryClient, requestUrl, - enterpriseFeatures: enterpriseCustomer.enterpriseFeatures, + enterpriseFeatures, }); // Redirect to the same URL without a trailing slash, if applicable. diff --git a/src/components/dashboard/data/dashboardLoader.test.jsx b/src/components/dashboard/data/dashboardLoader.test.jsx index d1d74beca..d4c3aabf2 100644 --- a/src/components/dashboard/data/dashboardLoader.test.jsx +++ b/src/components/dashboard/data/dashboardLoader.test.jsx @@ -249,7 +249,7 @@ describe('dashboardLoader', () => { expect(await screen.findByTestId('dashboard')).toBeInTheDocument(); } - expect(mockQueryClient.ensureQueryData).toHaveBeenCalledTimes(4); + expect(mockQueryClient.ensureQueryData).toHaveBeenCalledTimes(5); expect(mockQueryClient.ensureQueryData).toHaveBeenCalledWith( expect.objectContaining({ queryKey: shouldUseBFFQuery diff --git a/src/components/dashboard/data/dashboardLoader.ts b/src/components/dashboard/data/dashboardLoader.ts index 469bf0e1c..fddd2d7b8 100644 --- a/src/components/dashboard/data/dashboardLoader.ts +++ b/src/components/dashboard/data/dashboardLoader.ts @@ -1,6 +1,7 @@ import { ensureAuthenticatedUser, redirectToSearchPageForNewUser } from '../../app/routes/data'; import { extractEnterpriseCustomer, + extractEnterpriseFeatures, queryEnterpriseCourseEnrollments, queryEnterprisePathwaysList, queryEnterpriseProgramsList, @@ -31,19 +32,31 @@ const makeDashboardLoader: Types.MakeRouteLoaderFunctionWithQueryClient = functi } const { enterpriseSlug } = params; + + // Extract enterprise customer. const enterpriseCustomer = await extractEnterpriseCustomer({ queryClient, authenticatedUser, enterpriseSlug, }); + // Extract enterprise features. + const enterpriseFeatures = await extractEnterpriseFeatures({ + queryClient, + authenticatedUser, + enterpriseSlug, + }); + + // Attempt to resolve the BFF query for the dashboard. const dashboardBFFQuery = resolveBFFQuery( requestUrl.pathname, { enterpriseCustomerUuid: enterpriseCustomer.uuid, - enterpriseFeatures: enterpriseCustomer.enterpriseFeatures, + enterpriseFeatures, }, ); + + // Load enrollments, policies, and conditionally redirect for new users const loadEnrollmentsPoliciesAndRedirectForNewUsers = Promise.all([ queryClient.ensureQueryData( dashboardBFFQuery diff --git a/src/types.d.ts b/src/types.d.ts index caf122ea6..80f96e59f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -42,10 +42,11 @@ export interface EnterpriseCustomer { slug: string; name: string; enableOneAcademy: boolean; - enterpriseFeatures: { - enterpriseLearnerBFFEnabled: boolean; - [key: string]: boolean; - }; +} + +export interface EnterpriseFeatures { + enterpriseLearnerBFFEnabled: boolean; + [key: string]: boolean; } export interface EnterpriseLearnerData { @@ -53,6 +54,7 @@ export interface EnterpriseLearnerData { activeEnterpriseCustomer: Types.EnterpriseCustomer; allLinkedEnterpriseCustomerUsers: any[]; staffEnterpriseCustomer: Types.EnterpriseCustomer; + enterpriseFeatures: Types.EnterpriseFeatures; } interface EnrollmentDueDate {