diff --git a/src/libs/ajax/ExternalCredentials.ts b/src/libs/ajax/ExternalCredentials.ts index 5738170bb8..a5846826ca 100644 --- a/src/libs/ajax/ExternalCredentials.ts +++ b/src/libs/ajax/ExternalCredentials.ts @@ -75,3 +75,5 @@ export const ExternalCredentials = (signal?: AbortSignal) => (oAuth2Provider: OA }, }; }; + +export type ExternalCredentialsContract = ReturnType>; diff --git a/src/libs/ajax/User.ts b/src/libs/ajax/User.ts index 901b4cea1b..2ba44b19a4 100644 --- a/src/libs/ajax/User.ts +++ b/src/libs/ajax/User.ts @@ -346,3 +346,4 @@ export const User = (signal?: AbortSignal) => { }; export type UserContract = ReturnType; +export type UserProfileContract = UserContract['profile']; diff --git a/src/profile/external-identities/LinkOAuth2Account.tsx b/src/profile/external-identities/LinkOAuth2Account.tsx index fa4200139c..b26f3b83e4 100644 --- a/src/profile/external-identities/LinkOAuth2Account.tsx +++ b/src/profile/external-identities/LinkOAuth2Account.tsx @@ -2,7 +2,7 @@ import { ClickableProps } from '@terra-ui-packages/components'; import React, { ReactNode } from 'react'; import { ButtonPrimary, Link } from 'src/components/common'; import { icon } from 'src/components/icons'; -import { Ajax } from 'src/libs/ajax'; +import { ExternalCredentials } from 'src/libs/ajax/ExternalCredentials'; import { withErrorReporting } from 'src/libs/error'; import { useCancellation } from 'src/libs/react-utils'; import * as Utils from 'src/libs/utils'; @@ -22,7 +22,7 @@ export const LinkOAuth2Account = (props: LinkOAuth2AccountProps): ReactNode => { const getAuthUrlAndRedirect = withErrorReporting(`Error getting Authorization URL for ${provider.short}`)( async () => { - const url = await Ajax(signal).ExternalCredentials(provider).getAuthorizationUrl(); + const url = await ExternalCredentials(signal)(provider).getAuthorizationUrl(); window.open(url, Utils.newTabLinkProps.target, 'noopener,noreferrer'); } ); diff --git a/src/profile/external-identities/NihAccount.test.ts b/src/profile/external-identities/NihAccount.test.ts index 4dbceecc85..5c9654b46a 100644 --- a/src/profile/external-identities/NihAccount.test.ts +++ b/src/profile/external-identities/NihAccount.test.ts @@ -1,10 +1,11 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { h } from 'react-hyperscript-helpers'; -import { Ajax } from 'src/libs/ajax'; +import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics'; +import { User, UserContract } from 'src/libs/ajax/User'; import { authStore } from 'src/libs/state'; import { NihAccount } from 'src/profile/external-identities/NihAccount'; -import { asMockedFn, renderWithAppContexts } from 'src/testing/test-utils'; +import { asMockedFn, MockedFn, partial, renderWithAppContexts } from 'src/testing/test-utils'; jest.mock('src/libs/nav', () => ({ ...jest.requireActual('src/libs/nav'), @@ -25,7 +26,8 @@ jest.mock('react-notifications-component', () => { }; }); -jest.mock('src/libs/ajax'); +jest.mock('src/libs/ajax/Metrics'); +jest.mock('src/libs/ajax/User'); const nihStatus = { linkedNihUsername: 'TEST_USERNAME', @@ -42,14 +44,10 @@ describe('NihAccount', () => { describe('when given a token', () => { it('links the NIH Account and renders a linked NIH Account', async () => { // Arrange - const linkNihAccountFunction = jest.fn().mockReturnValue(Promise.resolve(nihStatus)); - asMockedFn(Ajax).mockImplementation( - () => - ({ - Metrics: { captureEvent: jest.fn() } as Partial['Metrics']>, - User: { linkNihAccount: linkNihAccountFunction } as Partial['User']>, - } as ReturnType) - ); + const linkNihAccountFunction: MockedFn = jest.fn(); + linkNihAccountFunction.mockResolvedValue(nihStatus); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: jest.fn() })); + asMockedFn(User).mockReturnValue(partial({ linkNihAccount: linkNihAccountFunction })); // Act await act(async () => { @@ -115,14 +113,11 @@ describe('NihAccount', () => { // Arrange const user = userEvent.setup(); - const unlinkNihAccountFunction = jest.fn().mockReturnValue(Promise.resolve()); - asMockedFn(Ajax).mockImplementation( - () => - ({ - Metrics: { captureEvent: jest.fn() } as Partial['Metrics']>, - User: { unlinkNihAccount: unlinkNihAccountFunction } as Partial['User']>, - } as ReturnType) - ); + const unlinkNihAccountFunction: MockedFn = jest.fn(); + unlinkNihAccountFunction.mockResolvedValue(undefined); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: jest.fn() })); + asMockedFn(User).mockReturnValue(partial({ unlinkNihAccount: unlinkNihAccountFunction })); + // Act await act(async () => { authStore.update((state) => ({ ...state, nihStatus, nihStatusLoaded: true })); diff --git a/src/profile/external-identities/NihAccount.ts b/src/profile/external-identities/NihAccount.ts index 348ad40c24..bf080fe8b8 100644 --- a/src/profile/external-identities/NihAccount.ts +++ b/src/profile/external-identities/NihAccount.ts @@ -5,7 +5,7 @@ import { div, h, h3, span } from 'react-hyperscript-helpers'; import Collapse from 'src/components/Collapse'; import { ButtonPrimary, Link, spinnerOverlay } from 'src/components/common'; import { InfoBox } from 'src/components/InfoBox'; -import { Ajax } from 'src/libs/ajax'; +import { User } from 'src/libs/ajax/User'; import { NihDatasetPermission } from 'src/libs/ajax/User'; import { withErrorReporting } from 'src/libs/error'; import * as Nav from 'src/libs/nav'; @@ -30,7 +30,7 @@ export const NihAccount = ({ nihToken }) => { withErrorReporting('Error linking NIH account'), Utils.withBusyState(setIsLinking) )(async () => { - const nihStatus = await Ajax().User.linkNihAccount(nihToken); + const nihStatus = await User().linkNihAccount(nihToken); authStore.update((oldState: AuthState) => ({ ...oldState, nihStatus, @@ -157,7 +157,7 @@ export const NihAccount = ({ nihToken }) => { Utils.withBusyState(setIsUnlinking) )(async () => { authStore.update(_.set('nihStatusLoaded', false)); - await Ajax().User.unlinkNihAccount(); + await User().unlinkNihAccount(); authStore.update((oldState: AuthState) => ({ ...oldState, nihStatus: undefined, diff --git a/src/profile/external-identities/OAuth2Account.test.tsx b/src/profile/external-identities/OAuth2Account.test.tsx index 75bf47dd98..9db2660170 100644 --- a/src/profile/external-identities/OAuth2Account.test.tsx +++ b/src/profile/external-identities/OAuth2Account.test.tsx @@ -1,19 +1,20 @@ -import { DeepPartial } from '@terra-ui-packages/core-utils'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { axe } from 'jest-axe'; import React from 'react'; -import { Ajax } from 'src/libs/ajax'; +import { ExternalCredentials, ExternalCredentialsContract } from 'src/libs/ajax/ExternalCredentials'; +import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics'; import Events from 'src/libs/events'; import { getCurrentRoute } from 'src/libs/nav'; import { authStore } from 'src/libs/state'; import * as Utils from 'src/libs/utils'; import { OAuth2Provider } from 'src/profile/external-identities/OAuth2Providers'; -import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils'; +import { asMockedFn, MockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils'; import { OAuth2Account } from './OAuth2Account'; -jest.mock('src/libs/ajax'); +jest.mock('src/libs/ajax/ExternalCredentials'); +jest.mock('src/libs/ajax/Metrics'); jest.mock('src/auth/auth', () => ({ ...jest.requireActual('src/auth/auth'), @@ -45,9 +46,6 @@ jest.mock( }) ); -type AjaxExports = typeof import('src/libs/ajax'); -type AjaxContract = ReturnType; - const testAccessTokenProvider: OAuth2Provider = { key: 'github', name: 'Test Provider', @@ -64,19 +62,17 @@ describe('OAuth2Account', () => { describe('When no account is linked', () => { it('shows the button to link an account', async () => { // Arrange - const getLinkStatusFn = jest.fn().mockResolvedValue(undefined); - const getAuthorizationUrlFn = jest.fn().mockResolvedValue('https://foo.bar.com/oauth2/authorize'); - asMockedFn(Ajax).mockImplementation( - () => - ({ - ExternalCredentials: () => { - return { - getAccountLinkStatus: getLinkStatusFn, - getAuthorizationUrl: getAuthorizationUrlFn, - }; - }, - } as DeepPartial as AjaxContract) + const getLinkStatusFn: MockedFn = jest.fn(); + getLinkStatusFn.mockResolvedValue(undefined); + const getAuthorizationUrlFn: MockedFn = jest.fn(); + getAuthorizationUrlFn.mockResolvedValue('https://foo.bar.com/oauth2/authorize'); + asMockedFn(ExternalCredentials).mockReturnValue(() => + partial({ + getAccountLinkStatus: getLinkStatusFn, + getAuthorizationUrl: getAuthorizationUrlFn, + }) ); + // Act const { container } = await act(() => render() @@ -93,19 +89,19 @@ describe('OAuth2Account', () => { // Arrange const user = userEvent.setup(); jest.spyOn(window, 'open').mockImplementation(() => null); - const getLinkStatusFn = jest.fn().mockResolvedValue(undefined); - const getAuthorizationUrlFn = jest.fn().mockResolvedValue('https://foo.bar.com/oauth2/authorize'); - asMockedFn(Ajax).mockImplementation( - () => - ({ - ExternalCredentials: () => { - return { - getAccountLinkStatus: getLinkStatusFn, - getAuthorizationUrl: getAuthorizationUrlFn, - }; - }, - } as DeepPartial as AjaxContract) + + const getLinkStatusFn: MockedFn = jest.fn(); + getLinkStatusFn.mockResolvedValue(undefined); + const getAuthorizationUrlFn: MockedFn = jest.fn(); + getAuthorizationUrlFn.mockResolvedValue('https://foo.bar.com/oauth2/authorize'); + + asMockedFn(ExternalCredentials).mockReturnValue(() => + partial({ + getAccountLinkStatus: getLinkStatusFn, + getAuthorizationUrl: getAuthorizationUrlFn, + }) ); + // Act await act(() => render()); @@ -122,28 +118,28 @@ describe('OAuth2Account', () => { oauthcode: 'abcde12345', state: btoa(JSON.stringify({ provider: testAccessTokenProvider.key, nonce: 'abcxyz' })), }; - const getLinkStatusFn = jest.fn().mockResolvedValue(undefined); - const getAuthorizationUrlFn = jest.fn().mockResolvedValue('https://foo.bar.com/oauth2/authorize'); - const linkAccountFn = jest - .fn() - .mockResolvedValue({ externalUserId: 'testUser', expirationTimestamp: new Date(), authenticated: true }); - const captureEventFn = jest.fn(); - - asMockedFn(Ajax).mockImplementation( - () => - ({ - ExternalCredentials: () => { - return { - getAccountLinkStatus: getLinkStatusFn, - getAuthorizationUrl: getAuthorizationUrlFn, - linkAccountWithAuthorizationCode: linkAccountFn, - }; - }, - Metrics: { - captureEvent: captureEventFn, - }, - } as DeepPartial as AjaxContract) + + const getLinkStatusFn: MockedFn = jest.fn(); + getLinkStatusFn.mockResolvedValue(undefined); + const getAuthorizationUrlFn: MockedFn = jest.fn(); + getAuthorizationUrlFn.mockResolvedValue('https://foo.bar.com/oauth2/authorize'); + const linkAccountFn: MockedFn = jest.fn(); + linkAccountFn.mockResolvedValue({ + externalUserId: 'testUser', + expirationTimestamp: new Date(), + authenticated: true, + }); + const captureEventFn: MockedFn = jest.fn(); + + asMockedFn(ExternalCredentials).mockReturnValue(() => + partial({ + getAccountLinkStatus: getLinkStatusFn, + getAuthorizationUrl: getAuthorizationUrlFn, + linkAccountWithAuthorizationCode: linkAccountFn, + }) ); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: captureEventFn })); + // Act await act(() => render()); @@ -170,26 +166,24 @@ describe('OAuth2Account', () => { screen.getByText('Link Expiration:'); screen.getByText(Utils.makeCompleteDate(linkStatus.expirationTimestamp)); }); + it("unlinks the account when 'Unlink' is clicked", async () => { // Arrange const user = userEvent.setup(); const linkStatus = { externalUserId: 'testUser', expirationTimestamp: new Date(), authenticated: true }; authStore.update((state) => ({ ...state, oAuth2AccountStatus: { [testAccessTokenProvider.key]: linkStatus } })); - const unlinkAccountFn = jest.fn().mockResolvedValue(undefined); - const captureEventFn = jest.fn(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - ExternalCredentials: () => { - return { - unlinkAccount: unlinkAccountFn, - }; - }, - Metrics: { - captureEvent: captureEventFn, - }, - } as DeepPartial as AjaxContract) + + const unlinkAccountFn: MockedFn = jest.fn(); + unlinkAccountFn.mockResolvedValue(undefined); + const captureEventFn: MockedFn = jest.fn(); + + asMockedFn(ExternalCredentials).mockReturnValue(() => + partial({ + unlinkAccount: unlinkAccountFn, + }) ); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: captureEventFn })); + // Act const { container } = await act(() => render() diff --git a/src/profile/external-identities/OAuth2Account.tsx b/src/profile/external-identities/OAuth2Account.tsx index e828535ed0..e5abb9cb94 100644 --- a/src/profile/external-identities/OAuth2Account.tsx +++ b/src/profile/external-identities/OAuth2Account.tsx @@ -1,7 +1,8 @@ import _ from 'lodash/fp'; import React, { useState } from 'react'; import { ClipboardButton } from 'src/components/ClipboardButton'; -import { Ajax } from 'src/libs/ajax'; +import { ExternalCredentials } from 'src/libs/ajax/ExternalCredentials'; +import { Metrics } from 'src/libs/ajax/Metrics'; import colors from 'src/libs/colors'; import { withErrorReporting } from 'src/libs/error'; import Events from 'src/libs/events'; @@ -76,11 +77,9 @@ export const OAuth2Account = (props: OAuth2AccountProps) => { useOnMount(() => { const linkAccount = withErrorReporting(`Error linking ${provider.short} account`)(async (code, state) => { - const accountInfo = await Ajax(signal) - .ExternalCredentials(provider) - .linkAccountWithAuthorizationCode(code, state); + const accountInfo = await ExternalCredentials(signal)(provider).linkAccountWithAuthorizationCode(code, state); authStore.update(_.set(['oAuth2AccountStatus', provider.key], accountInfo)); - Ajax().Metrics.captureEvent(Events.user.externalCredential.link, { provider: provider.key }); + void Metrics().captureEvent(Events.user.externalCredential.link, { provider: provider.key }); setIsLinking(false); }); @@ -117,7 +116,7 @@ export const OAuth2Account = (props: OAuth2AccountProps) => { {provider.supportsIdToken && ( - + Copy identity token to clipboard )} diff --git a/src/profile/external-identities/UnlinkOAuth2Account.tsx b/src/profile/external-identities/UnlinkOAuth2Account.tsx index d6df474e0b..771a07b0f0 100644 --- a/src/profile/external-identities/UnlinkOAuth2Account.tsx +++ b/src/profile/external-identities/UnlinkOAuth2Account.tsx @@ -2,7 +2,8 @@ import { Clickable, ClickableProps, Modal } from '@terra-ui-packages/components' import _ from 'lodash/fp'; import React, { ReactNode, useState } from 'react'; import { ButtonPrimary, spinnerOverlay } from 'src/components/common'; -import { Ajax } from 'src/libs/ajax'; +import { ExternalCredentials } from 'src/libs/ajax/ExternalCredentials'; +import { Metrics } from 'src/libs/ajax/Metrics'; import colors from 'src/libs/colors'; import { withErrorReporting } from 'src/libs/error'; import Events from 'src/libs/events'; @@ -34,9 +35,9 @@ export const UnlinkOAuth2Account = ({ linkText, provider }: UnlinkOAuth2AccountP withErrorReporting('Error unlinking account'), Utils.withBusyState(setIsUnlinking) )(async () => { - await Ajax().ExternalCredentials(provider).unlinkAccount(); + await ExternalCredentials()(provider).unlinkAccount(); authStore.update(_.unset(['oAuth2AccountStatus', provider.key])); - Ajax().Metrics.captureEvent(Events.user.externalCredential.unlink, { provider: provider.key }); + void Metrics().captureEvent(Events.user.externalCredential.unlink, { provider: provider.key }); setIsModalOpen(false); notify('success', 'Successfully unlinked account', { message: `Successfully unlinked your account from ${provider.name}`, diff --git a/src/profile/notification-settings/utils.test.ts b/src/profile/notification-settings/utils.test.ts index a79cf07f5d..4dfe375a7d 100644 --- a/src/profile/notification-settings/utils.test.ts +++ b/src/profile/notification-settings/utils.test.ts @@ -1,15 +1,16 @@ -import { asMockedFn } from '@terra-ui-packages/test-utils'; +import { asMockedFn, MockedFn, partial } from '@terra-ui-packages/test-utils'; import { refreshSamUserAttributes, refreshTerraProfile } from 'src/auth/user-profile/user'; -import { Ajax } from 'src/libs/ajax'; +import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics'; +import { SamUserAttributes, User, UserContract, UserProfileContract } from 'src/libs/ajax/User'; import Events, { EventWorkspaceAttributes, extractWorkspaceDetails } from 'src/libs/events'; import { notificationEnabled, updateNotificationPreferences, updateUserAttributes } from './utils'; -type AjaxContract = ReturnType; - jest.mock('src/auth/auth'); jest.mock('src/auth/user-profile/user'); -jest.mock('src/libs/ajax'); + +jest.mock('src/libs/ajax/Metrics'); +jest.mock('src/libs/ajax/User'); describe('utils', () => { describe('notificationEnabled', () => { @@ -35,25 +36,19 @@ describe('utils', () => { }); describe('updateNotificationPreferences', () => { - const setPreferences = jest.fn().mockReturnValue(Promise.resolve()); - const captureEvent = jest.fn(); + const setPreferences: MockedFn = jest.fn(); + setPreferences.mockResolvedValue(undefined); + const captureEvent: MockedFn = jest.fn(); const keys = ['key1', 'key2']; const workspace = { namespace: 'ns', name: 'name' } as EventWorkspaceAttributes; beforeEach(() => { jest.resetAllMocks(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - Metrics: { - captureEvent, - } as Partial, - User: { - profile: { - setPreferences, - } as Partial, - } as Partial, - } as Partial as AjaxContract) + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent })); + asMockedFn(User).mockReturnValue( + partial({ + profile: partial({ setPreferences }), + }) ); }); @@ -99,23 +94,15 @@ describe('utils', () => { }); describe('updateUserAttributes', () => { - const setUserAttributes = jest.fn().mockReturnValue(Promise.resolve()); - const captureEvent = jest.fn(); + const setUserAttributes: MockedFn = jest.fn(); + setUserAttributes.mockResolvedValue(partial({})); + const captureEvent: MockedFn = jest.fn(); const keys = ['key1', 'key2']; beforeEach(() => { jest.resetAllMocks(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - Metrics: { - captureEvent, - } as Partial, - User: { - setUserAttributes, - } as Partial, - } as Partial as AjaxContract) - ); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent })); + asMockedFn(User).mockReturnValue(partial({ setUserAttributes })); }); it('updates user attributes, refreshes the sam profile, and sends an event when the preference is disabled', async () => { diff --git a/src/profile/notification-settings/utils.ts b/src/profile/notification-settings/utils.ts index b34198d94d..c2ecdfe087 100644 --- a/src/profile/notification-settings/utils.ts +++ b/src/profile/notification-settings/utils.ts @@ -1,5 +1,6 @@ import { refreshSamUserAttributes, refreshTerraProfile } from 'src/auth/user-profile/user'; -import { Ajax } from 'src/libs/ajax'; +import { Metrics } from 'src/libs/ajax/Metrics'; +import { User } from 'src/libs/ajax/User'; import Events, { EventWorkspaceAttributes, extractWorkspaceDetails } from 'src/libs/events'; export const workspaceSubmissionNotificationKeys = (namespace: string, name: string) => [ @@ -42,13 +43,13 @@ export const updateNotificationPreferences = async ( notificationType: NotificationType | undefined, workspace: EventWorkspaceAttributes | undefined ) => { - await Ajax().User.profile.setPreferences(notificationKeysWithValue(notificationKeys, value)); + await User().profile.setPreferences(notificationKeysWithValue(notificationKeys, value)); await refreshTerraProfile(); let eventDetails = { notificationKeys, enabled: value, notificationType }; if (workspace) { eventDetails = { ...eventDetails, ...extractWorkspaceDetails(workspace) }; } - Ajax().Metrics.captureEvent(Events.notificationToggle, eventDetails); + void Metrics().captureEvent(Events.notificationToggle, eventDetails); }; export const updateUserAttributes = async ( @@ -56,7 +57,7 @@ export const updateUserAttributes = async ( value: boolean, notificationType: NotificationType | undefined ) => { - await Ajax().User.setUserAttributes({ marketingConsent: value }); - Ajax().Metrics.captureEvent(Events.notificationToggle, { notificationKeys, enabled: value, notificationType }); + await User().setUserAttributes({ marketingConsent: value }); + void Metrics().captureEvent(Events.notificationToggle, { notificationKeys, enabled: value, notificationType }); await refreshSamUserAttributes(); }; diff --git a/src/registration/Register.test.ts b/src/registration/Register.test.ts index 150f6891b0..a15543b1b0 100644 --- a/src/registration/Register.test.ts +++ b/src/registration/Register.test.ts @@ -1,15 +1,19 @@ -import { DeepPartial } from '@terra-ui-packages/core-utils'; import { act, fireEvent, screen } from '@testing-library/react'; import { axe } from 'jest-axe'; import { h } from 'react-hyperscript-helpers'; import { signOut } from 'src/auth/signout/sign-out'; import { loadTerraUser } from 'src/auth/user-profile/user'; -import { Ajax } from 'src/libs/ajax'; -import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils'; +import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics'; +import { TermsOfService, TermsOfServiceContract } from 'src/libs/ajax/TermsOfService'; +import { SamUserResponse, User, UserContract, UserProfileContract } from 'src/libs/ajax/User'; +import { TerraUserProfile } from 'src/libs/state'; +import { asMockedFn, MockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils'; import { Register } from './Register'; -jest.mock('src/libs/ajax'); +jest.mock('src/libs/ajax/Metrics'); +jest.mock('src/libs/ajax/TermsOfService'); +jest.mock('src/libs/ajax/User'); jest.mock('src/auth/signout/sign-out', () => ({ ...jest.requireActual('src/auth/signout/sign-out'), @@ -31,9 +35,6 @@ jest.mock('react-notifications-component', () => { }; }); -type AjaxExports = typeof import('src/libs/ajax'); -type AjaxContract = ReturnType; - const fillInPersonalInfo = (): void => { fireEvent.change(screen.getByLabelText(/First Name/), { target: { value: 'Test Name' } }); fireEvent.change(screen.getByLabelText(/Last Name/), { target: { value: 'Test Last Name' } }); @@ -47,14 +48,9 @@ const fillInOrgInfo = (): void => { fireEvent.change(screen.getByLabelText(/Title/), { target: { value: 'Test Title' } }); }; const acceptTermsOfService = (): void => { - asMockedFn(Ajax).mockImplementation( - () => - ({ - TermsOfService: { - getTermsOfServiceText: jest.fn().mockResolvedValue('Terra Terms of Service'), - }, - } as DeepPartial as AjaxContract) - ); + const getTermsOfServiceText: MockedFn = jest.fn(); + getTermsOfServiceText.mockResolvedValue('Terra Terms of Service'); + asMockedFn(TermsOfService).mockReturnValue(partial({ getTermsOfServiceText })); fireEvent.click(screen.getByText('Read Terra Platform Terms of Service here')); @@ -135,26 +131,20 @@ describe('Register', () => { }); it('enables the terms of service checkbox if the user has read the terms of service', async () => { // Arrange - const tosTextFn = jest.fn().mockResolvedValue('Terra Terms of Service'); + const getTermsOfServiceText: MockedFn = jest.fn(); + getTermsOfServiceText.mockResolvedValue('Terra Terms of Service'); // Act render(h(Register)); fillInPersonalInfo(); fillInOrgInfo(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - TermsOfService: { - getTermsOfServiceText: tosTextFn, - }, - } as DeepPartial as AjaxContract) - ); + asMockedFn(TermsOfService).mockReturnValue(partial({ getTermsOfServiceText })); fireEvent.click(screen.getByText('Read Terra Platform Terms of Service here')); fireEvent.click(screen.getByText('OK')); // Assert - expect(tosTextFn).toHaveBeenCalled(); + expect(getTermsOfServiceText).toHaveBeenCalled(); const termsOfServiceCheckbox = screen.getByLabelText( 'By checking this box, you are agreeing to the Terra Terms of Service' ); @@ -168,13 +158,10 @@ describe('Register', () => { render(h(Register)); fillInPersonalInfo(); fillInOrgInfo(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - TermsOfService: { - getTermsOfServiceText: jest.fn().mockResolvedValue('Terra Terms of Service'), - }, - } as DeepPartial as AjaxContract) + asMockedFn(TermsOfService).mockReturnValue( + partial({ + getTermsOfServiceText: jest.fn().mockResolvedValue('Terra Terms of Service'), + }) ); fireEvent.click(screen.getByText('Read Terra Platform Terms of Service here')); @@ -195,9 +182,14 @@ describe('Register', () => { describe('Registration', () => { it('fires off a request to Orch and Sam to register a user', async () => { // Arrange - const registerUserFunction = jest.fn().mockResolvedValue({}); - const setUserAttributesFunction = jest.fn().mockResolvedValue({ marketingConsent: false }); - const getUserAttributesFunction = jest.fn().mockResolvedValue({ marketingConsent: false }); + const registerWithProfile: MockedFn = jest.fn(); + registerWithProfile.mockResolvedValue(partial({})); + const setUserAttributes: MockedFn = jest.fn(); + setUserAttributes.mockResolvedValue({ marketingConsent: false }); + const getUserAttributes: MockedFn = jest.fn(); + getUserAttributes.mockResolvedValue({ marketingConsent: false }); + const userProfileGet: MockedFn = jest.fn(); + userProfileGet.mockResolvedValue(partial({})); // Act render(h(Register)); @@ -207,19 +199,14 @@ describe('Register', () => { fireEvent.click(screen.getByLabelText(/Marketing communications.*/)); acceptTermsOfService(); - asMockedFn(Ajax).mockImplementation( - () => - ({ - Metrics: { captureEvent: jest.fn() }, - User: { - setUserAttributes: setUserAttributesFunction, - getUserAttributes: getUserAttributesFunction, - registerWithProfile: registerUserFunction, - profile: { - get: jest.fn().mockReturnValue({}), - }, - }, - } as DeepPartial as AjaxContract) + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: jest.fn() })); + asMockedFn(User).mockReturnValue( + partial({ + setUserAttributes, + getUserAttributes, + registerWithProfile, + profile: partial({ get: userProfileGet }), + }) ); const loadTerraUserFn = jest.fn().mockResolvedValue(undefined); @@ -230,7 +217,7 @@ describe('Register', () => { await act(() => fireEvent.click(registerButton)); // Assert - expect(registerUserFunction).toHaveBeenCalledWith(true, { + expect(registerWithProfile).toHaveBeenCalledWith(true, { firstName: 'Test Name', lastName: 'Test Last Name', contactEmail: 'ltcommanderdata@neighborhood.horse', @@ -240,7 +227,7 @@ describe('Register', () => { interestInTerra: '', }); - expect(setUserAttributesFunction).toHaveBeenCalledWith({ marketingConsent: false }); + expect(setUserAttributes).toHaveBeenCalledWith({ marketingConsent: false }); expect(loadTerraUserFn).toHaveBeenCalled(); }); it('logs the user out if they cancel registration', async () => { diff --git a/src/registration/Register.tsx b/src/registration/Register.tsx index 1f01e4a35d..e3a912a335 100644 --- a/src/registration/Register.tsx +++ b/src/registration/Register.tsx @@ -5,7 +5,9 @@ import { loadTerraUser } from 'src/auth/user-profile/user'; import { ButtonPrimary, ButtonSecondary, LabeledCheckbox } from 'src/components/common'; import { centeredSpinner } from 'src/components/icons'; import planet from 'src/images/register-planet.svg'; -import { Ajax } from 'src/libs/ajax'; +import { Metrics } from 'src/libs/ajax/Metrics'; +import { TermsOfService } from 'src/libs/ajax/TermsOfService'; +import { User } from 'src/libs/ajax/User'; import colors from 'src/libs/colors'; import { reportError } from 'src/libs/error'; import Events from 'src/libs/events'; @@ -68,20 +70,20 @@ export const Register = (): ReactNode => { title, } : {}; - await Ajax().User.registerWithProfile(termsOfServiceAccepted, { + await User().registerWithProfile(termsOfServiceAccepted, { firstName: givenName, lastName: familyName, contactEmail: email, interestInTerra, ...orgFields, }); - await Ajax().User.setUserAttributes({ marketingConsent }); + await User().setUserAttributes({ marketingConsent }); await loadTerraUser(); const rootElement = document.getElementById('root'); if (rootElement) { rootElement!.scrollTop = 0; } - Ajax().Metrics.captureEvent(Events.user.register); + await Metrics().captureEvent(Events.user.register); } catch (error) { reportError('Error registering', error); setBusy(false); @@ -218,7 +220,7 @@ export const Register = (): ReactNode => { Ajax().TermsOfService.getTermsOfServiceText()} + getRemoteText={() => TermsOfService().getTermsOfServiceText()} failureMessage='Could not get Terms of Service' /> diff --git a/src/registration/privacy-policy/PrivacyPolicy.tsx b/src/registration/privacy-policy/PrivacyPolicy.tsx index c105a229d8..bb983b2980 100644 --- a/src/registration/privacy-policy/PrivacyPolicy.tsx +++ b/src/registration/privacy-policy/PrivacyPolicy.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ButtonPrimary } from 'src/components/common'; import scienceBackground from 'src/images/science-background.jpg'; -import { Ajax } from 'src/libs/ajax'; +import { TermsOfService } from 'src/libs/ajax/TermsOfService'; import * as Nav from 'src/libs/nav'; import { RemoteMarkdown } from 'src/libs/util/RemoteMarkdown'; import { docContainerStyles, headerStyles, mainStyles } from 'src/registration/legal-doc-styles'; @@ -14,7 +14,7 @@ const PrivacyPolicy = () => {

Terra Privacy Policy

Ajax().TermsOfService.getPrivacyPolicyText()} + getRemoteText={() => TermsOfService().getPrivacyPolicyText()} failureMessage='Could not get Privacy Policy' />
diff --git a/src/registration/terms-of-service/TermsOfServicePage.test.ts b/src/registration/terms-of-service/TermsOfServicePage.test.ts index f792ceed1c..c79ea1792c 100644 --- a/src/registration/terms-of-service/TermsOfServicePage.test.ts +++ b/src/registration/terms-of-service/TermsOfServicePage.test.ts @@ -1,26 +1,25 @@ -import { DeepPartial } from '@terra-ui-packages/core-utils'; import { act, fireEvent, screen } from '@testing-library/react'; import { h } from 'react-hyperscript-helpers'; -import { Ajax } from 'src/libs/ajax'; import { Groups, GroupsContract } from 'src/libs/ajax/Groups'; import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics'; import { SamUserTermsOfServiceDetails, TermsOfService, TermsOfServiceContract } from 'src/libs/ajax/TermsOfService'; import { + OrchestrationUserPreferLegacyFireCloudResponse, SamUserAllowances, SamUserCombinedStateResponse, SamUserResponse, User, UserContract, } from 'src/libs/ajax/User'; -import { AuthState, authStore } from 'src/libs/state'; +import { AuthState, authStore, TerraUserProfile } from 'src/libs/state'; import { TermsOfServicePage } from 'src/registration/terms-of-service/TermsOfServicePage'; -import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils'; +import { asMockedFn, MockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils'; jest.mock('src/libs/ajax/Metrics'); jest.mock('src/libs/ajax/TermsOfService'); jest.mock('src/libs/ajax/User'); jest.mock('src/libs/ajax/Groups'); -jest.mock('src/libs/ajax'); + jest.mock('react-notifications-component', () => { return { Store: { @@ -49,10 +48,10 @@ jest.mock( ); interface TermsOfServiceSetupResult { - getTosFn: jest.Mock; - getTermsOfServiceComplianceStatusFn: jest.Mock; - acceptTosFn: jest.Mock; - rejectTosFn: jest.Mock; + getTosFn: MockedFn; + getTermsOfServiceComplianceStatusFn: MockedFn; + acceptTosFn: MockedFn; + rejectTosFn: MockedFn; } const setupMockAjax = async ( @@ -81,50 +80,41 @@ const setupMockAjax = async ( termsOfService, enterpriseFeatures: [], }; - const getTermsOfServiceText = jest.fn().mockResolvedValue('some text'); - const getUserTermsOfServiceDetails = jest - .fn() - .mockResolvedValue(termsOfService satisfies SamUserTermsOfServiceDetails); - const acceptTermsOfService = jest.fn().mockResolvedValue(undefined); - const rejectTermsOfService = jest.fn().mockResolvedValue(undefined); - const getNihStatus = jest.fn(); - - type AjaxExports = typeof import('src/libs/ajax'); - type AjaxContract = ReturnType; - - asMockedFn(Ajax).mockImplementation( - () => - ({ - TermsOfService: { - acceptTermsOfService, - rejectTermsOfService, - getTermsOfServiceText, - }, - } as DeepPartial as AjaxContract) + const getTermsOfServiceText: MockedFn = jest.fn(); + getTermsOfServiceText.mockResolvedValue('some text'); + const getUserTermsOfServiceDetails: MockedFn = jest.fn(); + getUserTermsOfServiceDetails.mockResolvedValue(termsOfService satisfies SamUserTermsOfServiceDetails); + const acceptTermsOfService: MockedFn = jest.fn(); + acceptTermsOfService.mockResolvedValue(undefined); + const rejectTermsOfService: MockedFn = jest.fn(); + rejectTermsOfService.mockResolvedValue(undefined); + const getNihStatus: MockedFn = jest.fn(); + + asMockedFn(TermsOfService).mockReturnValue( + partial({ + acceptTermsOfService, + rejectTermsOfService, + getTermsOfServiceText, + getUserTermsOfServiceDetails, + }) ); - asMockedFn(Metrics).mockReturnValue({ - captureEvent: jest.fn(), - } as Partial as MetricsContract); - - asMockedFn(User).mockReturnValue({ - getSamUserCombinedState: jest.fn().mockResolvedValue(mockSamUserCombinedState), - profile: { - get: jest.fn().mockResolvedValue({ keyValuePairs: [] }), - update: jest.fn().mockResolvedValue({ keyValuePairs: [] }), - setPreferences: jest.fn().mockResolvedValue({}), - preferLegacyFirecloud: jest.fn().mockResolvedValue({}), - }, - getNihStatus, - } as Partial as UserContract); - - asMockedFn(Groups).mockReturnValue({ - list: jest.fn(), - } as Partial as GroupsContract); + asMockedFn(Metrics).mockReturnValue(partial({ captureEvent: jest.fn() })); + + asMockedFn(User).mockReturnValue( + partial({ + getSamUserCombinedState: jest.fn().mockResolvedValue(mockSamUserCombinedState), + profile: { + get: jest.fn(async () => partial({})), + update: jest.fn(async () => undefined), + setPreferences: jest.fn(async () => undefined), + preferLegacyFirecloud: jest.fn(async () => partial({})), + }, + getNihStatus, + }) + ); - asMockedFn(TermsOfService).mockReturnValue({ - getUserTermsOfServiceDetails, - } as Partial as TermsOfServiceContract); + asMockedFn(Groups).mockReturnValue(partial({ list: jest.fn() })); await act(async () => { authStore.update((state: AuthState) => ({ ...state, termsOfService, signInStatus, terraUserAllowances })); diff --git a/src/registration/terms-of-service/TermsOfServicePage.tsx b/src/registration/terms-of-service/TermsOfServicePage.tsx index 8a628fa947..a41a52c2ec 100644 --- a/src/registration/terms-of-service/TermsOfServicePage.tsx +++ b/src/registration/terms-of-service/TermsOfServicePage.tsx @@ -3,7 +3,7 @@ import { signOut } from 'src/auth/signout/sign-out'; import { loadTerraUser } from 'src/auth/user-profile/user'; import { ButtonPrimary, ButtonSecondary, spinnerOverlay } from 'src/components/common'; import scienceBackground from 'src/images/science-background.jpg'; -import { Ajax } from 'src/libs/ajax'; +import { TermsOfService } from 'src/libs/ajax/TermsOfService'; import { reportError } from 'src/libs/error'; import * as Nav from 'src/libs/nav'; import { useStore } from 'src/libs/react-utils'; @@ -23,7 +23,7 @@ export const TermsOfServicePage = () => { const accept = async () => { try { setBusy(true); - await Ajax().TermsOfService.acceptTermsOfService(); + await TermsOfService().acceptTermsOfService(); await loadTerraUser(); // Clear out the alerts so that the user doesn't see the alert again after accepting the TOS. tosAlertsStore.reset(); @@ -38,7 +38,7 @@ export const TermsOfServicePage = () => { const reject = async () => { try { setBusy(true); - await Ajax().TermsOfService.rejectTermsOfService(); + await TermsOfService().rejectTermsOfService(); } catch (error) { reportError('Error rejecting Terms of Service', error); } finally { @@ -84,7 +84,7 @@ export const TermsOfServicePage = () => { )} Ajax().TermsOfService.getTermsOfServiceText()} + getRemoteText={() => TermsOfService().getTermsOfServiceText()} failureMessage='Could not get Terms of Service' /> {buttons}