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: set is_bff_enabled custom attribute via logging service; create abstraction to make requests to BFF API endpoints with logError/logInfo #1234

Merged
merged 7 commits into from
Dec 10, 2024
38 changes: 0 additions & 38 deletions src/components/app/data/services/bffs.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';

import { v4 as uuidv4 } from 'uuid';
import { camelCaseObject } from '@edx/frontend-platform';
Expand All @@ -13,23 +14,29 @@ getAuthenticatedHttpClient.mockReturnValue(axios);
const APP_CONFIG = {
ENTERPRISE_ACCESS_BASE_URL: 'http://localhost:18270',
};

jest.mock('@edx/frontend-platform/config', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(() => APP_CONFIG),
}));

jest.mock('@edx/frontend-platform/auth', () => ({
...jest.requireActual('@edx/frontend-platform/auth'),
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('@edx/frontend-platform/logging', () => ({
...jest.requireActual('@edx/frontend-platform/logging'),
logError: jest.fn(),
logInfo: jest.fn(),
}));

const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockEnterpriseCustomer = enterpriseCustomerFactory() as Types.EnterpriseCustomer;
const mockCustomerAgreementUuid = uuidv4();
const mockSubscriptionCatalogUuid = uuidv4();
const mockSubscriptionLicenseUuid = uuidv4();
const mockSubscriptionPlanUuid = uuidv4();
const mockActivationKey = uuidv4();
const mockBFFDashboardResponse = {

const mockBaseLearnerBFFResponse = {
enterprise_customer_user_subsidies: {
subscriptions: {
customer_agreement: {
Expand Down Expand Up @@ -96,6 +103,12 @@ const mockBFFDashboardResponse = {
},
},
},
errors: [],
warnings: [],
};

const mockBFFDashboardResponse = {
...mockBaseLearnerBFFResponse,
enterprise_course_enrollments: [
{
course_run_id: 'course-v1:edX+DemoX+3T2022',
Expand All @@ -119,25 +132,78 @@ const mockBFFDashboardResponse = {
is_revoked: false,
},
],
errors: [],
warnings: [],
};

describe('fetchEnterpriseLearnerDashboard', () => {
const enterpriseDashboard = `${APP_CONFIG.ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;
const urlForDashboardBFF = `${APP_CONFIG.ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;

beforeEach(() => {
jest.clearAllMocks();
axiosMock.reset();
});

it('returns learner dashboard metadata', async () => {
axiosMock.onPost(enterpriseDashboard).reply(200, mockBFFDashboardResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId: mockEnterpriseCustomer.uuid });
it.each([
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: undefined,
},
{
enterpriseId: undefined,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
])('returns learner dashboard metadata (%s)', async ({
enterpriseId,
enterpriseSlug,
}) => {
axiosMock.onPost(urlForDashboardBFF).reply(200, mockBFFDashboardResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId, enterpriseSlug });
expect(result).toEqual(camelCaseObject(mockBFFDashboardResponse));
});

it('catches error and returns null', async () => {
axiosMock.onPost(enterpriseDashboard).reply(404, learnerDashboardBFFResponse);
const result = await fetchEnterpriseLearnerDashboard(null);
it.each([
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: undefined,
},
{
enterpriseId: undefined,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
])('catches error and returns default dashboard BFF response (%s)', async ({
enterpriseId,
enterpriseSlug,
}) => {
axiosMock.onPost(urlForDashboardBFF).reply(404, learnerDashboardBFFResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId, enterpriseSlug });
expect(result).toEqual(learnerDashboardBFFResponse);
});

it('logs errors and warnings from BFF response', async () => {
const mockError = {
developer_message: 'This is a developer message',
};
const mockWarning = {
developer_message: 'This is a developer warning',
};
const mockResponseWithErrorsAndWarnings = {
...mockBFFDashboardResponse,
errors: [mockError],
warnings: [mockWarning],
};
axiosMock.onPost(urlForDashboardBFF).reply(200, mockResponseWithErrorsAndWarnings);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseSlug: mockEnterpriseCustomer.slug });
expect(result).toEqual(camelCaseObject(mockResponseWithErrorsAndWarnings));

// Assert the logError and logInfo functions were called with the expected arguments.
expect(logError).toHaveBeenCalledWith(`BFF Error (${urlForDashboardBFF}): ${mockError.developer_message}`);
expect(logInfo).toHaveBeenCalledWith(`BFF Warning (${urlForDashboardBFF}): ${mockWarning.developer_message}`);
});
});
111 changes: 111 additions & 0 deletions src/components/app/data/services/bffs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { getConfig } from '@edx/frontend-platform/config';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils';

export const baseLearnerBFFResponse = {
enterpriseCustomerUserSubsidies: {
subscriptions: {
customerAgreement: {},
subscriptionLicenses: [],
subscriptionLicensesByStatus: {},
},
},
errors: [],
warnings: [],
};

export const learnerDashboardBFFResponse = {
...baseLearnerBFFResponse,
enterpriseCourseEnrollments: [],
};

/**
* Log any errors and warnings from the BFF response.
* @param {Object} args
* @param {String} args.url - The URL of the BFF API endpoint.
* @param {Object} args.response - The camelCased response from the BFF API endpoint.
*/
export function logErrorsAndWarningsFromBFFResponse({ url, response }) {
response.errors.forEach((error) => {
logError(`BFF Error (${url}): ${error.developerMessage}`);
});
response.warnings.forEach((warning) => {
logInfo(`BFF Warning (${url}): ${warning.developerMessage}`);
});
}

/**
* Make a request to the specified BFF API endpoint.
* @param {Object} args
* @param {String} args.url - The URL of the BFF API endpoint.
* @param {Object} args.defaultResponse - The default response to return if unable to resolve the request.
* @param {Object} args.options - The options to pass to the BFF API endpoint.
* @param {String} [args.options.enterpriseId] - The UUID of the enterprise customer.
* @param {String} [args.options.enterpriseSlug] - The slug of the enterprise customer.
* @returns {Promise<Object>} - The response from the BFF.
*/
export async function makeBFFRequest({
url,
defaultResponse,
options = {} as Types.BFFRequestOptions,
}) {
const { enterpriseId, enterpriseSlug, ...optionsRest } = options;
const snakeCaseOptionsRest = optionsRest ? snakeCaseObject(optionsRest) : {};

// If neither enterpriseId or enterpriseSlug is provided, return the default response.
if (!enterpriseId && !enterpriseSlug) {
return defaultResponse;
}

try {
const params = {
enterprise_customer_uuid: enterpriseId,
enterprise_customer_slug: enterpriseSlug,
...snakeCaseOptionsRest,
};

// Make request to BFF.
const result = await getAuthenticatedHttpClient().post(url, params);
const response = camelCaseObject(result.data);

// Log any errors and warnings from the BFF response.
logErrorsAndWarningsFromBFFResponse({ url, response });

// Return the response from the BFF.
return response;
} catch (error) {
logError(error);
return defaultResponse;
}
}

export interface EnterpriseLearnerDashboardOptions {
enterpriseId?: string;
enterpriseSlug?: string;
}

/**
* Fetch the learner dashboard BFF API for the specified enterprise customer.
* @param {Object} args
* @param {String} [args.enterpriseId] - The UUID of the enterprise customer.
* @param {String} [args.enterpriseSlug] - The slug of the enterprise customer.
* @returns {Promise<Object>} - The learner dashboard metadata.
*/
export async function fetchEnterpriseLearnerDashboard({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[inform] Moved from bffs.js -> bffs.ts

enterpriseId,
enterpriseSlug,
}: EnterpriseLearnerDashboardOptions) {
const options = {} as Types.BFFRequestOptions;
if (enterpriseId) {
options.enterpriseId = enterpriseId;
}
if (enterpriseSlug) {
options.enterpriseSlug = enterpriseSlug;
}
return makeBFFRequest({
url: `${getConfig().ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`,
defaultResponse: learnerDashboardBFFResponse,
options,
});
}
24 changes: 18 additions & 6 deletions src/components/enterprise-page/EnterprisePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,35 @@ import { getLoggingService } from '@edx/frontend-platform/logging';
import { isDefinedAndNotNull } from '../../utils/common';
import { useAlgoliaSearch } from '../../utils/hooks';
import { pushUserCustomerAttributes } from '../../utils/optimizely';
import { useEnterpriseCustomer } from '../app/data';
import { isBFFEnabledForEnterpriseCustomer, useEnterpriseCustomer } from '../app/data';

const EnterprisePage = ({ children }) => {
/**
* Custom hook to set custom attributes for logging service:
* - enterprise_customer_uuid - The UUID of the enterprise customer
* - is_bff_enabled - Whether the BFF is enabled for the enterprise customer
*/
function useLoggingCustomAttributes() {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const config = getConfig();
const [searchClient, searchIndex] = useAlgoliaSearch(config);
const { authenticatedUser } = useContext(AppContext);

useEffect(() => {
if (isDefinedAndNotNull(enterpriseCustomer)) {
pushUserCustomerAttributes(enterpriseCustomer);

// Set custom attributes for logging service
const loggingService = getLoggingService();
loggingService.setCustomAttribute('enterprise_customer_uuid', enterpriseCustomer.uuid);
const isBFFEnabled = isBFFEnabledForEnterpriseCustomer(enterpriseCustomer.uuid);
loggingService.setCustomAttribute('is_bff_enabled', isBFFEnabled);
}
}, [enterpriseCustomer]);
}

const EnterprisePage = ({ children }) => {
const config = getConfig();
const [searchClient, searchIndex] = useAlgoliaSearch(config);
const { authenticatedUser } = useContext(AppContext);

// Set custom attributes via logging service
useLoggingCustomAttributes();

const contextValue = useMemo(() => ({
authenticatedUser,
Expand Down
29 changes: 28 additions & 1 deletion src/components/enterprise-page/EnterprisePage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ describe('<EnterprisePage />', () => {
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
});

const defaultAppContextValue = { authenticatedUser: mockAuthenticatedUser };
const defaultAppContextValue = {
authenticatedUser: mockAuthenticatedUser,
config: {
FEATURE_ENABLE_BFF_API_FOR_ENTERPRISE_CUSTOMERS: [],
},
};

const EnterprisePageWrapper = ({ children, appContextValue = defaultAppContextValue }) => (
<AppContext.Provider value={appContextValue}>
Expand Down Expand Up @@ -70,4 +75,26 @@ describe('<EnterprisePage />', () => {
}),
);
});

it.each([
{ isBFFEnabled: false },
{ isBFFEnabled: true },
])('sets custom attributes via logging service', ({ isBFFEnabled }) => {
// Mock the BFF-related feature flag
const bffFeatureFlag = isBFFEnabled ? [mockEnterpriseCustomer.uuid] : [];
const appContextValueWithBFFConfig = {
authenticatedUser: mockAuthenticatedUser,
config: {
FEATURE_ENABLE_BFF_API_FOR_ENTERPRISE_CUSTOMERS: bffFeatureFlag,
},
};

// Mount the component
mount(<EnterprisePageWrapper appContextValue={appContextValueWithBFFConfig} />);

// Verify that the custom attributes were set
expect(mockSetCustomAttribute).toHaveBeenCalledTimes(2);
expect(mockSetCustomAttribute).toHaveBeenCalledWith('enterprise_customer_uuid', mockEnterpriseCustomer.uuid);
expect(mockSetCustomAttribute).toHaveBeenCalledWith('is_bff_enabled', isBFFEnabled);
});
});
Loading
Loading