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: migrate enterprise customer business logic to the BFF layer #1263

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
8 changes: 0 additions & 8 deletions src/components/app/data/hooks/useBFF.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { useLocation, useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { logError } from '@edx/frontend-platform/logging';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import { resolveBFFQuery } from '../queries';
import useEnterpriseFeatures from './useEnterpriseFeatures';

/**
* Uses the route to determine which API call to make for the BFF
Expand All @@ -19,18 +17,12 @@ export default function useBFF({
bffQueryOptions = {},
fallbackQueryConfig = null,
}) {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: enterpriseFeatures } = useEnterpriseFeatures();
const { enterpriseSlug } = useParams();
const location = useLocation();

// Determine the BFF query to use based on the current location
const matchedBFFQuery = resolveBFFQuery(
location.pathname,
{
enterpriseCustomerUuid: enterpriseCustomer.uuid,
enterpriseFeatures,
},
);

// Determine which query to call, the original hook or the new BFF
Expand Down
96 changes: 26 additions & 70 deletions src/components/app/data/hooks/useBFF.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider } from '@tanstack/react-query';
import { v4 as uuidv4 } from 'uuid';
import { useLocation, useParams } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform/config';
import { MemoryRouter, useParams } from 'react-router-dom';
import { enterpriseCustomerFactory } from '../services/data/__factories__';
import { queryClient } from '../../../../utils/tests';
import { fetchEnterpriseLearnerDashboard } from '../services';
import useBFF from './useBFF';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import useEnterpriseFeatures from './useEnterpriseFeatures';

jest.mock('./useEnterpriseCustomer');
jest.mock('./useEnterpriseFeatures');
Expand All @@ -18,15 +16,8 @@ jest.mock('../services', () => ({
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useParams: jest.fn(),
}));
jest.mock('@edx/frontend-platform/config', () => ({
...jest.requireActual('@edx/frontend-platform/config'),
getConfig: jest.fn(() => ({
FEATURE_ENABLE_BFF_API_FOR_ENTERPRISE_CUSTOMERS: [],
})),
}));

const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockCustomerAgreementUuid = uuidv4();
Expand Down Expand Up @@ -129,90 +120,50 @@ const mockBFFDashboardData = {
};

describe('useBFF', () => {
const Wrapper = ({ children }) => (
const Wrapper = ({ routes = null, children }) => (
<QueryClientProvider client={queryClient()}>
{children}
<MemoryRouter initialEntries={[routes]}>
{children}
</MemoryRouter>
</QueryClientProvider>
);

beforeEach(() => {
jest.clearAllMocks();
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
useEnterpriseFeatures.mockReturnValue({ data: { enterpriseLearnerBffEnabled: false } });
fetchEnterpriseLearnerDashboard.mockResolvedValue(mockBFFDashboardData);
useLocation.mockReturnValue({ pathname: '/test-enterprise' });
useParams.mockReturnValue({ enterpriseSlug: 'test-enterprise' });
getConfig.mockReturnValue({
FEATURE_ENABLE_BFF_API_FOR_ENTERPRISE_CUSTOMERS: [mockEnterpriseCustomer.uuid],
});
});

it.each([
// BFF enabled via customer opt-in (without query options)
// BFF disabled route (without query options)
{
isBFFEnabledForCustomer: true,
isBFFEnabledForUser: false,
isMatchedRoute: false,
hasQueryOptions: false,
},
// BFF enabled via customer opt-in (with query options)
{
isBFFEnabledForCustomer: true,
isBFFEnabledForUser: false,
hasQueryOptions: true,
},
// BFF enabled via Waffle flag (without query options)
// BFF enabled route (without query options)
{
isBFFEnabledForCustomer: false,
isBFFEnabledForUser: true,
isMatchedRoute: true,
hasQueryOptions: false,
},
// BFF enabled via Waffle flag (with query options)
// BFF enabled route (with query options)
{
isBFFEnabledForCustomer: false,
isBFFEnabledForUser: true,
isMatchedRoute: true,
hasQueryOptions: true,
},
// BFF enabled via customer opt-in and Waffle flag (without query options)
{
isBFFEnabledForCustomer: true,
isBFFEnabledForUser: true,
hasQueryOptions: false,
},
// BFF enabled via customer opt-in and Waffle flag (with query options)
{
isBFFEnabledForCustomer: true,
isBFFEnabledForUser: true,
hasQueryOptions: true,
},
// BFF disabled (without query options)
{
isBFFEnabledForCustomer: false,
isBFFEnabledForUser: false,
hasQueryOptions: false,
},
// BFF disabled (with query options)

// BFF disabled route(with query options)
{
isBFFEnabledForCustomer: false,
isBFFEnabledForUser: false,
isMatchedRoute: false,
hasQueryOptions: true,
},
])('should handle resolved value correctly for the dashboard route, and the config enabled (%s)', async ({
isBFFEnabledForCustomer,
isBFFEnabledForUser,
])('should handle resolved value correctly for based on route (%s)', async ({
isMatchedRoute,
hasQueryOptions,
}) => {
if (!isBFFEnabledForCustomer) {
getConfig.mockReturnValue({
FEATURE_ENABLE_BFF_API_FOR_ENTERPRISE_CUSTOMERS: [],
});
}
if (isBFFEnabledForUser) {
useEnterpriseFeatures.mockReturnValue({ data: { enterpriseLearnerBffEnabled: true } });
}
const isBFFEnabled = isBFFEnabledForCustomer || isBFFEnabledForUser;
const mockFallbackData = { fallback: 'data' };
const mockSelect = jest.fn(() => {
if (isBFFEnabled) {
if (isMatchedRoute) {
return mockBFFDashboardData;
}
return mockFallbackData;
Expand All @@ -234,11 +185,16 @@ describe('useBFF', () => {
},
fallbackQueryConfig: mockFallbackQueryConfig,
}),
{ wrapper: Wrapper },
{
wrapper: ({ children }) => Wrapper({
routes: isMatchedRoute ? '/test-enterprise' : '/test-enterprise/search',
children,
}),
},
);
await waitForNextUpdate();

const expectedData = isBFFEnabled ? mockBFFDashboardData : mockFallbackData;
const expectedData = isMatchedRoute ? mockBFFDashboardData : mockFallbackData;
expect(result.current).toEqual(
expect.objectContaining({
data: expectedData,
Expand All @@ -249,7 +205,7 @@ describe('useBFF', () => {

if (hasQueryOptions) {
expect(mockSelect).toHaveBeenCalledTimes(1);
if (isBFFEnabled) {
if (isMatchedRoute) {
// Expects the select function to be called with the resolved BFF data
expect(mockSelect).toHaveBeenCalledWith(mockBFFDashboardData);
} else {
Expand All @@ -258,7 +214,7 @@ describe('useBFF', () => {
}
}

if (isBFFEnabled) {
if (isMatchedRoute) {
expect(fetchEnterpriseLearnerDashboard).toHaveBeenCalledTimes(1);
expect(fetchEnterpriseLearnerDashboard).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down
9 changes: 5 additions & 4 deletions src/components/app/data/hooks/useEnterpriseCustomer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ export default function useEnterpriseCustomer(queryOptions = {}) {
const { select, ...queryOptionsRest } = queryOptions;
return useEnterpriseLearner({
...queryOptionsRest,
select: (enterpriseLearner) => {
select: (data) => {
const transformedData = data.enterpriseCustomer || data.transformed.enterpriseCustomer;
if (select) {
return select({
original: enterpriseLearner,
transformed: enterpriseLearner.enterpriseCustomer,
original: data,
transformed: transformedData,
});
}
return enterpriseLearner.enterpriseCustomer;
return transformedData;
},
});
}
108 changes: 83 additions & 25 deletions src/components/app/data/hooks/useEnterpriseCustomer.test.jsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,111 @@
import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider } from '@tanstack/react-query';
import { AppContext } from '@edx/frontend-platform/react';
import { MemoryRouter, useParams } from 'react-router-dom';
import { authenticatedUserFactory, enterpriseCustomerFactory } from '../services/data/__factories__';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import { queryClient } from '../../../../utils/tests';
import { fetchEnterpriseLearnerData } from '../services';
import { fetchEnterpriseLearnerDashboard, fetchEnterpriseLearnerData } from '../services';

jest.mock('../services', () => ({
...jest.requireActual('../services'),
fetchEnterpriseLearnerData: jest.fn().mockResolvedValue(null),
fetchEnterpriseLearnerDashboard: jest.fn().mockResolvedValue(null),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
}));
const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockAuthenticatedUser = authenticatedUserFactory();
const mockEnterpriseLearnerData = {
enterpriseCustomer: mockEnterpriseCustomer,
enterpriseCustomerUserRoleAssignments: [],
activeEnterpriseCustomer: null,
activeEnterpriseCustomerUserRoleAssignments: [],
transformed: {
enterpriseCustomer: mockEnterpriseCustomer,
enterpriseCustomerUserRoleAssignments: [],
activeEnterpriseCustomer: null,
activeEnterpriseCustomerUserRoleAssignments: [],
allLinkedEnterpriseCustomerUsers: [],
staffEnterpriseCustomer: null,
enterpriseFeatures: {
isBFFEnabled: false,
},
shouldUpdateActiveEnterpriseCustomerUser: false,
},
};
const mockBFFDashboardData = {
enterpriseCustomer: {
...mockEnterpriseCustomer,
isBFFEnabled: true,
},
allLinkedEnterpriseCustomerUsers: [],
enterpriseFeatures: {},
staffEnterpriseCustomer: null,
enterpriseFeatures: {
isBFFEnabled: true,
},
shouldUpdateActiveEnterpriseCustomerUser: false,
enterpriseCustomerUserSubsidies: {
subscriptions: {
customerAgreement: {},
subscriptionLicenses: [],
subscriptionLicensesByStatus: {
activated: [],
assigned: [],
expired: [],
revoked: [],
},
},
},
enterpriseCourseEnrollments: [],
errors: [],
warnings: [],
};

const mockExpectedEnterpriseCustomers = (isMatchedRoute) => (isMatchedRoute
? mockBFFDashboardData.enterpriseCustomer
: mockEnterpriseLearnerData.transformed.enterpriseCustomer);

describe('useEnterpriseCustomer', () => {
const Wrapper = ({ children }) => (
const Wrapper = ({ routes = null, children }) => (
<QueryClientProvider client={queryClient()}>
<AppContext.Provider value={{ authenticatedUser: mockAuthenticatedUser }}>
{children}
</AppContext.Provider>
<MemoryRouter initialEntries={[routes]}>
<AppContext.Provider value={{ authenticatedUser: mockAuthenticatedUser }}>
{children}
</AppContext.Provider>
</MemoryRouter>
</QueryClientProvider>
);
beforeEach(() => {
jest.clearAllMocks();
fetchEnterpriseLearnerData.mockResolvedValue(mockEnterpriseLearnerData);
fetchEnterpriseLearnerDashboard.mockResolvedValue(mockBFFDashboardData);
useParams.mockReturnValue({ enterpriseSlug: 'test-slug' });
});
it('should return enterprise customer metadata correctly', async () => {
const { result, waitForNextUpdate } = renderHook(() => useEnterpriseCustomer(), { wrapper: Wrapper });
it.each([
{ isMatchedRoute: false },
{ isMatchedRoute: true },
])('should return enterprise customers correctly (%s)', async ({ isMatchedRoute }) => {
const mockSelect = jest.fn(data => data.transformed);
const { result, waitForNextUpdate } = renderHook(
() => {
if (isMatchedRoute) {
return useEnterpriseCustomer({ select: mockSelect });
}
return useEnterpriseCustomer();
},
{
wrapper: ({ children }) => Wrapper({
routes: isMatchedRoute ? '/test-enterprise' : 'test-enterprise/search',
children,
}),
},
);
await waitForNextUpdate();
const actualEnterpriseCustomer = result.current.data;
expect(actualEnterpriseCustomer.uuid).toEqual(mockEnterpriseCustomer.uuid);
expect(actualEnterpriseCustomer.slug).toEqual(mockEnterpriseCustomer.slug);
});
it('should return enterprise customer metadata correctly with select', async () => {
const { result, waitForNextUpdate } = renderHook(() => useEnterpriseCustomer({
select: (data) => data,
}), { wrapper: Wrapper });
await waitForNextUpdate();
const actualEnterpriseCustomerSelectArgs = result.current.data;
expect(actualEnterpriseCustomerSelectArgs.original).toEqual(mockEnterpriseLearnerData);
expect(actualEnterpriseCustomerSelectArgs.transformed).toEqual(mockEnterpriseCustomer);
if (isMatchedRoute) {
expect(mockSelect).toHaveBeenCalledTimes(2);
} else {
expect(mockSelect).toHaveBeenCalledTimes(0);
}

const actualEnterpriseFeatures = result.current.data;
expect(actualEnterpriseFeatures).toEqual(mockExpectedEnterpriseCustomers(isMatchedRoute));
});
});
5 changes: 3 additions & 2 deletions src/components/app/data/hooks/useEnterpriseFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ export default function useEnterpriseFeatures(queryOptions = {}) {
return useEnterpriseLearner({
...queryOptionsRest,
select: (data) => {
const transformedData = data.enterpriseFeatures || data.transformed.enterpriseFeatures;
if (select) {
return select({
original: data,
transformed: data?.enterpriseFeatures,
transformed: transformedData,
});
}
return data?.enterpriseFeatures;
return transformedData;
},
});
}
Loading