Skip to content

Commit

Permalink
feat: auto setting active linked enterprise
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-sheehan-edx committed Jan 17, 2024
1 parent 4b56103 commit 6e1fe54
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/components/EnterpriseApp/EnterpriseAppContextProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { createContext, useMemo } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';

import { EnterpriseSubsidiesContext, useEnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';
import { SubsidyRequestsContext, useSubsidyRequestsContext } from '../subsidy-requests/SubsidyRequestsContext';
import {
useEnterpriseCurationContext,
useUpdateActiveEnterpriseForUser,
} from './data/hooks';
import EnterpriseAppSkeleton from './EnterpriseAppSkeleton';

Expand Down Expand Up @@ -49,6 +51,7 @@ const EnterpriseAppContextProvider = ({
enablePortalLearnerCreditManagementScreen,
children,
}) => {
const { authenticatedUser } = useContext(AppContext);
// subsidies for the enterprise customer
const enterpriseSubsidiesContext = useEnterpriseSubsidiesContext({
enterpriseId,
Expand All @@ -68,10 +71,16 @@ const EnterpriseAppContextProvider = ({
curationTitleForCreation: enterpriseName,
});

const { isLoading: isUpdatingActiveEnterprise } = useUpdateActiveEnterpriseForUser({
enterpriseId,
user: authenticatedUser,
});

const isLoading = (
subsidyRequestsContext.isLoading
|| enterpriseSubsidiesContext.isLoading
|| enterpriseCurationContext.isLoading
|| isUpdatingActiveEnterprise
);

// [tech debt] consolidate the other context values (e.g., useSubsidyRequestsContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,43 @@ describe('<EnterpriseAppContextProvider />', () => {
isLoadingEnterpriseSubsidies: true,
isLoadingSubsidyRequests: false,
isLoadingEnterpriseCuration: false,
isLoadingUpdateActiveEnterpriseForUser: false,
},
{
isLoadingEnterpriseSubsidies: false,
isLoadingSubsidyRequests: true,
isLoadingEnterpriseCuration: false,
isLoadingUpdateActiveEnterpriseForUser: false,
},
{
isLoadingEnterpriseSubsidies: false,
isLoadingSubsidyRequests: false,
isLoadingEnterpriseCuration: true,
isLoadingUpdateActiveEnterpriseForUser: false,
},
{
isLoadingEnterpriseSubsidies: true,
isLoadingSubsidyRequests: true,
isLoadingEnterpriseCuration: false,
isLoadingUpdateActiveEnterpriseForUser: false,
},
{
isLoadingEnterpriseSubsidies: false,
isLoadingSubsidyRequests: false,
isLoadingEnterpriseCuration: false,
isLoadingUpdateActiveEnterpriseForUser: true,
},
{
isLoadingEnterpriseSubsidies: true,
isLoadingSubsidyRequests: true,
isLoadingEnterpriseCuration: true,
isLoadingUpdateActiveEnterpriseForUser: true,
},
])('renders <EnterpriseAppSkeleton /> when: %s', async ({
isLoadingEnterpriseSubsidies,
isLoadingSubsidyRequests,
isLoadingEnterpriseCuration,
isLoadingUpdateActiveEnterpriseForUser,
}) => {
const mockUseEnterpriseSubsidiesContext = jest.spyOn(enterpriseSubsidiesContext, 'useEnterpriseSubsidiesContext').mockReturnValue({
isLoading: isLoadingEnterpriseSubsidies,
Expand All @@ -62,6 +74,11 @@ describe('<EnterpriseAppContextProvider />', () => {
isLoading: isLoadingEnterpriseCuration,
},
);
const mockUseUpdateActiveEnterpriseForUser = jest.spyOn(hooks, 'useUpdateActiveEnterpriseForUser').mockReturnValue(
{
isLoading: isLoadingUpdateActiveEnterpriseForUser,
},
);

render(
<EnterpriseAppContextProvider
Expand All @@ -75,11 +92,17 @@ describe('<EnterpriseAppContextProvider />', () => {
);

await waitFor(() => {
expect(mockUseUpdateActiveEnterpriseForUser).toHaveBeenCalled();
expect(mockUseSubsidyRequestsContext).toHaveBeenCalled();
expect(mockUseEnterpriseSubsidiesContext).toHaveBeenCalled();
expect(mockUseEnterpriseCurationContext).toHaveBeenCalled();

if (isLoadingEnterpriseSubsidies || isLoadingSubsidyRequests || isLoadingEnterpriseCuration) {
if (
isLoadingEnterpriseSubsidies
|| isLoadingSubsidyRequests
|| isLoadingEnterpriseCuration
|| isLoadingUpdateActiveEnterpriseForUser
) {
expect(screen.getByText('Loading...'));
} else {
expect(screen.getByText('children'));
Expand Down
1 change: 1 addition & 0 deletions src/components/EnterpriseApp/data/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as useEnterpriseCuration } from './useEnterpriseCuration';
export { default as useEnterpriseCurationContext } from './useEnterpriseCurationContext';
export { default as useUpdateActiveEnterpriseForUser } from './useUpdateActiveEnterpriseForUser';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { logError } from '@edx/frontend-platform/logging';
import {
useQuery,
} from '@tanstack/react-query';

import LmsApiService from '../../../../data/services/LmsApiService';

const useUpdateActiveEnterpriseForUser = ({ enterpriseId, user }) => {
const { username } = user;
const { isLoading, error } = useQuery({
queryKey: ['updateUsersActiveEnterprise'],
queryFn: async () => {
await LmsApiService.getActiveLinkedEnterprise(username).then(async (linkedEnterprise) => {
if (linkedEnterprise.uuid !== enterpriseId) {
await LmsApiService.updateUserActiveEnterprise(enterpriseId);
}
});
return true;
},
});

if (error) { logError(`Could not set active enterprise for learner, failed with error: ${logError}`); }

return {
isLoading,
};
};

export default useUpdateActiveEnterpriseForUser;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider } from '@tanstack/react-query';
import { logError } from '@edx/frontend-platform/logging';
import { useUpdateActiveEnterpriseForUser } from './index';
import LmsApiService from '../../../../data/services/LmsApiService';
import { queryClient } from '../../../test/testUtils';

jest.mock('../../../../data/services/LmsApiService');
jest.mock('@edx/frontend-platform/logging', () => ({
...jest.requireActual('@edx/frontend-platform/logging'),
logError: jest.fn(),
}));

describe('useUpdateActiveEnterpriseForUser', () => {
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient()}>
{children}
</QueryClientProvider>
);
const mockEnterpriseId = 'enterprise-uuid';
const mockUser = { username: 'joe_shmoe' };
const connectedEnterprise = 'someID';
beforeEach(() => {
LmsApiService.getActiveLinkedEnterprise.mockResolvedValue({ uuid: connectedEnterprise });
});

afterEach(() => jest.clearAllMocks());

it("should update user's active enterprise if it differs from the current enterprise", async () => {
const { result, waitForNextUpdate } = renderHook(
() => useUpdateActiveEnterpriseForUser({
enterpriseId: mockEnterpriseId,
user: mockUser,
}),
{ wrapper },
);
expect(result.current.isLoading).toBe(true);

await waitForNextUpdate();

expect(LmsApiService.updateUserActiveEnterprise).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
});

it('should do nothing if active enterprise is the same as current enterprise', async () => {
// Pass the value of the enterprise ID returned by ``getActiveLinkedEnterprise`` to the hook
const { waitForNextUpdate } = renderHook(
() => useUpdateActiveEnterpriseForUser({
enterpriseId: connectedEnterprise,
user: mockUser,
}),
{ wrapper },
);
await waitForNextUpdate();
expect(LmsApiService.updateUserActiveEnterprise).toHaveBeenCalledTimes(0);
});

it('should handle errors', async () => {
LmsApiService.updateUserActiveEnterprise.mockRejectedValueOnce(Error('uh oh'));
const { result, waitForNextUpdate } = renderHook(
() => useUpdateActiveEnterpriseForUser({
enterpriseId: mockEnterpriseId,
user: mockUser,
}),
{ wrapper },
);
expect(result.current.isLoading).toBe(true);

await waitForNextUpdate();

expect(LmsApiService.updateUserActiveEnterprise).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(logError).toHaveBeenCalledTimes(1);
});
});
33 changes: 33 additions & 0 deletions src/data/services/LmsApiService.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { logError } from '@edx/frontend-platform/logging';

import { configuration } from '../../config';
import generateFormattedStatusUrl from './apiServiceUtils';
Expand Down Expand Up @@ -384,6 +385,38 @@ class LmsApiService {
const url = `${LmsApiService.baseUrl}/enterprise/api/v1/analytics-summary/${enterpriseUUID}`;
return LmsApiService.apiClient().post(url, formData);
}

static updateUserActiveEnterprise = (enterpriseId) => {
const url = `${configuration.LMS_BASE_URL}/enterprise/select/active/`;
const formData = new FormData();
formData.append('enterprise', enterpriseId);

return LmsApiService.apiClient().post(
url,
formData,
);
};

static fetchEnterpriseLearnerData(options) {
const enterpriseLearnerUrl = `${configuration.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/`;
const queryParams = new URLSearchParams({
...options,
page: 1,
});
const url = `${enterpriseLearnerUrl}?${queryParams.toString()}`;
return LmsApiService.apiClient().get(url);
}

static async getActiveLinkedEnterprise(username) {
const response = await this.fetchEnterpriseLearnerData({ username });
const transformedResponse = camelCaseObject(response.data);
const enterprisesForLearner = transformedResponse.results;
const activeLinkedEnterprise = enterprisesForLearner.find(enterprise => enterprise.active);
if (!activeLinkedEnterprise) {
logError(`${username} does not have any active linked enterprise customers`);

Check warning on line 416 in src/data/services/LmsApiService.js

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L416

Added line #L416 was not covered by tests
}
return activeLinkedEnterprise.enterpriseCustomer;
}
}

export default LmsApiService;
32 changes: 32 additions & 0 deletions src/data/services/tests/LmsApiService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import { configuration } from '../../../config';

const lmsBaseUrl = `${configuration.LMS_BASE_URL}`;
const mockEnterpriseUUID = 'test-enterprise-id';
const mockUsername = 'test_username';

const axiosMock = new MockAdapter(axios);
getAuthenticatedHttpClient.mockReturnValue(axios);

axiosMock.onAny().reply(200);
axios.patch = jest.fn();
axios.post = jest.fn();
axios.get = jest.fn();

describe('LmsApiService', () => {
test('updateEnterpriseCustomer calls the LMS to update the enterprise customer', () => {
Expand Down Expand Up @@ -41,4 +44,33 @@ describe('LmsApiService', () => {
{ primary_color: '#A8DABC' },
);
});
test('updateUserActiveEnterprise calls the LMS to update the active linked enterprise org', () => {
LmsApiService.updateUserActiveEnterprise(
mockEnterpriseUUID,
);
const expectedFormData = new FormData();
expectedFormData.append('enterprise', mockEnterpriseUUID);
expect(axios.post).toBeCalledWith(
`${lmsBaseUrl}/enterprise/select/active/`,
expectedFormData,
);
});
test('fetchEnterpriseLearnerData calls the LMS to fetch learner data', () => {
LmsApiService.fetchEnterpriseLearnerData({ username: mockUsername });
expect(axios.get).toBeCalledWith(
`${lmsBaseUrl}/enterprise/api/v1/enterprise-learner/?username=${mockUsername}&page=1`,
);
});
test('getActiveLinkedEnterprise returns the actively linked enterprise', async () => {
axios.get.mockReturnValue({
data: {
results: [{
active: true,
enterpriseCustomer: { uuid: 'test-uuid' },
}],
},
});
const activeCustomer = await LmsApiService.getActiveLinkedEnterprise(mockUsername);
expect(activeCustomer).toEqual({ uuid: 'test-uuid' });
});
});

0 comments on commit 6e1fe54

Please sign in to comment.