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 10, 2024
1 parent c67c5e4 commit 70423df
Show file tree
Hide file tree
Showing 6 changed files with 205 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,40 @@
import { useState, useEffect, useCallback } from 'react';
import { logError } from '@edx/frontend-platform/logging';

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

const useUpdateActiveEnterpriseForUser = ({
enterpriseId,
user,
}) => {
const [isLoading, setIsLoading] = useState(false);

const updateActiveEnterpriseAndRefreshJWT = useCallback(async (username, currentEnterpriseId) => {
setIsLoading(true);
try {
await LmsApiService.getActiveLinkedEnterprise(username).then(async (linkedEnterpriseId) => {
if (linkedEnterpriseId !== currentEnterpriseId) {
await LmsApiService.updateUserActiveEnterprise(currentEnterpriseId);
await LmsApiService.loginRefresh();
}
});
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
}, []);

useEffect(() => {
if (!(enterpriseId && user)) {
return;
}
const { username } = user;
updateActiveEnterpriseAndRefreshJWT(username, enterpriseId);
}, [enterpriseId, user, updateActiveEnterpriseAndRefreshJWT]);
return {
isLoading,
};
};

export default useUpdateActiveEnterpriseForUser;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { renderHook } from '@testing-library/react-hooks';
import { useUpdateActiveEnterpriseForUser } from './index';
import LmsApiService from '../../../../data/services/LmsApiService';

jest.mock('../../../../data/services/LmsApiService');

describe('useUpdateActiveEnterpriseForUser', () => {
const mockEnterpriseId = 'enterprise-uuid';
const mockCurrentActiveEnterpriseId = 'current-active-enterprise-uuid';
const mockUser = {
roles: [
'enterprise_admin:random-enterprise-uuid',
`enterprise_learner:${mockCurrentActiveEnterpriseId}`,
`enterprise_learner:${mockEnterpriseId}`,
],
};
beforeEach(() => {
LmsApiService.getActiveLinkedEnterprise.mockResolvedValue('someID');
});

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,
}));
expect(result.current.isLoading).toBe(true);

await waitForNextUpdate();

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

it.each([
{
enterpriseId: undefined,
user: mockUser,
},
{
enterpriseId: mockEnterpriseId,
user: undefined,
},
{
enterpriseId: mockEnterpriseId,
user: {
roles: [`enterprise_learner:${mockEnterpriseId}`],
},
},
])('should do nothing if missing enterpriseId or user, or active enterprise is the same as current enterprise', async (
{
enterpriseId,
user,
},
) => {
renderHook(() => useUpdateActiveEnterpriseForUser({
enterpriseId,
user,
}));

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,
}));
expect(result.current.isLoading).toBe(true);

await waitForNextUpdate();

expect(LmsApiService.updateUserActiveEnterprise).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
});
});
52 changes: 52 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 Cookies from 'universal-cookie';

import { configuration } from '../../config';
import generateFormattedStatusUrl from './apiServiceUtils';
Expand Down Expand Up @@ -384,6 +385,57 @@ 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);

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L390-L392

Added lines #L390 - L392 were not covered by tests

return LmsApiService.apiClient().post(

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L394

Added line #L394 was not covered by tests
url,
formData,
);
};

static loginRefresh = async () => {
const loginRefreshUrl = `${configuration.LMS_BASE_URL}/login_refresh/`;

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L401

Added line #L401 was not covered by tests

try {
return LmsApiService.apiClient().post(loginRefreshUrl);

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L403-L404

Added lines #L403 - L404 were not covered by tests
} catch (error) {
const isUserUnauthenticated = error.response?.status === 401;

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L406

Added line #L406 was not covered by tests
if (isUserUnauthenticated) {
// Clean up the cookie if it exists to eliminate any situation
// where the cookie is not expired but the jwt is expired.
const cookies = new Cookies();
cookies.remove(configuration.ACCESS_TOKEN_COOKIE_NAME);

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L410-L411

Added lines #L410 - L411 were not covered by tests
}
return Promise.resolve();

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L413

Added line #L413 was not covered by tests
}
};

static fetchEnterpriseLearnerData(options) {
const enterpriseLearnerUrl = `${configuration.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/`;
const queryParams = new URLSearchParams({

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L417-L419

Added lines #L417 - L419 were not covered by tests
...options,
page: 1,
});
const url = `${enterpriseLearnerUrl}?${queryParams.toString()}`;
return LmsApiService.apiClient().get(url);

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L423-L424

Added lines #L423 - L424 were not covered by tests
}

static getActiveLinkedEnterprise(username) {
return this.fetchEnterpriseLearnerData({ username }).then((response) => {
const { data } = response;
const results = data?.results;
for (let i = 0; i < results.length; i++) {

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L427-L431

Added lines #L427 - L431 were not covered by tests
if (results[i].active) {
return results[i].uuid;

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L433

Added line #L433 was not covered by tests
}
}
return '';

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

View check run for this annotation

Codecov / codecov/patch

src/data/services/LmsApiService.js#L436

Added line #L436 was not covered by tests
});
}
}

export default LmsApiService;

0 comments on commit 70423df

Please sign in to comment.