From 2fda86b1c958ddc803dd30b790fe85a1eb93bd1e Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 1 Mar 2024 13:42:24 -0800 Subject: [PATCH 01/33] 10007: Create user gateway method for refreshing id token --- .../auth/renewIdTokenInteractor.test.ts | 10 +++--- .../useCases/auth/renewIdTokenInteractor.ts | 4 +-- .../user}/renewIdToken.test.ts | 34 +++++++++---------- .../cognito => gateways/user}/renewIdToken.ts | 17 ++++------ web-api/src/getUserGateway.ts | 2 ++ .../dynamo/users/updateUserRecords.test.ts | 6 ---- 6 files changed, 32 insertions(+), 41 deletions(-) rename web-api/src/{persistence/cognito => gateways/user}/renewIdToken.test.ts (90%) rename web-api/src/{persistence/cognito => gateways/user}/renewIdToken.ts (59%) diff --git a/web-api/src/business/useCases/auth/renewIdTokenInteractor.test.ts b/web-api/src/business/useCases/auth/renewIdTokenInteractor.test.ts index cf0a2f705b6..600e5704383 100644 --- a/web-api/src/business/useCases/auth/renewIdTokenInteractor.test.ts +++ b/web-api/src/business/useCases/auth/renewIdTokenInteractor.test.ts @@ -10,7 +10,7 @@ describe('renewIdTokenInteractor', () => { message: 'Refresh token is expired', }); applicationContext - .getPersistenceGateway() + .getUserGateway() .renewIdToken.mockRejectedValue(mockError); await expect( @@ -23,7 +23,7 @@ describe('renewIdTokenInteractor', () => { it('should rethrow when an error occurs that is not recognized', async () => { const mockError = new Error('Cognito exploded!!!'); applicationContext - .getPersistenceGateway() + .getUserGateway() .renewIdToken.mockRejectedValue(mockError); await expect( @@ -35,16 +35,14 @@ describe('renewIdTokenInteractor', () => { it('should make a call to get an id token and refresh token', async () => { const expectedRefresh = 'sometoken'; - applicationContext.getPersistenceGateway().renewIdToken.mockResolvedValue({ - idToken: 'abc', - }); + applicationContext.getUserGateway().renewIdToken.mockResolvedValue('abc'); const result = await renewIdTokenInteractor(applicationContext, { refreshToken: expectedRefresh, }); expect( - applicationContext.getPersistenceGateway().renewIdToken.mock.calls[0][1], + applicationContext.getUserGateway().renewIdToken.mock.calls[0][1], ).toEqual({ refreshToken: expectedRefresh }); expect(result).toEqual({ idToken: 'abc', diff --git a/web-api/src/business/useCases/auth/renewIdTokenInteractor.ts b/web-api/src/business/useCases/auth/renewIdTokenInteractor.ts index ece131637c8..5729bbbc653 100644 --- a/web-api/src/business/useCases/auth/renewIdTokenInteractor.ts +++ b/web-api/src/business/useCases/auth/renewIdTokenInteractor.ts @@ -8,8 +8,8 @@ export const renewIdTokenInteractor = async ( idToken: string; }> => { try { - const { idToken } = await applicationContext - .getPersistenceGateway() + const idToken = await applicationContext + .getUserGateway() .renewIdToken(applicationContext, { refreshToken }); return { diff --git a/web-api/src/persistence/cognito/renewIdToken.test.ts b/web-api/src/gateways/user/renewIdToken.test.ts similarity index 90% rename from web-api/src/persistence/cognito/renewIdToken.test.ts rename to web-api/src/gateways/user/renewIdToken.test.ts index 4fdf12038d7..490e42cf248 100644 --- a/web-api/src/persistence/cognito/renewIdToken.test.ts +++ b/web-api/src/gateways/user/renewIdToken.test.ts @@ -1,39 +1,39 @@ import { InitiateAuthCommandOutput } from '@aws-sdk/client-cognito-identity-provider'; import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; -import { renewIdToken } from '@web-api/persistence/cognito/renewIdToken'; +import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; describe('renewIdToken', () => { - it('should use the provided refresh token to request and return a new id token', async () => { - const mockIdToken = 'some_id_token'; + it('should throw an error when initiateAuth does not return an IdToken', async () => { const mockOutput: InitiateAuthCommandOutput = { $metadata: {}, - AuthenticationResult: { - IdToken: mockIdToken, - }, }; applicationContext .getCognito() .initiateAuth.mockResolvedValueOnce(mockOutput); - const result = await renewIdToken(applicationContext, { - refreshToken: 'some_token', - }); - - expect(result.idToken).toEqual(mockIdToken); + await expect( + renewIdToken(applicationContext, { + refreshToken: 'some_token', + }), + ).rejects.toThrow('Id token not present on initiateAuth response'); }); - it('should throw an error when initiateAuth does not return an IdToken', async () => { + it('should use the provided refresh token to request and return a new id token', async () => { + const mockIdToken = 'some_id_token'; const mockOutput: InitiateAuthCommandOutput = { $metadata: {}, + AuthenticationResult: { + IdToken: mockIdToken, + }, }; applicationContext .getCognito() .initiateAuth.mockResolvedValueOnce(mockOutput); - await expect( - renewIdToken(applicationContext, { - refreshToken: 'some_token', - }), - ).rejects.toThrow('Id token not present on initiateAuth response'); + const result = await renewIdToken(applicationContext, { + refreshToken: 'some_token', + }); + + expect(result).toEqual(mockIdToken); }); }); diff --git a/web-api/src/persistence/cognito/renewIdToken.ts b/web-api/src/gateways/user/renewIdToken.ts similarity index 59% rename from web-api/src/persistence/cognito/renewIdToken.ts rename to web-api/src/gateways/user/renewIdToken.ts index 108a877734b..5f0074937c2 100644 --- a/web-api/src/persistence/cognito/renewIdToken.ts +++ b/web-api/src/gateways/user/renewIdToken.ts @@ -1,24 +1,21 @@ +import { AuthFlowType } from '@aws-sdk/client-cognito-identity-provider'; import { ServerApplicationContext } from '@web-api/applicationContext'; -export const renewIdToken = async ( +export async function renewIdToken( applicationContext: ServerApplicationContext, { refreshToken }: { refreshToken: string }, -): Promise<{ idToken: string }> => { - const clientId = applicationContext.environment.cognitoClientId; - +): Promise { const result = await applicationContext.getCognito().initiateAuth({ - AuthFlow: 'REFRESH_TOKEN_AUTH', + AuthFlow: AuthFlowType.REFRESH_TOKEN_AUTH, AuthParameters: { REFRESH_TOKEN: refreshToken, }, - ClientId: clientId, + ClientId: applicationContext.environment.cognitoClientId, }); if (!result.AuthenticationResult?.IdToken) { throw new Error('Id token not present on initiateAuth response'); } - return { - idToken: result.AuthenticationResult.IdToken, - }; -}; + return result.AuthenticationResult.IdToken; +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index 20432dc1403..665f71ba81b 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,5 +1,7 @@ import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; +import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; export const getUserGateway = () => ({ getUserByEmail, + renewIdToken, }); diff --git a/web-api/src/persistence/dynamo/users/updateUserRecords.test.ts b/web-api/src/persistence/dynamo/users/updateUserRecords.test.ts index d8c27ba7468..b9ad952264a 100644 --- a/web-api/src/persistence/dynamo/users/updateUserRecords.test.ts +++ b/web-api/src/persistence/dynamo/users/updateUserRecords.test.ts @@ -12,12 +12,6 @@ describe('updateUserRecords', () => { section: 'inactivePractitioner', }; - beforeEach(() => { - applicationContext.getCognito().adminUpdateUserAttributes.mockReturnValue({ - promise: () => null, - }); - }); - it('should successfully update a private practitioner user to inactivePractitioner', async () => { const oldUser = { barNumber: 'PT1234', From 7ab38800036212733aca7d6e7a24da773e11edfb Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 1 Mar 2024 13:57:42 -0800 Subject: [PATCH 02/33] 10007: Remove import --- web-api/src/getPersistenceGateway.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/web-api/src/getPersistenceGateway.ts b/web-api/src/getPersistenceGateway.ts index 836bc6263ca..5be6c81d993 100644 --- a/web-api/src/getPersistenceGateway.ts +++ b/web-api/src/getPersistenceGateway.ts @@ -147,7 +147,6 @@ import { removeIrsPractitionerOnCase, removePrivatePractitionerOnCase, } from './persistence/dynamo/cases/removePractitionerOnCase'; -import { renewIdToken } from './persistence/cognito/renewIdToken'; import { saveDispatchNotification } from './persistence/dynamo/notifications/saveDispatchNotification'; import { saveDocumentFromLambda } from './persistence/s3/saveDocumentFromLambda'; import { saveUserConnection } from './persistence/dynamo/notifications/saveUserConnection'; @@ -388,7 +387,6 @@ const gatewayMethods = { removeIrsPractitionerOnCase, removeLock, removePrivatePractitionerOnCase, - renewIdToken, setChangeOfAddressCaseAsDone, setStoredApplicationHealth, verifyCaseForUser, From bf24e30c15cb93e7c449bd26b4aa57df657be5f1 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 1 Mar 2024 14:09:16 -0800 Subject: [PATCH 03/33] 10007: Add basic initiateAuth function for user gateway, still a WIP --- .../useCases/auth/changePasswordInteractor.ts | 14 ++++++-------- .../business/useCases/auth/loginInteractor.ts | 16 ++++------------ web-api/src/gateways/user/initiateAuth.ts | 18 ++++++++++++++++++ web-api/src/getUserGateway.ts | 2 ++ 4 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 web-api/src/gateways/user/initiateAuth.ts diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index dccbae50677..bb217b45a6c 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -135,14 +135,12 @@ export const changePasswordInteractor = async ( Username: email, }); - const result = await applicationContext.getCognito().initiateAuth({ - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: password, - USERNAME: email, - }, - ClientId: applicationContext.environment.cognitoClientId, - }); + const result = await applicationContext + .getUserGateway() + .initiateAuth(applicationContext, { + email, + password, + }); if ( !result.AuthenticationResult?.AccessToken || diff --git a/web-api/src/business/useCases/auth/loginInteractor.ts b/web-api/src/business/useCases/auth/loginInteractor.ts index 8cc27a09306..e52c42388cd 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.ts @@ -1,7 +1,4 @@ -import { - AuthFlowType, - ChallengeNameType, -} from '@aws-sdk/client-cognito-identity-provider'; +import { ChallengeNameType } from '@aws-sdk/client-cognito-identity-provider'; import { InvalidRequest, NotFoundError, @@ -15,14 +12,9 @@ export const loginInteractor = async ( { email, password }: { email: string; password: string }, ): Promise<{ idToken: string; accessToken: string; refreshToken: string }> => { try { - const result = await applicationContext.getCognito().initiateAuth({ - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: password, - USERNAME: email, - }, - ClientId: applicationContext.environment.cognitoClientId, - }); + const result = await applicationContext + .getUserGateway() + .initiateAuth(applicationContext, { email, password }); if (result?.ChallengeName === ChallengeNameType.NEW_PASSWORD_REQUIRED) { const PasswordChangeError = new Error('NewPasswordRequired'); diff --git a/web-api/src/gateways/user/initiateAuth.ts b/web-api/src/gateways/user/initiateAuth.ts new file mode 100644 index 00000000000..9d250264e1c --- /dev/null +++ b/web-api/src/gateways/user/initiateAuth.ts @@ -0,0 +1,18 @@ +import { AuthFlowType } from '@aws-sdk/client-cognito-identity-provider'; +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function initiateAuth( + applicationContext: ServerApplicationContext, + { email, password }: { email: string; password: string }, +) { + const result = await applicationContext.getCognito().initiateAuth({ + AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, + AuthParameters: { + PASSWORD: password, + USERNAME: email, + }, + ClientId: applicationContext.environment.cognitoClientId, + }); + + return result; +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index 665f71ba81b..22f5f9cc776 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,7 +1,9 @@ import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; +import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; export const getUserGateway = () => ({ getUserByEmail, + initiateAuth, renewIdToken, }); From 8f01ddb0c6d89c3aed9dcd1b621f2c5e50327c0d Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 1 Mar 2024 14:29:27 -0800 Subject: [PATCH 04/33] 10007: Fixing tests, update initiateAuth command that was missed in change password interactor --- .../auth/changePasswordInteractor.test.ts | 93 +++++++------------ .../useCases/auth/changePasswordInteractor.ts | 17 +--- .../useCases/auth/loginInteractor.test.ts | 20 ++-- 3 files changed, 51 insertions(+), 79 deletions(-) diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index 7ce9e72d2a0..76888776fff 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -1,5 +1,4 @@ import { - AuthFlowType, ChallengeNameType, CodeMismatchException, ExpiredCodeException, @@ -66,7 +65,7 @@ describe('changePasswordInteractor', () => { }; applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); applicationContext @@ -85,7 +84,7 @@ describe('changePasswordInteractor', () => { AuthenticationResult: {}, }; applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); await expect( @@ -97,16 +96,12 @@ describe('changePasswordInteractor', () => { }), ).rejects.toThrow('User is not in `FORCE_CHANGE_PASSWORD` state'); - expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( - { - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: mockPassword, - USERNAME: mockEmail, - }, - ClientId: applicationContext.environment.cognitoClientId, - }, - ); + expect( + applicationContext.getUserGateway().initiateAuth, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, + password: mockPassword, + }); }); it('should update the user`s password in persistence when they are in NEW_PASSWORD_REQUIRED state and their change password request is valid', async () => { @@ -279,7 +274,7 @@ describe('changePasswordInteractor', () => { }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); }); @@ -314,16 +309,12 @@ describe('changePasswordInteractor', () => { Password: mockPassword, Username: mockEmail, }); - expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( - { - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: mockPassword, - USERNAME: mockEmail, - }, - ClientId: applicationContext.environment.cognitoClientId, - }, - ); + expect( + applicationContext.getUserGateway().initiateAuth, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, + password: mockPassword, + }); expect(result).toEqual({ accessToken: mockToken, idToken: mockToken, @@ -332,7 +323,7 @@ describe('changePasswordInteractor', () => { }); it('should throw an error if initiate auth does not return the correct tokens', async () => { - applicationContext.getCognito().initiateAuth.mockResolvedValue({}); + applicationContext.getUserGateway().initiateAuth.mockResolvedValue({}); await expect( changePasswordInteractor(applicationContext, { @@ -343,21 +334,17 @@ describe('changePasswordInteractor', () => { }), ).rejects.toThrow(`Unable to change password for email: ${mockEmail}`); - expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( - { - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: mockPassword, - USERNAME: mockEmail, - }, - ClientId: applicationContext.environment.cognitoClientId, - }, - ); + expect( + applicationContext.getUserGateway().initiateAuth, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, + password: mockPassword, + }); }); it('should throw an InvalidRequest error if initiateAuth returns a CodeMismatchException', async () => { applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValueOnce( new CodeMismatchException({ $metadata: {}, message: '' }), ); @@ -371,21 +358,17 @@ describe('changePasswordInteractor', () => { }), ).rejects.toThrow('Forgot password code is expired or incorrect'); - expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( - { - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: mockPassword, - USERNAME: mockEmail, - }, - ClientId: applicationContext.environment.cognitoClientId, - }, - ); + expect( + applicationContext.getUserGateway().initiateAuth, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, + password: mockPassword, + }); }); it('should throw an InvalidRequest error if initiateAuth returns a ExpiredCodeException', async () => { applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValueOnce( new ExpiredCodeException({ $metadata: {}, message: '' }), ); @@ -399,16 +382,12 @@ describe('changePasswordInteractor', () => { }), ).rejects.toThrow('Forgot password code is expired or incorrect'); - expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( - { - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: mockPassword, - USERNAME: mockEmail, - }, - ClientId: applicationContext.environment.cognitoClientId, - }, - ); + expect( + applicationContext.getUserGateway().initiateAuth, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, + password: mockPassword, + }); }); }); }); diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index bb217b45a6c..01d25568f5f 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -1,7 +1,4 @@ -import { - AuthFlowType, - ChallengeNameType, -} from '@aws-sdk/client-cognito-identity-provider'; +import { ChallengeNameType } from '@aws-sdk/client-cognito-identity-provider'; import { ChangePasswordForm } from '@shared/business/entities/ChangePasswordForm'; import { InvalidEntityError, NotFoundError } from '@web-api/errors/errors'; import { MESSAGE_TYPES } from '@web-api/gateways/worker/workerRouter'; @@ -47,14 +44,10 @@ export const changePasswordInteractor = async ( if (tempPassword) { const initiateAuthResult = await applicationContext - .getCognito() - .initiateAuth({ - AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, - AuthParameters: { - PASSWORD: tempPassword, - USERNAME: email, - }, - ClientId: applicationContext.environment.cognitoClientId, + .getUserGateway() + .initiateAuth(applicationContext, { + email, + password: tempPassword, }); if ( diff --git a/web-api/src/business/useCases/auth/loginInteractor.test.ts b/web-api/src/business/useCases/auth/loginInteractor.test.ts index b0da1640857..8a9fb4da2bd 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.test.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.test.ts @@ -22,7 +22,7 @@ describe('loginInteractor', () => { ChallengeName: ChallengeNameType.NEW_PASSWORD_REQUIRED, }; applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue(mockNewPasswordRequiredResponse); await expect( @@ -41,7 +41,7 @@ describe('loginInteractor', () => { message: '', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockWrongEmailOrPasswordError); await expect( @@ -60,7 +60,7 @@ describe('loginInteractor', () => { message: 'Password attempts exceeded', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockTooManyAttemptsError); await expect( @@ -79,7 +79,7 @@ describe('loginInteractor', () => { message: '', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockWrongEmailOrPasswordError); await expect( @@ -97,7 +97,7 @@ describe('loginInteractor', () => { 'Totally unexpected, unhandled error.', ); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockWrongEmailOrPasswordError); await expect( @@ -108,11 +108,11 @@ describe('loginInteractor', () => { ).rejects.toThrow(mockWrongEmailOrPasswordError); }); - it('should throw an error if initiateAuth does not return access, id, and refresh tokens', async () => { + it('should throw an error when initiateAuth does not return access, id, and refresh tokens', async () => { const mockEmail = 'petitioner@example.com'; const mockPassword = 'MyPa$Sword!'; applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue({ AuthenticationResult: {} }); await expect( @@ -131,7 +131,7 @@ describe('loginInteractor', () => { message: '', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockWrongEmailOrPasswordError); applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ email: mockEmail, @@ -154,7 +154,7 @@ describe('loginInteractor', () => { message: '', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockWrongEmailOrPasswordError); applicationContext .getUserGateway() @@ -182,7 +182,7 @@ describe('loginInteractor', () => { }, }; applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockResolvedValue(mockSuccessFullLoginResponse); const result = await loginInteractor(applicationContext, { From 556be48ee9a0f6a9ddc1cdb29b85a8ada230143e Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 09:51:08 -0800 Subject: [PATCH 05/33] 10007: Fix unit test --- web-api/src/business/useCases/auth/loginInteractor.test.ts | 2 +- web-api/src/business/useCases/auth/loginInteractor.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web-api/src/business/useCases/auth/loginInteractor.test.ts b/web-api/src/business/useCases/auth/loginInteractor.test.ts index 524bb9b03c7..9a1e4d340b5 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.test.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.test.ts @@ -156,7 +156,7 @@ describe('loginInteractor', () => { }, ); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockExpiredTemporaryPasswordExpiredError); applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ email: mockEmail, diff --git a/web-api/src/business/useCases/auth/loginInteractor.ts b/web-api/src/business/useCases/auth/loginInteractor.ts index 4ea52af3a0a..71f085fd910 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.ts @@ -69,9 +69,11 @@ export async function authErrorHandling( await resendTemporaryPassword(applicationContext, { email }); throw new UnauthorizedError('User temporary password expired'); //403 } + if (error.message?.includes('Password attempts exceeded')) { throw new Error('Password attempts exceeded'); } + throw new UnidentifiedUserError('Invalid Username or Password'); //401 Security Concern do not reveal if the user account does not exist or if they have an incorrect password. } From 0635e45d8ecaaf26543fda39b2a3513508073eec Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 10:50:14 -0800 Subject: [PATCH 06/33] 10007: Have initiate auth handle AWS specific errors --- .../auth/changePasswordInteractor.test.ts | 14 +++---- .../useCases/auth/changePasswordInteractor.ts | 27 +++++++------- .../useCases/auth/loginInteractor.test.ts | 25 +++++-------- .../business/useCases/auth/loginInteractor.ts | 28 +++----------- web-api/src/gateways/user/initiateAuth.ts | 37 +++++++++++++++++-- 5 files changed, 69 insertions(+), 62 deletions(-) diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index 76888776fff..89174fad631 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -36,14 +36,13 @@ describe('changePasswordInteractor', () => { }); describe('when the user is attempting to log in with a temporary password', () => { - let mockInitiateAuthResponse: InitiateAuthResponse; + let mockInitiateAuthResponse; let mockRespondToAuthChallengeResponse: RespondToAuthChallengeResponse; let mockUserWithPendingEmail: UserRecord; beforeEach(() => { mockInitiateAuthResponse = { - ChallengeName: ChallengeNameType.NEW_PASSWORD_REQUIRED, - Session: '0943fbef-a573-484a-8164-a1a5a35f8f3e', + session: '0943fbef-a573-484a-8164-a1a5a35f8f3e', }; mockRespondToAuthChallengeResponse = { @@ -80,12 +79,11 @@ describe('changePasswordInteractor', () => { }); it('should throw an error when the user is NOT in NEW_PASSWORD_REQUIRED state', async () => { - mockInitiateAuthResponse = { - AuthenticationResult: {}, - }; + const mockIntiateAuthError = new Error('NewPasswordRequired'); + mockIntiateAuthError.name = 'NewPasswordRequired'; applicationContext .getUserGateway() - .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); + .initiateAuth.mockRejectedValue(mockInitiateAuthResponse); await expect( changePasswordInteractor(applicationContext, { @@ -121,7 +119,7 @@ describe('changePasswordInteractor', () => { USERNAME: mockEmail, }, ClientId: applicationContext.environment.cognitoClientId, - Session: mockInitiateAuthResponse.Session, + Session: mockInitiateAuthResponse.session, }); }); diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index 868bcbc037c..76678b3dcd0 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -43,18 +43,19 @@ export const changePasswordInteractor = async ( } if (tempPassword) { - const initiateAuthResult = await applicationContext - .getUserGateway() - .initiateAuth(applicationContext, { - email, - password: tempPassword, - }); - - if ( - initiateAuthResult.ChallengeName !== - ChallengeNameType.NEW_PASSWORD_REQUIRED - ) { - throw new Error('User is not in `FORCE_CHANGE_PASSWORD` state'); + let initiateAuthResult; + + try { + initiateAuthResult = await applicationContext + .getUserGateway() + .initiateAuth(applicationContext, { + email, + password: tempPassword, + }); + } catch (err: any) { + if (err.name !== 'NewPasswordRequired') { + throw new Error('User is not in `FORCE_CHANGE_PASSWORD` state'); + } } const result = await applicationContext @@ -66,7 +67,7 @@ export const changePasswordInteractor = async ( USERNAME: email, }, ClientId: applicationContext.environment.cognitoClientId, - Session: initiateAuthResult.Session, + Session: initiateAuthResult.session, }); if ( diff --git a/web-api/src/business/useCases/auth/loginInteractor.test.ts b/web-api/src/business/useCases/auth/loginInteractor.test.ts index 9a1e4d340b5..92d17b15769 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.test.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.test.ts @@ -1,6 +1,4 @@ import { - ChallengeNameType, - InitiateAuthCommandOutput, InvalidPasswordException, NotAuthorizedException, UserNotConfirmedException, @@ -17,13 +15,11 @@ describe('loginInteractor', () => { it('should throw an error when the user attempts to log in and they are in a NEW_PASSWORD_REQUIRED state', async () => { const mockEmail = 'petitioner@example.com'; const mockPassword = 'MyPa$Sword!'; - const mockNewPasswordRequiredResponse: InitiateAuthCommandOutput = { - $metadata: {}, - ChallengeName: ChallengeNameType.NEW_PASSWORD_REQUIRED, - }; + const mockNewPasswordRequiredError = new Error('NewPasswordRequired'); + mockNewPasswordRequiredError.name = 'NewPasswordRequired'; applicationContext .getUserGateway() - .initiateAuth.mockResolvedValue(mockNewPasswordRequiredResponse); + .initiateAuth.mockRejectedValue(mockNewPasswordRequiredError); await expect( loginInteractor(applicationContext, { @@ -111,9 +107,11 @@ describe('loginInteractor', () => { it('should throw an error when initiateAuth does not return access, id, and refresh tokens', async () => { const mockEmail = 'petitioner@example.com'; const mockPassword = 'MyPa$Sword!'; + const initiateAuthError = new Error('InitiateAuthError'); + initiateAuthError.name = 'InitiateAuthError'; applicationContext .getUserGateway() - .initiateAuth.mockResolvedValue({ AuthenticationResult: {} }); + .initiateAuth.mockRejectedValue(initiateAuthError); await expect( loginInteractor(applicationContext, { @@ -208,13 +206,10 @@ describe('loginInteractor', () => { it('should return the access, id, refresh tokens to the user when the user is successfully authenticated', async () => { const mockEmail = 'petitioner@example.com'; const mockPassword = 'MyPa$Sword!'; - const mockSuccessFullLoginResponse: InitiateAuthCommandOutput = { - $metadata: {}, - AuthenticationResult: { - AccessToken: 'TEST_ACCESS_TOKEN', - IdToken: 'TEST_ID_TOKEN', - RefreshToken: 'TEST_REFRESH_TOKEN', - }, + const mockSuccessFullLoginResponse = { + accessToken: 'TEST_ACCESS_TOKEN', + idToken: 'TEST_ID_TOKEN', + refreshToken: 'TEST_REFRESH_TOKEN', }; applicationContext .getUserGateway() diff --git a/web-api/src/business/useCases/auth/loginInteractor.ts b/web-api/src/business/useCases/auth/loginInteractor.ts index 71f085fd910..604377dbb5c 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.ts @@ -1,7 +1,4 @@ -import { - AdminCreateUserCommandInput, - ChallengeNameType, -} from '@aws-sdk/client-cognito-identity-provider'; +import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { InvalidRequest, NotFoundError, @@ -15,35 +12,20 @@ export const loginInteractor = async ( { email, password }: { email: string; password: string }, ): Promise<{ idToken: string; accessToken: string; refreshToken: string }> => { try { - const result = await applicationContext + return await applicationContext .getUserGateway() .initiateAuth(applicationContext, { email, password }); - - if (result?.ChallengeName === ChallengeNameType.NEW_PASSWORD_REQUIRED) { - const PasswordChangeError = new Error('NewPasswordRequired'); - PasswordChangeError.name = 'NewPasswordRequired'; - throw PasswordChangeError; - } - - if ( - !result.AuthenticationResult?.AccessToken || - !result.AuthenticationResult?.IdToken || - !result.AuthenticationResult?.RefreshToken - ) { + } catch (err: any) { + if (err.name === 'InitiateAuthError') { throw new Error('Unsuccessful authentication'); } - return { - accessToken: result.AuthenticationResult.AccessToken, - idToken: result.AuthenticationResult.IdToken, - refreshToken: result.AuthenticationResult.RefreshToken, - }; - } catch (err: any) { await authErrorHandling(applicationContext, { email, error: err, sendAccountConfirmation: true, }); + throw err; } }; diff --git a/web-api/src/gateways/user/initiateAuth.ts b/web-api/src/gateways/user/initiateAuth.ts index 9d250264e1c..33f0090e279 100644 --- a/web-api/src/gateways/user/initiateAuth.ts +++ b/web-api/src/gateways/user/initiateAuth.ts @@ -1,10 +1,18 @@ -import { AuthFlowType } from '@aws-sdk/client-cognito-identity-provider'; +import { + AuthFlowType, + ChallengeNameType, +} from '@aws-sdk/client-cognito-identity-provider'; import { ServerApplicationContext } from '@web-api/applicationContext'; export async function initiateAuth( applicationContext: ServerApplicationContext, { email, password }: { email: string; password: string }, -) { +): Promise<{ + accessToken: string; + idToken: string; + refreshToken: string; + session: string | undefined; +}> { const result = await applicationContext.getCognito().initiateAuth({ AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, AuthParameters: { @@ -14,5 +22,28 @@ export async function initiateAuth( ClientId: applicationContext.environment.cognitoClientId, }); - return result; + if (result.ChallengeName) { + if (result.ChallengeName === ChallengeNameType.NEW_PASSWORD_REQUIRED) { + const PasswordChangeError = new Error('NewPasswordRequired'); + PasswordChangeError.name = 'NewPasswordRequired'; + throw PasswordChangeError; + } + } + + if ( + !result.AuthenticationResult?.AccessToken || + !result.AuthenticationResult?.IdToken || + !result.AuthenticationResult?.RefreshToken + ) { + const InitiateAuthError = new Error('InitiateAuthError'); + InitiateAuthError.name = 'InitiateAuthError'; + throw InitiateAuthError; + } + + return { + accessToken: result.AuthenticationResult.AccessToken, + idToken: result.AuthenticationResult.IdToken, + refreshToken: result.AuthenticationResult.RefreshToken, + session: result.Session, + }; } From 611e84f5ba21c706e13011d6605c3cddd863c74e Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 12:33:10 -0800 Subject: [PATCH 07/33] 10007: Change password from temp password can't use initiate auth from user gateway because it needs a session token --- .../auth/changePasswordInteractor.test.ts | 58 +++++++++++-------- .../useCases/auth/changePasswordInteractor.ts | 57 +++++++++--------- web-api/src/gateways/user/initiateAuth.ts | 13 +---- 3 files changed, 63 insertions(+), 65 deletions(-) diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index 89174fad631..5906198a8d9 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -1,8 +1,8 @@ import { + AuthFlowType, ChallengeNameType, CodeMismatchException, ExpiredCodeException, - InitiateAuthResponse, RespondToAuthChallengeResponse, UserStatusType, } from '@aws-sdk/client-cognito-identity-provider'; @@ -42,7 +42,8 @@ describe('changePasswordInteractor', () => { beforeEach(() => { mockInitiateAuthResponse = { - session: '0943fbef-a573-484a-8164-a1a5a35f8f3e', + ChallengeName: ChallengeNameType.NEW_PASSWORD_REQUIRED, + Session: '0943fbef-a573-484a-8164-a1a5a35f8f3e', }; mockRespondToAuthChallengeResponse = { @@ -79,11 +80,12 @@ describe('changePasswordInteractor', () => { }); it('should throw an error when the user is NOT in NEW_PASSWORD_REQUIRED state', async () => { - const mockIntiateAuthError = new Error('NewPasswordRequired'); - mockIntiateAuthError.name = 'NewPasswordRequired'; + mockInitiateAuthResponse = { + AuthenticationResult: {}, + }; applicationContext - .getUserGateway() - .initiateAuth.mockRejectedValue(mockInitiateAuthResponse); + .getCognito() + .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); await expect( changePasswordInteractor(applicationContext, { @@ -94,15 +96,23 @@ describe('changePasswordInteractor', () => { }), ).rejects.toThrow('User is not in `FORCE_CHANGE_PASSWORD` state'); - expect( - applicationContext.getUserGateway().initiateAuth, - ).toHaveBeenCalledWith(applicationContext, { - email: mockEmail, - password: mockPassword, - }); + expect(applicationContext.getCognito().initiateAuth).toHaveBeenCalledWith( + { + AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, + AuthParameters: { + PASSWORD: mockPassword, + USERNAME: mockEmail, + }, + ClientId: expect.anything(), + }, + ); }); it('should update the user`s password in persistence when they are in NEW_PASSWORD_REQUIRED state and their change password request is valid', async () => { + applicationContext + .getCognito() + .initiateAuth.mockResolvedValue(mockInitiateAuthResponse); + await changePasswordInteractor(applicationContext, { confirmPassword: mockPassword, email: mockEmail, @@ -119,7 +129,7 @@ describe('changePasswordInteractor', () => { USERNAME: mockEmail, }, ClientId: applicationContext.environment.cognitoClientId, - Session: mockInitiateAuthResponse.session, + Session: mockInitiateAuthResponse.Session, }); }); @@ -235,7 +245,7 @@ describe('changePasswordInteractor', () => { }); describe('when the user is attempting to log in with a forgot password code', () => { - let mockInitiateAuthResponse: InitiateAuthResponse; + let mockInitiateAuthResponse; let mockUser: { userId: string; email: string; @@ -256,11 +266,9 @@ describe('changePasswordInteractor', () => { }; mockInitiateAuthResponse = { - AuthenticationResult: { - AccessToken: mockToken, - IdToken: mockToken, - RefreshToken: mockToken, - }, + accessToken: mockToken, + idToken: mockToken, + refreshToken: mockToken, }; applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ @@ -320,8 +328,12 @@ describe('changePasswordInteractor', () => { }); }); - it('should throw an error if initiate auth does not return the correct tokens', async () => { - applicationContext.getUserGateway().initiateAuth.mockResolvedValue({}); + it('should throw an error when initiate auth does not return the correct tokens', async () => { + const initiateAuthError = new Error('InitiateAuthError'); + initiateAuthError.name = 'InitiateAuthError'; + applicationContext + .getUserGateway() + .initiateAuth.mockRejectedValue(initiateAuthError); await expect( changePasswordInteractor(applicationContext, { @@ -340,7 +352,7 @@ describe('changePasswordInteractor', () => { }); }); - it('should throw an InvalidRequest error if initiateAuth returns a CodeMismatchException', async () => { + it('should throw an InvalidRequest error when initiateAuth returns a CodeMismatchException', async () => { applicationContext .getUserGateway() .initiateAuth.mockRejectedValueOnce( @@ -364,7 +376,7 @@ describe('changePasswordInteractor', () => { }); }); - it('should throw an InvalidRequest error if initiateAuth returns a ExpiredCodeException', async () => { + it('should throw an InvalidRequest error when initiateAuth returns a ExpiredCodeException', async () => { applicationContext .getUserGateway() .initiateAuth.mockRejectedValueOnce( diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index 76678b3dcd0..0fdb83a89f9 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -1,4 +1,7 @@ -import { ChallengeNameType } from '@aws-sdk/client-cognito-identity-provider'; +import { + AuthFlowType, + ChallengeNameType, +} from '@aws-sdk/client-cognito-identity-provider'; import { ChangePasswordForm } from '@shared/business/entities/ChangePasswordForm'; import { InvalidEntityError, NotFoundError } from '@web-api/errors/errors'; import { MESSAGE_TYPES } from '@web-api/gateways/worker/workerRouter'; @@ -43,19 +46,22 @@ export const changePasswordInteractor = async ( } if (tempPassword) { - let initiateAuthResult; - - try { - initiateAuthResult = await applicationContext - .getUserGateway() - .initiateAuth(applicationContext, { - email, - password: tempPassword, - }); - } catch (err: any) { - if (err.name !== 'NewPasswordRequired') { - throw new Error('User is not in `FORCE_CHANGE_PASSWORD` state'); - } + const initiateAuthResult = await applicationContext + .getCognito() + .initiateAuth({ + AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, + AuthParameters: { + PASSWORD: tempPassword, + USERNAME: email, + }, + ClientId: applicationContext.environment.cognitoClientId, + }); + + if ( + initiateAuthResult.ChallengeName !== + ChallengeNameType.NEW_PASSWORD_REQUIRED + ) { + throw new Error('User is not in `FORCE_CHANGE_PASSWORD` state'); } const result = await applicationContext @@ -67,7 +73,7 @@ export const changePasswordInteractor = async ( USERNAME: email, }, ClientId: applicationContext.environment.cognitoClientId, - Session: initiateAuthResult.session, + Session: initiateAuthResult.Session, }); if ( @@ -129,33 +135,24 @@ export const changePasswordInteractor = async ( Username: email, }); - const result = await applicationContext + return await applicationContext .getUserGateway() .initiateAuth(applicationContext, { email, password, }); - - if ( - !result.AuthenticationResult?.AccessToken || - !result.AuthenticationResult?.IdToken || - !result.AuthenticationResult?.RefreshToken - ) { - throw new Error(`Unable to change password for email: ${email}`); - } - - return { - accessToken: result.AuthenticationResult.AccessToken, - idToken: result.AuthenticationResult.IdToken, - refreshToken: result.AuthenticationResult.RefreshToken, - }; } } catch (err: any) { + if (err.name === 'InitiateAuthError') { + throw new Error(`Unable to change password for email: ${email}`); + } + await authErrorHandling(applicationContext, { email, error: err, sendAccountConfirmation: false, }); + throw err; } }; diff --git a/web-api/src/gateways/user/initiateAuth.ts b/web-api/src/gateways/user/initiateAuth.ts index 33f0090e279..5fef2ce8a0d 100644 --- a/web-api/src/gateways/user/initiateAuth.ts +++ b/web-api/src/gateways/user/initiateAuth.ts @@ -1,7 +1,4 @@ -import { - AuthFlowType, - ChallengeNameType, -} from '@aws-sdk/client-cognito-identity-provider'; +import { AuthFlowType } from '@aws-sdk/client-cognito-identity-provider'; import { ServerApplicationContext } from '@web-api/applicationContext'; export async function initiateAuth( @@ -22,14 +19,6 @@ export async function initiateAuth( ClientId: applicationContext.environment.cognitoClientId, }); - if (result.ChallengeName) { - if (result.ChallengeName === ChallengeNameType.NEW_PASSWORD_REQUIRED) { - const PasswordChangeError = new Error('NewPasswordRequired'); - PasswordChangeError.name = 'NewPasswordRequired'; - throw PasswordChangeError; - } - } - if ( !result.AuthenticationResult?.AccessToken || !result.AuthenticationResult?.IdToken || From c7cd506e90fe4ae8590f419528c815cd623311d5 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 13:04:38 -0800 Subject: [PATCH 08/33] 10007: Extract confirmForgotPassword to changePassword user gateway function --- .../auth/changePasswordInteractor.test.ts | 2 +- .../useCases/auth/changePasswordInteractor.ts | 20 +++++++++++++------ web-api/src/gateways/user/changePassword.ts | 17 ++++++++++++++++ web-api/src/gateways/user/initiateAuth.ts | 2 -- web-api/src/getUserGateway.ts | 2 ++ 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 web-api/src/gateways/user/changePassword.ts diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index 5906198a8d9..b9e057c175a 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -308,7 +308,7 @@ describe('changePasswordInteractor', () => { }); expect( - applicationContext.getCognito().confirmForgotPassword, + applicationContext.getUserGateway().confirmForgotPassword, ).toHaveBeenCalledWith({ ClientId: applicationContext.environment.cognitoClientId, ConfirmationCode: mockCode, diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.ts index 0fdb83a89f9..ad210ed4378 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.ts @@ -128,12 +128,20 @@ export const changePasswordInteractor = async ( throw new NotFoundError(`User not found with email: ${email}`); } - await applicationContext.getCognito().confirmForgotPassword({ - ClientId: applicationContext.environment.cognitoClientId, - ConfirmationCode: code, - Password: password, - Username: email, - }); + if (!code) { + applicationContext.logger.info( + `Unable to change password for ${email}. No code was provided.`, + ); + throw new Error('Unable to change password'); + } + + await applicationContext + .getUserGateway() + .changePassword(applicationContext, { + code, + email, + newPassword: password, + }); return await applicationContext .getUserGateway() diff --git a/web-api/src/gateways/user/changePassword.ts b/web-api/src/gateways/user/changePassword.ts new file mode 100644 index 00000000000..1e4b85037d6 --- /dev/null +++ b/web-api/src/gateways/user/changePassword.ts @@ -0,0 +1,17 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function changePassword( + applicationContext: ServerApplicationContext, + { + code, + email, + newPassword, + }: { code: string; newPassword: string; email: string }, +): Promise { + await applicationContext.getCognito().confirmForgotPassword({ + ClientId: applicationContext.environment.cognitoClientId, + ConfirmationCode: code, + Password: newPassword, + Username: email, + }); +} diff --git a/web-api/src/gateways/user/initiateAuth.ts b/web-api/src/gateways/user/initiateAuth.ts index 5fef2ce8a0d..3f33fa82fbd 100644 --- a/web-api/src/gateways/user/initiateAuth.ts +++ b/web-api/src/gateways/user/initiateAuth.ts @@ -8,7 +8,6 @@ export async function initiateAuth( accessToken: string; idToken: string; refreshToken: string; - session: string | undefined; }> { const result = await applicationContext.getCognito().initiateAuth({ AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, @@ -33,6 +32,5 @@ export async function initiateAuth( accessToken: result.AuthenticationResult.AccessToken, idToken: result.AuthenticationResult.IdToken, refreshToken: result.AuthenticationResult.RefreshToken, - session: result.Session, }; } diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index 22f5f9cc776..ea970512dcd 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,8 +1,10 @@ +import { changePassword } from '@web-api/gateways/user/changePassword'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; export const getUserGateway = () => ({ + changePassword, getUserByEmail, initiateAuth, renewIdToken, From b37cc0c726a5fa1991db060d9bcc5f0a745a9690 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 13:23:39 -0800 Subject: [PATCH 09/33] 10007: Throw new password needed error when trying to initiate auth when user account status is new password required, inline calls to cognito for easier searching --- .../useCases/auth/forgotPasswordInteractor.ts | 9 ++----- .../useCases/auth/signUpUserInteractor.ts | 21 +++++++--------- .../health/getHealthCheckInteractor.ts | 4 +-- .../user/verifyUserPendingEmailInteractor.ts | 5 +--- web-api/src/gateways/user/getUserByEmail.ts | 3 +-- web-api/src/gateways/user/initiateAuth.ts | 13 +++++++++- .../dynamo/users/createNewPetitionerUser.ts | 9 ++----- .../users/createOrUpdatePractitionerUser.ts | 25 +++++++++---------- .../dynamo/users/createOrUpdateUser.ts | 25 +++++++------------ .../dynamo/users/updatePractitionerUser.ts | 4 +-- 10 files changed, 50 insertions(+), 68 deletions(-) diff --git a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts index f7d58506fbe..7d7c27fff6f 100644 --- a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts @@ -1,9 +1,6 @@ -import { - CognitoIdentityProvider, - UserStatusType, -} from '@aws-sdk/client-cognito-identity-provider'; import { ServerApplicationContext } from '@web-api/applicationContext'; import { UnauthorizedError } from '@web-api/errors/errors'; +import { UserStatusType } from '@aws-sdk/client-cognito-identity-provider'; import { resendTemporaryPassword } from '@web-api/business/useCases/auth/loginInteractor'; export const forgotPasswordInteractor = async ( @@ -14,8 +11,6 @@ export const forgotPasswordInteractor = async ( email: string; }, ): Promise => { - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - const user = await applicationContext .getUserGateway() .getUserByEmail(applicationContext, { email }); @@ -40,7 +35,7 @@ export const forgotPasswordInteractor = async ( throw new UnauthorizedError('User is unconfirmed'); //403 } - await cognito.forgotPassword({ + await applicationContext.getCognito().forgotPassword({ ClientId: applicationContext.environment.cognitoClientId, Username: email, }); diff --git a/web-api/src/business/useCases/auth/signUpUserInteractor.ts b/web-api/src/business/useCases/auth/signUpUserInteractor.ts index 934d4d24f1b..c7e15ae26bb 100644 --- a/web-api/src/business/useCases/auth/signUpUserInteractor.ts +++ b/web-api/src/business/useCases/auth/signUpUserInteractor.ts @@ -1,10 +1,7 @@ -import { - CognitoIdentityProvider, - UserStatusType, -} from '@aws-sdk/client-cognito-identity-provider'; import { NewPetitionerUser } from '@shared/business/entities/NewPetitionerUser'; import { ROLES } from '@shared/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; +import { UserStatusType } from '@aws-sdk/client-cognito-identity-provider'; export type SignUpUserResponse = { email: string; @@ -25,15 +22,15 @@ export const signUpUserInteractor = async ( }; }, ): Promise => { - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - // Temporary code to prevent creation of duplicate accounts while Cognito is still case sensitive. // We do not want to allow people to make two accounts for the same email that only differ by casing. - const { Users: existingAccounts } = await cognito.listUsers({ - AttributesToGet: ['email'], - Filter: `email = "${user.email}"`, - UserPoolId: applicationContext.environment.userPoolId, - }); + const { Users: existingAccounts } = await applicationContext + .getCognito() + .listUsers({ + AttributesToGet: ['email'], + Filter: `email = "${user.email}"`, + UserPoolId: applicationContext.environment.userPoolId, + }); if (existingAccounts?.length) { const accountUnconfirmed = existingAccounts.some( @@ -49,7 +46,7 @@ export const signUpUserInteractor = async ( const newUser = new NewPetitionerUser(user).validate().toRawObject(); const userId = applicationContext.getUniqueId(); - await cognito.signUp({ + await applicationContext.getCognito().signUp({ ClientId: applicationContext.environment.cognitoClientId, Password: newUser.password, UserAttributes: [ diff --git a/web-api/src/business/useCases/health/getHealthCheckInteractor.ts b/web-api/src/business/useCases/health/getHealthCheckInteractor.ts index 0cbf19d2b31..ea14d13fd73 100644 --- a/web-api/src/business/useCases/health/getHealthCheckInteractor.ts +++ b/web-api/src/business/useCases/health/getHealthCheckInteractor.ts @@ -198,9 +198,7 @@ const getCognitoStatus = async ({ applicationContext: ServerApplicationContext; }): Promise => { try { - const cognito = applicationContext.getCognito(); - - await cognito.describeUserPool({ + await applicationContext.getCognito().describeUserPool({ UserPoolId: applicationContext.environment.userPoolId, }); return true; diff --git a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts index 7c9b1df6c26..9e02c33a880 100644 --- a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts +++ b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts @@ -1,4 +1,3 @@ -import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider'; import { MESSAGE_TYPES } from '@web-api/gateways/worker/workerRouter'; import { ROLE_PERMISSIONS, @@ -51,9 +50,7 @@ export const verifyUserPendingEmailInteractor = async ( }, ); - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - - await cognito.adminUpdateUserAttributes({ + await applicationContext.getCognito().adminUpdateUserAttributes({ UserAttributes: [ { Name: 'email', diff --git a/web-api/src/gateways/user/getUserByEmail.ts b/web-api/src/gateways/user/getUserByEmail.ts index 8bf80ccfd20..cb0c4b64a51 100644 --- a/web-api/src/gateways/user/getUserByEmail.ts +++ b/web-api/src/gateways/user/getUserByEmail.ts @@ -29,10 +29,9 @@ export const getUserByEmail = async ( } | undefined > => { - const cognito = applicationContext.getCognito(); let foundUser: AdminGetUserCommandOutput; try { - foundUser = await cognito.adminGetUser({ + foundUser = await applicationContext.getCognito().adminGetUser({ UserPoolId: process.env.USER_POOL_ID, Username: email, }); diff --git a/web-api/src/gateways/user/initiateAuth.ts b/web-api/src/gateways/user/initiateAuth.ts index 3f33fa82fbd..a018ab13912 100644 --- a/web-api/src/gateways/user/initiateAuth.ts +++ b/web-api/src/gateways/user/initiateAuth.ts @@ -1,4 +1,7 @@ -import { AuthFlowType } from '@aws-sdk/client-cognito-identity-provider'; +import { + AuthFlowType, + ChallengeNameType, +} from '@aws-sdk/client-cognito-identity-provider'; import { ServerApplicationContext } from '@web-api/applicationContext'; export async function initiateAuth( @@ -18,6 +21,14 @@ export async function initiateAuth( ClientId: applicationContext.environment.cognitoClientId, }); + if (result.ChallengeName) { + if (result.ChallengeName === ChallengeNameType.NEW_PASSWORD_REQUIRED) { + const PasswordChangeError = new Error('NewPasswordRequired'); + PasswordChangeError.name = 'NewPasswordRequired'; + throw PasswordChangeError; + } + } + if ( !result.AuthenticationResult?.AccessToken || !result.AuthenticationResult?.IdToken || diff --git a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts index 1ad11e56d79..2b492b4735c 100644 --- a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts @@ -1,8 +1,5 @@ import * as client from '../../dynamodbClientService'; -import { - AdminCreateUserCommandInput, - CognitoIdentityProvider, -} from '@aws-sdk/client-cognito-identity-provider'; +import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { RawUser } from '@shared/business/entities/User'; @@ -72,9 +69,7 @@ export const createNewPetitionerUser = async ({ input.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; } - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - - await cognito.adminCreateUser(input); + await applicationContext.getCognito().adminCreateUser(input); const newUser: RawUser = await createUserRecords({ applicationContext, diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts index 7c3546a5133..bbb184903e6 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts @@ -1,10 +1,8 @@ import * as client from '../../dynamodbClientService'; -import { - AdminCreateUserCommandInput, - CognitoIdentityProvider, -} from '@aws-sdk/client-cognito-identity-provider'; +import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { RawUser } from '@shared/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; import { isUserAlreadyCreated } from './createOrUpdateUser'; export const createUserRecords = async ({ @@ -61,7 +59,7 @@ export const createOrUpdatePractitionerUser = async ({ applicationContext, user, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; user: RawUser; }) => { let userId = applicationContext.getUniqueId(); @@ -93,8 +91,6 @@ export const createOrUpdatePractitionerUser = async ({ userPoolId: process.env.USER_POOL_ID as string, }); - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - if (!userExists) { let params: AdminCreateUserCommandInput = { DesiredDeliveryMediums: ['EMAIL'], @@ -124,7 +120,9 @@ export const createOrUpdatePractitionerUser = async ({ params.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; } - const response = await cognito.adminCreateUser(params); + const response = await applicationContext + .getCognito() + .adminCreateUser(params); if (response?.User?.Username) { const userIdAttribute = @@ -141,12 +139,13 @@ export const createOrUpdatePractitionerUser = async ({ userId = userIdAttribute?.Value!; } } else { - const response = await cognito.adminGetUser({ - UserPoolId: process.env.USER_POOL_ID, - Username: userEmail, - }); + const response = await applicationContext + .getUserGateway() + .getUserByEmail(applicationContext, { + email: userEmail, + }); - await cognito.adminUpdateUserAttributes({ + await applicationContext.getCognito().adminUpdateUserAttributes({ UserAttributes: [ { Name: 'custom:role', diff --git a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts index 511194dbb0c..3ee4002ed1d 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts @@ -1,14 +1,12 @@ import * as client from '../../dynamodbClientService'; -import { - CognitoIdentityProvider, - UserNotFoundException, -} from '@aws-sdk/client-cognito-identity-provider'; import { DOCKET_SECTION, PETITIONS_SECTION, ROLES, } from '../../../../../shared/src/business/entities/EntityConstants'; import { RawUser } from '@shared/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; +import { UserNotFoundException } from '@aws-sdk/client-cognito-identity-provider'; export const createUserRecords = async ({ applicationContext, @@ -117,14 +115,12 @@ export const isUserAlreadyCreated = async ({ email, userPoolId, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; email: string; userPoolId: string; }) => { - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - try { - await cognito.adminGetUser({ + await applicationContext.getCognito().adminGetUser({ UserPoolId: userPoolId, Username: email, }); @@ -144,7 +140,7 @@ export const createOrUpdateUser = async ({ password, user, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; disableCognitoUser: boolean; password: string; user: RawUser; @@ -161,11 +157,8 @@ export const createOrUpdateUser = async ({ userPoolId: userPoolId as string, }); - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - if (!userExists) { - const response = await cognito.adminCreateUser({ - //TODO: make 1000000% sure this works fine on deployed env + const response = await applicationContext.getCognito().adminCreateUser({ DesiredDeliveryMediums: ['EMAIL'], MessageAction: 'SUPPRESS', TemporaryPassword: password, @@ -193,11 +186,11 @@ export const createOrUpdateUser = async ({ // replace sub here userId = response.User!.Username; } else { - const response = await cognito.adminGetUser({ + const response = await applicationContext.getCognito().adminGetUser({ UserPoolId: userPoolId, Username: user.email, }); - await cognito.adminUpdateUserAttributes({ + await applicationContext.getCognito().adminUpdateUserAttributes({ UserAttributes: [ { Name: 'custom:role', @@ -213,7 +206,7 @@ export const createOrUpdateUser = async ({ } if (disableCognitoUser) { - await cognito.adminDisableUser({ + await applicationContext.getCognito().adminDisableUser({ UserPoolId: userPoolId, Username: userId, }); diff --git a/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts index 4ff0f2198d0..a15f57ff8f6 100644 --- a/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts @@ -1,4 +1,3 @@ -import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider'; import { RawUser } from '@shared/business/entities/User'; import { getUserById } from './getUserById'; import { updateUserRecords } from './updateUserRecords'; @@ -18,8 +17,7 @@ export const updatePractitionerUser = async ({ }); try { - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - await cognito.adminUpdateUserAttributes({ + await applicationContext.getCognito().adminUpdateUserAttributes({ UserAttributes: [ { Name: 'custom:role', From 5e8d68e83989f1b7124318b6897e02c892cea0d9 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 13:30:54 -0800 Subject: [PATCH 10/33] 10007: Move createUserInteractor to web api, extract disable user into a utility function --- .../user}/createUserInteractor.test.ts | 4 +-- .../useCases/user}/createUserInteractor.ts | 28 +++++++++++++------ web-api/src/gateways/user/disableUser.ts | 11 ++++++++ web-api/src/getUseCases.ts | 2 +- web-api/src/getUserGateway.ts | 2 ++ .../dynamo/users/createOrUpdateUser.ts | 9 ------ 6 files changed, 36 insertions(+), 20 deletions(-) rename {shared/src/business/useCases/users => web-api/src/business/useCases/user}/createUserInteractor.test.ts (97%) rename {shared/src/business/useCases/users => web-api/src/business/useCases/user}/createUserInteractor.ts (58%) create mode 100644 web-api/src/gateways/user/disableUser.ts diff --git a/shared/src/business/useCases/users/createUserInteractor.test.ts b/web-api/src/business/useCases/user/createUserInteractor.test.ts similarity index 97% rename from shared/src/business/useCases/users/createUserInteractor.test.ts rename to web-api/src/business/useCases/user/createUserInteractor.test.ts index 3c9d14b6ebb..9c2033da033 100644 --- a/shared/src/business/useCases/users/createUserInteractor.test.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.test.ts @@ -1,6 +1,6 @@ -import { ROLES } from '../../entities/EntityConstants'; +import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { UnauthorizedError } from '@web-api/errors/errors'; -import { applicationContext } from '../../test/createTestApplicationContext'; +import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { createUserInteractor } from './createUserInteractor'; describe('create user', () => { diff --git a/shared/src/business/useCases/users/createUserInteractor.ts b/web-api/src/business/useCases/user/createUserInteractor.ts similarity index 58% rename from shared/src/business/useCases/users/createUserInteractor.ts rename to web-api/src/business/useCases/user/createUserInteractor.ts index 08dfc3e5067..d0ea9c49c7f 100644 --- a/shared/src/business/useCases/users/createUserInteractor.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.ts @@ -1,15 +1,22 @@ -import { Practitioner, RawPractitioner } from '../../entities/Practitioner'; -import { ROLES } from '../../entities/EntityConstants'; +import { + Practitioner, + RawPractitioner, +} from '../../../../../shared/src/business/entities/Practitioner'; +import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { ROLE_PERMISSIONS, isAuthorized, -} from '../../../authorization/authorizationClientService'; -import { RawUser, User } from '../../entities/User'; -import { UnauthorizedError } from '../../../../../web-api/src/errors/errors'; -import { createPractitionerUser } from '../../utilities/createPractitionerUser'; +} from '../../../../../shared/src/authorization/authorizationClientService'; +import { + RawUser, + User, +} from '../../../../../shared/src/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; +import { UnauthorizedError } from '../../../errors/errors'; +import { createPractitionerUser } from '../../../../../shared/src/business/utilities/createPractitionerUser'; export const createUserInteractor = async ( - applicationContext: IApplicationContext, + applicationContext: ServerApplicationContext, { user }: { user: RawUser & { barNumber?: string; password: string } }, ): Promise => { const requestUser = applicationContext.getCurrentUser(); @@ -43,11 +50,16 @@ export const createUserInteractor = async ( .getPersistenceGateway() .createOrUpdateUser({ applicationContext, - disableCognitoUser: user.role === ROLES.legacyJudge, password: user.password, user: userEntity.validate().toRawObject(), }); + if (user.role === ROLES.legacyJudge) { + await applicationContext.getUserGateway().disableUser(applicationContext, { + userId, + }); + } + userEntity.userId = userId; return userEntity.validate().toRawObject(); diff --git a/web-api/src/gateways/user/disableUser.ts b/web-api/src/gateways/user/disableUser.ts new file mode 100644 index 00000000000..5c0a52efaf4 --- /dev/null +++ b/web-api/src/gateways/user/disableUser.ts @@ -0,0 +1,11 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function disableUser( + applicationContext: ServerApplicationContext, + { userId }: { userId: string }, +): Promise { + await applicationContext.getCognito().adminDisableUser({ + UserPoolId: process.env.USER_POOL_ID, + Username: userId, + }); +} diff --git a/web-api/src/getUseCases.ts b/web-api/src/getUseCases.ts index bd4524b10a7..8f0a5550a23 100644 --- a/web-api/src/getUseCases.ts +++ b/web-api/src/getUseCases.ts @@ -33,7 +33,7 @@ import { createPetitionerAccountInteractor } from '../../shared/src/business/use import { createPractitionerDocumentInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerDocumentInteractor'; import { createPractitionerUserInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerUserInteractor'; import { createTrialSessionInteractor } from '../../shared/src/business/useCases/trialSessions/createTrialSessionInteractor'; -import { createUserInteractor } from '../../shared/src/business/useCases/users/createUserInteractor'; +import { createUserInteractor } from './business/useCases/user/createUserInteractor'; import { deleteCaseDeadlineInteractor } from '../../shared/src/business/useCases/caseDeadline/deleteCaseDeadlineInteractor'; import { deleteCaseNoteInteractor } from '../../shared/src/business/useCases/caseNote/deleteCaseNoteInteractor'; import { deleteCounselFromCaseInteractor } from './business/useCases/caseAssociation/deleteCounselFromCaseInteractor'; diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index ea970512dcd..d2fbfbe22fa 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,10 +1,12 @@ import { changePassword } from '@web-api/gateways/user/changePassword'; +import { disableUser } from '@web-api/gateways/user/disableUser'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; export const getUserGateway = () => ({ changePassword, + disableUser, getUserByEmail, initiateAuth, renewIdToken, diff --git a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts index 3ee4002ed1d..38eb8437197 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts @@ -136,12 +136,10 @@ export const isUserAlreadyCreated = async ({ export const createOrUpdateUser = async ({ applicationContext, - disableCognitoUser = false, password, user, }: { applicationContext: ServerApplicationContext; - disableCognitoUser: boolean; password: string; user: RawUser; }) => { @@ -205,13 +203,6 @@ export const createOrUpdateUser = async ({ userId = response.Username; } - if (disableCognitoUser) { - await applicationContext.getCognito().adminDisableUser({ - UserPoolId: userPoolId, - Username: userId, - }); - } - return await createUserRecords({ applicationContext, user, From 7300afd04fd3162d6d58a6384c36f2df60c2ce11 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 14:14:29 -0800 Subject: [PATCH 11/33] 10007: Add admin user to mockUsers --- shared/src/test/mockUsers.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shared/src/test/mockUsers.ts b/shared/src/test/mockUsers.ts index 960bebf95ae..32266c75e3c 100644 --- a/shared/src/test/mockUsers.ts +++ b/shared/src/test/mockUsers.ts @@ -18,6 +18,13 @@ import { getJudgesChambersWithLegacy, } from '../../../web-client/src/business/chambers/getJudgesChambers'; +export const adminUser: RawUser = { + entityName: 'User', + name: 'Test admin', + role: ROLES.admin, + userId: 'ad5b7d39-8fae-4c2f-893c-3c829598bc71', +}; + export const adcUser = { name: 'ADC', role: ROLES.adc, From c2208ee71ad53c6c59d3058701c13f1336e7721c Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 14:16:51 -0800 Subject: [PATCH 12/33] 10007: Update types, fix tests --- .../auth/changePasswordInteractor.test.ts | 11 +- .../user/createUserInteractor.test.ts | 226 ++++++------------ .../useCases/user/createUserInteractor.ts | 9 +- .../createOrUpdatePractitionerUser.test.ts | 51 +--- .../users/createOrUpdatePractitionerUser.ts | 42 ++-- .../dynamo/users/createOrUpdateUser.test.ts | 29 --- 6 files changed, 111 insertions(+), 257 deletions(-) diff --git a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts index b9e057c175a..3f7e2ece3af 100644 --- a/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/changePasswordInteractor.test.ts @@ -308,12 +308,11 @@ describe('changePasswordInteractor', () => { }); expect( - applicationContext.getUserGateway().confirmForgotPassword, - ).toHaveBeenCalledWith({ - ClientId: applicationContext.environment.cognitoClientId, - ConfirmationCode: mockCode, - Password: mockPassword, - Username: mockEmail, + applicationContext.getUserGateway().changePassword, + ).toHaveBeenCalledWith(applicationContext, { + code: mockCode, + email: mockEmail, + newPassword: mockPassword, }); expect( applicationContext.getUserGateway().initiateAuth, diff --git a/web-api/src/business/useCases/user/createUserInteractor.test.ts b/web-api/src/business/useCases/user/createUserInteractor.test.ts index 9c2033da033..a2744639b4b 100644 --- a/web-api/src/business/useCases/user/createUserInteractor.test.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.test.ts @@ -1,66 +1,39 @@ -import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; +import { + ROLES, + SERVICE_INDICATOR_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; +import { RawUser } from '@shared/business/entities/User'; import { UnauthorizedError } from '@web-api/errors/errors'; +import { adminUser, petitionerUser } from '@shared/test/mockUsers'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { createUserInteractor } from './createUserInteractor'; -describe('create user', () => { - it('creates the user', async () => { - const mockUser = { - name: 'Test PetitionsClerk', - role: ROLES.petitionsClerk, - userId: '615b7d39-8fae-4c2f-893c-3c829598bc71', - }; - - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.admin, - userId: 'ad3b7d39-8fae-4c2f-893c-3c829598bc71', - }); - applicationContext - .getPersistenceGateway() - .createOrUpdateUser.mockReturnValue(mockUser); - - const userToCreate = { - barNumber: '', - name: 'Jesse Pinkman', - role: ROLES.petitionsClerk, - userId: '245b7d39-8fae-4c2f-893c-3c829598bc71', - }; - const user = await createUserInteractor(applicationContext, { - user: userToCreate, - } as any); - expect(user).not.toBeUndefined(); - }); - - it('throws unauthorized for any user without an "admin" role', async () => { +describe('createUserInteractor', () => { + it('should throw an unauthorized error when the current user does NOT have an "admin" role', async () => { const mockUser = { name: 'Test Petitioner', role: ROLES.petitioner, - userId: '245b7d39-8fae-4c2f-893c-3c829598bc71', + userId: '615b7d39-8fae-4c2f-893c-3c829598bc71', }; - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.petitioner, - userId: 'ad2b7d39-8fae-4c2f-893c-3c829598bc71', - }); + applicationContext.getCurrentUser.mockReturnValue(petitionerUser); applicationContext .getPersistenceGateway() .createOrUpdateUser.mockReturnValue(mockUser); - const userToCreate = { userId: '145b7d39-8fae-4c2f-893c-3c829598bc71' }; await expect( createUserInteractor(applicationContext, { - user: userToCreate, - } as any), + user: { + entityName: 'User', + name: 'Test Petitioner', + password: 'P@ssw0rd', + role: ROLES.petitioner, + } as RawUser, + }), ).rejects.toThrow(UnauthorizedError); }); it('should create a practitioner user when the user role is privatePractitioner', async () => { - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.admin, - userId: 'ad5b7d39-8fae-4c2f-893c-3c829598bc71', - }); + applicationContext.getCurrentUser.mockReturnValue(adminUser); applicationContext.getPersistenceGateway().createUser.mockReturnValue({ barNumber: 'CS20001', name: 'Test PrivatePractitioner', @@ -68,21 +41,23 @@ describe('create user', () => { userId: '745b7d39-8fae-4c2f-893c-3c829598bc71', }); - const userToCreate = { - admissionsDate: '2020-03-14', - admissionsStatus: 'Active', - birthYear: '1993', - employer: 'Private', - firstName: 'Test', - lastName: 'PrivatePractitioner', - originalBarState: 'CA', - practitionerType: 'Attorney', - role: ROLES.privatePractitioner, - }; - const user = await createUserInteractor(applicationContext, { - user: userToCreate, - } as any); + user: { + admissionsDate: '2020-03-14', + admissionsStatus: 'Active', + birthYear: '1993', + employer: 'Private', + entityName: 'Practitioner', + firstName: 'Test', + lastName: 'PrivatePractitioner', + name: 'Test PrivatePractitioner', + originalBarState: 'CA', + password: 'P@ssw0rd', + practitionerType: 'Attorney', + role: ROLES.privatePractitioner, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_PAPER, + }, + }); expect(user).toMatchObject({ barNumber: 'CS20001', @@ -91,19 +66,16 @@ describe('create user', () => { }); it('should create a practitioner user when the user role is irsPractitioner', async () => { - applicationContext.getCurrentUser.mockReturnValue({ - role: ROLES.admin, - userId: 'admin', - }); + const mockAdmissionsDate = '1876-02-19'; + applicationContext.getCurrentUser.mockReturnValue(adminUser); applicationContext .getPersistenceGateway() .createOrUpdateUser.mockReturnValue({ barNumber: 'CS20001', - name: 'Test PrivatePractitioner', + name: 'Test IrsPractitioner', role: ROLES.irsPractitioner, userId: '745b7d39-8fae-4c2f-893c-3c829598bc71', }); - const mockAdmissionsDate = '1876-02-19'; const user = await createUserInteractor(applicationContext, { user: { @@ -111,12 +83,16 @@ describe('create user', () => { admissionsStatus: 'Active', birthYear: '1993', employer: 'DOJ', + entityName: 'Practitioner', firstName: 'Test', lastName: 'IRSPractitioner', + name: 'Test IRS Practitioner', originalBarState: 'CA', + password: 'P@ssw0rd', practitionerType: 'Attorney', role: ROLES.irsPractitioner, - } as any, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_NONE, + }, }); expect(user).toMatchObject({ @@ -126,10 +102,8 @@ describe('create user', () => { }); it('should create a practitioner user when the user role is inactivePractitioner', async () => { - applicationContext.getCurrentUser.mockReturnValue({ - role: ROLES.admin, - userId: 'admin', - }); + const mockAdmissionsDate = '1876-02-19'; + applicationContext.getCurrentUser.mockReturnValue(adminUser); applicationContext .getPersistenceGateway() .createOrUpdateUser.mockReturnValue({ @@ -138,7 +112,6 @@ describe('create user', () => { role: ROLES.inactivePractitioner, userId: '745b7d39-8fae-4c2f-893c-3c829598bc71', }); - const mockAdmissionsDate = '1876-02-19'; const user = await createUserInteractor(applicationContext, { user: { @@ -146,12 +119,16 @@ describe('create user', () => { admissionsStatus: 'Inactive', birthYear: '1993', employer: 'DOJ', + entityName: 'Practitioner', firstName: 'Test', - lastName: 'IRSPractitioner', + lastName: 'InactivePractitioner', + name: 'Test Inactive Practitioner', originalBarState: 'CA', + password: 'P@ssw0rd', practitionerType: 'Attorney', role: ROLES.inactivePractitioner, - } as any, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_ELECTRONIC, + }, }); expect(user).toMatchObject({ @@ -161,111 +138,60 @@ describe('create user', () => { }); it('should create a generic user and delete the barNumber when it is defined and the user is not a pracititoner', async () => { - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.admin, - userId: 'ad5b7d39-8fae-4c2f-893c-3c829598bc71', - }); + applicationContext.getCurrentUser.mockReturnValue(adminUser); applicationContext .getPersistenceGateway() .createOrUpdateUser.mockReturnValue({ barNumber: '', name: 'Test PrivatePractitioner', - role: ROLES.judh, + role: ROLES.judge, userId: '745b7d39-8fae-4c2f-893c-3c829598bc71', }); - const userToCreate = { - admissionsDate: '2020-03-14', - admissionsStatus: 'Active', - birthYear: '1993', - employer: 'Private', - firstName: 'Test', - lastName: 'PrivatePractitioner', - originalBarState: 'CA', - practitionerType: 'Attorney', - role: ROLES.privatePractitioner, - }; - const user = await createUserInteractor(applicationContext, { - user: userToCreate, - } as any); + user: { + barNumber: 'NOT_VALID', + entityName: 'User', + name: 'Test Petitioner', + password: 'P@ssw0rd', + role: ROLES.petitioner, + userId: '9f797a9b-b596-488f-aa31-eca147b9b18d', + }, + }); expect(user).toMatchObject({ - barNumber: 'CS20001', - role: ROLES.privatePractitioner, + role: ROLES.petitioner, }); }); - it('creates a legacyJudge user and deletes bar number when it is defined', async () => { + it('should create a legacyJudge user and disable the user', async () => { const mockUser = { name: 'Test Legacy Judge', role: ROLES.legacyJudge, userId: '845b7d39-8fae-4c2f-893c-3c829598bc71', }; - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.admin, - userId: 'admin', - }); - + applicationContext.getCurrentUser.mockReturnValue(adminUser); applicationContext .getPersistenceGateway() .createOrUpdateUser.mockReturnValue(mockUser); - const userToCreate = { - barNumber: '', - name: 'Jesse Pinkman', - role: ROLES.legacyJudge, - userId: 'legacyJudge1@example.com', - }; - - const user = await createUserInteractor(applicationContext, { - user: userToCreate, - } as any); - - expect(user).not.toBeUndefined(); - expect( - applicationContext.getPersistenceGateway().createOrUpdateUser, - ).toHaveBeenCalledWith( - expect.objectContaining({ - disableCognitoUser: true, - }), - ); - }); - - it('creates a legacyJudge user and does not delete bar number when it is not defined', async () => { - const mockUser = { - name: 'Test Legacy Judge', - role: ROLES.legacyJudge, - userId: '845b7d39-8fae-4c2f-893c-3c829598bc71', - }; - applicationContext.getCurrentUser.mockReturnValue({ - name: 'Admin', - role: ROLES.admin, - userId: 'admin', + await createUserInteractor(applicationContext, { + user: { + barNumber: 'LR1234', + entityName: 'User', + name: 'Jesse Pinkman', + password: 'P@ssw0rd', + role: ROLES.legacyJudge, + userId: 'ce2bfd0f-f06c-4ff8-9ee3-1a385e3a8bef', + }, }); - applicationContext - .getPersistenceGateway() - .createOrUpdateUser.mockReturnValue(mockUser); - - const userToCreate = { - name: 'Jesse Pinkman', - role: ROLES.legacyJudge, - userId: 'legacyJudge1@example.com', - }; - - const user = await createUserInteractor(applicationContext, { - user: userToCreate, - } as any); - - expect(user).not.toBeUndefined(); expect( - applicationContext.getPersistenceGateway().createOrUpdateUser, + applicationContext.getUserGateway().disableUser, ).toHaveBeenCalledWith( + expect.anything(), expect.objectContaining({ - disableCognitoUser: true, + userId: mockUser.userId, }), ); }); diff --git a/web-api/src/business/useCases/user/createUserInteractor.ts b/web-api/src/business/useCases/user/createUserInteractor.ts index d0ea9c49c7f..c430374cce3 100644 --- a/web-api/src/business/useCases/user/createUserInteractor.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.ts @@ -17,7 +17,14 @@ import { createPractitionerUser } from '../../../../../shared/src/business/utili export const createUserInteractor = async ( applicationContext: ServerApplicationContext, - { user }: { user: RawUser & { barNumber?: string; password: string } }, + { + user, + }: { + user: (RawUser | RawPractitioner) & { + barNumber?: string; + password: string; + }; + }, ): Promise => { const requestUser = applicationContext.getCurrentUser(); diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts index 048f71ef579..c2a3840ac85 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts @@ -66,23 +66,17 @@ describe('createOrUpdatePractitionerUser', () => { }; beforeAll(() => { - applicationContext.getCognito().adminGetUser.mockReturnValue({ - promise: () => - Promise.resolve({ - Username: 'f7bea269-fa95-424d-aed8-2cb988df2073', - }), + applicationContext.getCognito().adminGetUser.mockResolvedValue({ + Username: 'f7bea269-fa95-424d-aed8-2cb988df2073', }); - applicationContext.getCognito().adminCreateUser.mockReturnValue({ - promise: () => - Promise.resolve({ - User: { Username: userId }, - }), + applicationContext.getCognito().adminCreateUser.mockResolvedValue({ + User: { Username: userId }, }); - applicationContext.getCognito().adminUpdateUserAttributes.mockReturnValue({ - promise: () => Promise.resolve(), - }); + applicationContext + .getCognito() + .adminUpdateUserAttributes.mockResolvedValue({}); applicationContext.getDocumentClient().put.mockResolvedValue(null); }); @@ -233,32 +227,6 @@ describe('createOrUpdatePractitionerUser', () => { ).not.toHaveBeenCalled(); }); - it('should call cognito adminGetUser and adminUpdateUserAttributes if adminCreateUser throws an error', async () => { - applicationContext.getCognito().adminCreateUser.mockReturnValue({ - promise: () => Promise.reject(new Error('bad!')), - }); - - applicationContext.getCognito().adminGetUser.mockReturnValue({ - promise: () => - Promise.resolve({ - Username: '562d6260-aa9b-4010-af99-536d3872c752', - }), - }); - - await createOrUpdatePractitionerUser({ - applicationContext, - user: privatePractitionerUserWithSection as any, - }); - - expect( - applicationContext.getCognito().adminCreateUser, - ).not.toHaveBeenCalled(); - expect(applicationContext.getCognito().adminGetUser).toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).toHaveBeenCalled(); - }); - it('should throw an error when attempting to create a user that is not role private, IRS practitioner or inactive practitioner', async () => { await expect( createOrUpdatePractitionerUser({ @@ -272,10 +240,7 @@ describe('createOrUpdatePractitionerUser', () => { it('should call adminCreateUser with the correct UserAttributes', async () => { applicationContext.getCognito().adminCreateUser.mockReturnValue({ - promise: () => - Promise.resolve({ - User: { Username: '123' }, - }), + User: { Username: '123' }, }); setupNonExistingUserMock(); diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts index bbb184903e6..9fa149ce64e 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts @@ -1,9 +1,11 @@ import * as client from '../../dynamodbClientService'; import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; -import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; +import { + ROLES, + Role, +} from '../../../../../shared/src/business/entities/EntityConstants'; import { RawUser } from '@shared/business/entities/User'; import { ServerApplicationContext } from '@web-api/applicationContext'; -import { isUserAlreadyCreated } from './createOrUpdateUser'; export const createUserRecords = async ({ applicationContext, @@ -63,7 +65,7 @@ export const createOrUpdatePractitionerUser = async ({ user: RawUser; }) => { let userId = applicationContext.getUniqueId(); - const practitionerRoleTypes = [ + const practitionerRoleTypes: Role[] = [ ROLES.privatePractitioner, ROLES.irsPractitioner, ROLES.inactivePractitioner, @@ -85,13 +87,13 @@ export const createOrUpdatePractitionerUser = async ({ }); } - const userExists = await isUserAlreadyCreated({ - applicationContext, - email: userEmail, - userPoolId: process.env.USER_POOL_ID as string, - }); + const existingUser = await applicationContext + .getUserGateway() + .getUserByEmail(applicationContext, { + email: userEmail, + }); - if (!userExists) { + if (!existingUser) { let params: AdminCreateUserCommandInput = { DesiredDeliveryMediums: ['EMAIL'], UserAttributes: [ @@ -139,12 +141,6 @@ export const createOrUpdatePractitionerUser = async ({ userId = userIdAttribute?.Value!; } } else { - const response = await applicationContext - .getUserGateway() - .getUserByEmail(applicationContext, { - email: userEmail, - }); - await applicationContext.getCognito().adminUpdateUserAttributes({ UserAttributes: [ { @@ -153,22 +149,12 @@ export const createOrUpdatePractitionerUser = async ({ }, ], UserPoolId: process.env.USER_POOL_ID, - // and here? Username: userEmail, }); - // and here - userId = - response.UserAttributes?.find(element => { - if (element.Name === 'custom:userId') { - return element; - } - }) || - response.UserAttributes?.find(element => { - if (element.Name === 'sub') { - return element; - } - }); + + userId = existingUser.userId; } + return await createUserRecords({ applicationContext, user, diff --git a/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts b/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts index f713a24ab48..fe2c933e9e2 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts @@ -24,7 +24,6 @@ describe('createOrUpdateUser', () => { await createOrUpdateUser({ applicationContext, - disableCognitoUser: false, password: mockTemporaryPassword, user: petitionsClerkUser, }); @@ -57,34 +56,7 @@ describe('createOrUpdateUser', () => { UserPoolId: undefined, Username: petitionsClerkUser.email, }); - expect( - applicationContext.getCognito().adminDisableUser, - ).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).not.toHaveBeenCalled(); - }); - - it('should create a user and cognito record, but disable the cognito user', async () => { - applicationContext - .getCognito() - .adminGetUser.mockRejectedValue( - new UserNotFoundException({ $metadata: {}, message: '' }), - ); - applicationContext.getCognito().adminCreateUser.mockResolvedValue({ - User: { Username: petitionsClerkUser.userId }, - }); - await createOrUpdateUser({ - applicationContext, - disableCognitoUser: true, - password: mockTemporaryPassword, - user: petitionsClerkUser, - }); - - expect(applicationContext.getCognito().adminCreateUser).toHaveBeenCalled(); - expect(applicationContext.getCognito().adminDisableUser).toHaveBeenCalled(); - expect(applicationContext.getCognito().adminGetUser).toHaveBeenCalled(); expect( applicationContext.getCognito().adminUpdateUserAttributes, ).not.toHaveBeenCalled(); @@ -97,7 +69,6 @@ describe('createOrUpdateUser', () => { await createOrUpdateUser({ applicationContext, - disableCognitoUser: false, password: mockTemporaryPassword, user: petitionsClerkUser, }); From 6fcf347643ea430934fb19de8f013818a78775fb Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 14:31:43 -0800 Subject: [PATCH 13/33] 10007: Add test coverage --- .../src/gateways/user/changePassword.test.ts | 27 ++++++++ web-api/src/gateways/user/disableUser.test.ts | 21 ++++++ .../src/gateways/user/initiateAuth.test.ts | 65 +++++++++++++++++++ .../src/gateways/user/renewIdToken.test.ts | 8 +-- 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 web-api/src/gateways/user/changePassword.test.ts create mode 100644 web-api/src/gateways/user/disableUser.test.ts create mode 100644 web-api/src/gateways/user/initiateAuth.test.ts diff --git a/web-api/src/gateways/user/changePassword.test.ts b/web-api/src/gateways/user/changePassword.test.ts new file mode 100644 index 00000000000..feaeaf93ad5 --- /dev/null +++ b/web-api/src/gateways/user/changePassword.test.ts @@ -0,0 +1,27 @@ +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { changePassword } from '@web-api/gateways/user/changePassword'; + +describe('changePassword', () => { + it('should make a call to disable the provided userId', async () => { + const mockEmail = 'test@example.com'; + const mockNewPassword = 'P@ssw0rd'; + const mockCode = 'afde08bd-7ccc-4163-9242-87f78cbb2452'; + const mockCognitoClientId = 'test'; + applicationContext.environment.cognitoClientId = mockCognitoClientId; + + await changePassword(applicationContext, { + code: mockCode, + email: mockEmail, + newPassword: mockNewPassword, + }); + + expect( + applicationContext.getCognito().confirmForgotPassword, + ).toHaveBeenCalledWith({ + ClientId: mockCognitoClientId, + ConfirmationCode: mockCode, + Password: mockNewPassword, + Username: mockEmail, + }); + }); +}); diff --git a/web-api/src/gateways/user/disableUser.test.ts b/web-api/src/gateways/user/disableUser.test.ts new file mode 100644 index 00000000000..f420e0ea1bc --- /dev/null +++ b/web-api/src/gateways/user/disableUser.test.ts @@ -0,0 +1,21 @@ +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { disableUser } from '@web-api/gateways/user/disableUser'; + +describe('disableUser', () => { + it('should make a call to disable the provided userId', async () => { + const mockUserId = 'afde08bd-7ccc-4163-9242-87f78cbb2452'; + const mockUserPoolId = 'test'; + process.env.USER_POOL_ID = mockUserPoolId; + + await disableUser(applicationContext, { + userId: mockUserId, + }); + + expect( + applicationContext.getCognito().adminDisableUser, + ).toHaveBeenCalledWith({ + UserPoolId: mockUserPoolId, + Username: mockUserId, + }); + }); +}); diff --git a/web-api/src/gateways/user/initiateAuth.test.ts b/web-api/src/gateways/user/initiateAuth.test.ts new file mode 100644 index 00000000000..5e26f668cd6 --- /dev/null +++ b/web-api/src/gateways/user/initiateAuth.test.ts @@ -0,0 +1,65 @@ +import { + ChallengeNameType, + InitiateAuthCommandOutput, +} from '@aws-sdk/client-cognito-identity-provider'; +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; + +describe('initiateAuth', () => { + it('should throw an error when initiateAuth returns a new password required challenge', async () => { + const mockOutput: InitiateAuthCommandOutput = { + $metadata: {}, + ChallengeName: ChallengeNameType.NEW_PASSWORD_REQUIRED, + }; + applicationContext + .getCognito() + .initiateAuth.mockResolvedValueOnce(mockOutput); + + await expect( + initiateAuth(applicationContext, { + email: 'test@example.com', + password: 'P@ssw0rd', + }), + ).rejects.toThrow('NewPasswordRequired'); + }); + + it('should throw an error when initiateAuth does not return any tokens', async () => { + const mockOutput: InitiateAuthCommandOutput = { + $metadata: {}, + }; + applicationContext.getCognito().initiateAuth.mockResolvedValue(mockOutput); + + await expect( + initiateAuth(applicationContext, { + email: 'test@example.com', + password: 'P@ssw0rd', + }), + ).rejects.toThrow('InitiateAuthError'); + }); + + it('should return the user`s tokens when they are successfully authenticated', async () => { + const mockAccessToken = 'c39d1dea-ca08-47ba-9935-0bfc354b68dc'; + const mockRefreshToken = 'e4216a49-aa21-4e37-93d6-23374c0ac126'; + const mockIdToken = '7120b318-153d-454e-8c24-c710ea4ff4ab'; + const mockOutput: InitiateAuthCommandOutput = { + $metadata: {}, + AuthenticationResult: { + AccessToken: mockAccessToken, + IdToken: mockIdToken, + RefreshToken: mockRefreshToken, + }, + }; + applicationContext.getCognito().initiateAuth.mockResolvedValue(mockOutput); + + const result = await initiateAuth(applicationContext, { + email: 'test@example.com', + password: 'P@ssw0rd', + }); + + expect(result).toEqual({ + accessToken: mockAccessToken, + idToken: mockIdToken, + refreshToken: mockRefreshToken, + }); + }); +}); diff --git a/web-api/src/gateways/user/renewIdToken.test.ts b/web-api/src/gateways/user/renewIdToken.test.ts index 490e42cf248..ccf9d8595f4 100644 --- a/web-api/src/gateways/user/renewIdToken.test.ts +++ b/web-api/src/gateways/user/renewIdToken.test.ts @@ -7,9 +7,7 @@ describe('renewIdToken', () => { const mockOutput: InitiateAuthCommandOutput = { $metadata: {}, }; - applicationContext - .getCognito() - .initiateAuth.mockResolvedValueOnce(mockOutput); + applicationContext.getCognito().initiateAuth.mockResolvedValue(mockOutput); await expect( renewIdToken(applicationContext, { @@ -26,9 +24,7 @@ describe('renewIdToken', () => { IdToken: mockIdToken, }, }; - applicationContext - .getCognito() - .initiateAuth.mockResolvedValueOnce(mockOutput); + applicationContext.getCognito().initiateAuth.mockResolvedValue(mockOutput); const result = await renewIdToken(applicationContext, { refreshToken: 'some_token', From 764f8ee73d2eaafcfd3979c53314a93800419dcb Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 14:47:54 -0800 Subject: [PATCH 14/33] 10007: Disable prefer destructuring rule for line that doesn't need destructuring --- .../persistence/dynamo/users/createOrUpdatePractitionerUser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts index 9fa149ce64e..96ea7a7d63b 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts @@ -152,6 +152,7 @@ export const createOrUpdatePractitionerUser = async ({ Username: userEmail, }); + // eslint-disable-next-line prefer-destructuring userId = existingUser.userId; } From 3761501084dd8894291ed0bf00e3c648cebe61fe Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:03:04 -0800 Subject: [PATCH 15/33] 10007: Disable user now accepts email like all other user gateway functions --- .../src/business/useCases/user/createUserInteractor.ts | 2 +- web-api/src/gateways/user/disableUser.test.ts | 8 ++++---- web-api/src/gateways/user/disableUser.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web-api/src/business/useCases/user/createUserInteractor.ts b/web-api/src/business/useCases/user/createUserInteractor.ts index c430374cce3..1739f3acd6c 100644 --- a/web-api/src/business/useCases/user/createUserInteractor.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.ts @@ -63,7 +63,7 @@ export const createUserInteractor = async ( if (user.role === ROLES.legacyJudge) { await applicationContext.getUserGateway().disableUser(applicationContext, { - userId, + email: user.email, }); } diff --git a/web-api/src/gateways/user/disableUser.test.ts b/web-api/src/gateways/user/disableUser.test.ts index f420e0ea1bc..48e17382bdb 100644 --- a/web-api/src/gateways/user/disableUser.test.ts +++ b/web-api/src/gateways/user/disableUser.test.ts @@ -2,20 +2,20 @@ import { applicationContext } from '../../../../shared/src/business/test/createT import { disableUser } from '@web-api/gateways/user/disableUser'; describe('disableUser', () => { - it('should make a call to disable the provided userId', async () => { - const mockUserId = 'afde08bd-7ccc-4163-9242-87f78cbb2452'; + it('should make a call to disable the user with the provided email', async () => { + const mockEmail = 'test@example.com'; const mockUserPoolId = 'test'; process.env.USER_POOL_ID = mockUserPoolId; await disableUser(applicationContext, { - userId: mockUserId, + email: mockEmail, }); expect( applicationContext.getCognito().adminDisableUser, ).toHaveBeenCalledWith({ UserPoolId: mockUserPoolId, - Username: mockUserId, + Username: mockEmail, }); }); }); diff --git a/web-api/src/gateways/user/disableUser.ts b/web-api/src/gateways/user/disableUser.ts index 5c0a52efaf4..705c6ce238a 100644 --- a/web-api/src/gateways/user/disableUser.ts +++ b/web-api/src/gateways/user/disableUser.ts @@ -2,10 +2,10 @@ import { ServerApplicationContext } from '@web-api/applicationContext'; export async function disableUser( applicationContext: ServerApplicationContext, - { userId }: { userId: string }, + { email }: { email: string }, ): Promise { await applicationContext.getCognito().adminDisableUser({ UserPoolId: process.env.USER_POOL_ID, - Username: userId, + Username: email, }); } From 10eabb5860c138077314ad8a01e3b62fadf3968b Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:03:43 -0800 Subject: [PATCH 16/33] 10007: Extract forgot password cognito call to user gateway --- .../auth/forgotPasswordInteractor.test.ts | 78 ++++++++----------- .../useCases/auth/forgotPasswordInteractor.ts | 5 +- .../src/gateways/user/forgotPassword.test.ts | 21 +++++ web-api/src/gateways/user/forgotPassword.ts | 11 +++ web-api/src/getUserGateway.ts | 2 + 5 files changed, 67 insertions(+), 50 deletions(-) create mode 100644 web-api/src/gateways/user/forgotPassword.test.ts create mode 100644 web-api/src/gateways/user/forgotPassword.ts diff --git a/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts b/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts index df06bd99d17..566e68d13aa 100644 --- a/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts @@ -20,7 +20,9 @@ describe('forgotPasswordInteractor', () => { userId, }; - beforeAll(() => { + beforeEach(() => { + process.env.STAGE = 'local'; + applicationContext .getUserGateway() .getUserByEmail.mockResolvedValue(mockUser); @@ -32,28 +34,22 @@ describe('forgotPasswordInteractor', () => { }); }); - beforeEach(() => { - process.env.DEFAULT_ACCOUNT_PASS = 'password'; - process.env.STAGE = 'local'; - }); - afterEach(() => { - applicationContext.environment.stage = 'local'; process.env = OLD_ENV; }); it('should return early when user account does not exist', async () => { applicationContext .getUserGateway() - .getUserByEmail.mockResolvedValueOnce(undefined); + .getUserByEmail.mockResolvedValue(undefined); - const result = await forgotPasswordInteractor(applicationContext, { + await forgotPasswordInteractor(applicationContext, { email, }); expect( - applicationContext.getUserGateway().getUserByEmail.mock.calls[0][1], - ).toEqual({ email }); + applicationContext.getUserGateway().getUserByEmail, + ).toHaveBeenCalledWith(applicationContext, { email }); expect( applicationContext.getUseCaseHelpers().createUserConfirmation, ).not.toHaveBeenCalled(); @@ -63,11 +59,9 @@ describe('forgotPasswordInteractor', () => { expect( applicationContext.getMessageGateway().sendEmailToUser, ).not.toHaveBeenCalled(); - - expect(result).toBeUndefined(); }); - it('should throw an UnauthorizedError and call createUserConfirmation when user account is unconfirmed', async () => { + it('should throw an UnauthorizedError and resend an account confirmation email when the user`s account is unconfirmed', async () => { applicationContext.getUserGateway().getUserByEmail.mockResolvedValueOnce({ ...mockUser, accountStatus: UserStatusType.UNCONFIRMED, @@ -80,16 +74,17 @@ describe('forgotPasswordInteractor', () => { ).rejects.toThrow(new UnauthorizedError('User is unconfirmed')); expect( - applicationContext.getUserGateway().getUserByEmail.mock.calls[0][1], - ).toEqual({ email }); - + applicationContext.getUserGateway().getUserByEmail, + ).toHaveBeenCalledWith(applicationContext, { email }); expect( - applicationContext.getUseCaseHelpers().createUserConfirmation.mock - .calls[0][1], - ).toEqual({ email, userId: mockUser.userId }); + applicationContext.getUseCaseHelpers().createUserConfirmation, + ).toHaveBeenCalledWith(applicationContext, { + email, + userId: mockUser.userId, + }); }); - it('should throw an UnauthorizedError and call adminCreateUser (without TemporaryPassword on prod) when user account is in FORCE_CHANGE_PASSWORD status', async () => { + it('should throw an UnauthorizedError and resend a password change email when user`s account is in a force change password state', async () => { process.env.STAGE = 'prod'; applicationContext.getUserGateway().getUserByEmail.mockResolvedValueOnce({ @@ -104,12 +99,13 @@ describe('forgotPasswordInteractor', () => { ).rejects.toThrow(new UnauthorizedError('User is unconfirmed')); expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0] - .TemporaryPassword, - ).toBeUndefined(); + applicationContext.getCognito().adminCreateUser, + ).not.toHaveBeenCalledWith({ + TemporaryPassword: undefined, + }); }); - it('should throw an UnauthorizedError and call adminCreateUser (with TemporaryPassword on non-prod environments) when user account is in FORCE_CHANGE_PASSWORD status', async () => { + it('should throw an UnauthorizedErrorresend a password change email when user`s account is in a force change password state (with TemporaryPassword on non-prod environments)', async () => { applicationContext.getUserGateway().getUserByEmail.mockResolvedValueOnce({ ...mockUser, accountStatus: UserStatusType.FORCE_CHANGE_PASSWORD, @@ -122,14 +118,11 @@ describe('forgotPasswordInteractor', () => { ).rejects.toThrow(new UnauthorizedError('User is unconfirmed')); expect( - applicationContext.getUserGateway().getUserByEmail.mock.calls[0][1], - ).toEqual({ email }); + applicationContext.getUserGateway().getUserByEmail, + ).toHaveBeenCalledWith(applicationContext, { email }); expect( - applicationContext.getUseCaseHelpers().createUserConfirmation, - ).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0], - ).toEqual({ + applicationContext.getCognito().adminCreateUser, + ).toHaveBeenCalledWith({ DesiredDeliveryMediums: ['EMAIL'], MessageAction: 'RESEND', TemporaryPassword: process.env.DEFAULT_ACCOUNT_PASS, @@ -139,26 +132,17 @@ describe('forgotPasswordInteractor', () => { }); it('should call forgotPassword when user account has a valid status', async () => { - const result = await forgotPasswordInteractor(applicationContext, { + await forgotPasswordInteractor(applicationContext, { email, }); expect( - applicationContext.getUserGateway().getUserByEmail.mock.calls[0][1], - ).toEqual({ email }); - expect( - applicationContext.getUseCaseHelpers().createUserConfirmation, - ).not.toHaveBeenCalled(); + applicationContext.getUserGateway().getUserByEmail, + ).toHaveBeenCalledWith(applicationContext, { email }); expect( - applicationContext.getCognito().adminCreateUser, - ).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().forgotPassword.mock.calls[0][0], - ).toEqual({ - ClientId: applicationContext.environment.cognitoClientId, - Username: email, + applicationContext.getUserGateway().forgotPassword, + ).toHaveBeenCalledWith(applicationContext, { + email, }); - - expect(result).toBeUndefined(); }); }); diff --git a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts index 7d7c27fff6f..795f296014f 100644 --- a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts @@ -35,8 +35,7 @@ export const forgotPasswordInteractor = async ( throw new UnauthorizedError('User is unconfirmed'); //403 } - await applicationContext.getCognito().forgotPassword({ - ClientId: applicationContext.environment.cognitoClientId, - Username: email, + await applicationContext.getUserGateway().forgotPassword(applicationContext, { + email, }); }; diff --git a/web-api/src/gateways/user/forgotPassword.test.ts b/web-api/src/gateways/user/forgotPassword.test.ts new file mode 100644 index 00000000000..630abce61c2 --- /dev/null +++ b/web-api/src/gateways/user/forgotPassword.test.ts @@ -0,0 +1,21 @@ +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; + +describe('forgotPassword', () => { + it('should make a call to indicate the user with the provided email forgot their password', async () => { + const mockEmail = 'test@example.com'; + const mockCognitoClientId = 'test'; + applicationContext.environment.cognitoClientId = mockCognitoClientId; + + await forgotPassword(applicationContext, { + email: mockEmail, + }); + + expect(applicationContext.getCognito().forgotPassword).toHaveBeenCalledWith( + { + ClientId: mockCognitoClientId, + Username: mockEmail, + }, + ); + }); +}); diff --git a/web-api/src/gateways/user/forgotPassword.ts b/web-api/src/gateways/user/forgotPassword.ts new file mode 100644 index 00000000000..8087cf60f7e --- /dev/null +++ b/web-api/src/gateways/user/forgotPassword.ts @@ -0,0 +1,11 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function forgotPassword( + applicationContext: ServerApplicationContext, + { email }: { email: string }, +): Promise { + await applicationContext.getCognito().forgotPassword({ + ClientId: applicationContext.environment.cognitoClientId, + Username: email, + }); +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index d2fbfbe22fa..3beebcdd211 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,5 +1,6 @@ import { changePassword } from '@web-api/gateways/user/changePassword'; import { disableUser } from '@web-api/gateways/user/disableUser'; +import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; @@ -7,6 +8,7 @@ import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; export const getUserGateway = () => ({ changePassword, disableUser, + forgotPassword, getUserByEmail, initiateAuth, renewIdToken, From f40ee46e7145423844aef03e7cd133fe57bd427f Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:11:27 -0800 Subject: [PATCH 17/33] 10007: Fix test after updating disableUser args --- .../src/business/useCases/user/createUserInteractor.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-api/src/business/useCases/user/createUserInteractor.test.ts b/web-api/src/business/useCases/user/createUserInteractor.test.ts index a2744639b4b..66b8797cc84 100644 --- a/web-api/src/business/useCases/user/createUserInteractor.test.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.test.ts @@ -166,6 +166,7 @@ describe('createUserInteractor', () => { it('should create a legacyJudge user and disable the user', async () => { const mockUser = { + email: 'test@example.com', name: 'Test Legacy Judge', role: ROLES.legacyJudge, userId: '845b7d39-8fae-4c2f-893c-3c829598bc71', @@ -178,6 +179,7 @@ describe('createUserInteractor', () => { await createUserInteractor(applicationContext, { user: { barNumber: 'LR1234', + email: 'test@example.com', entityName: 'User', name: 'Jesse Pinkman', password: 'P@ssw0rd', @@ -191,7 +193,7 @@ describe('createUserInteractor', () => { ).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - userId: mockUser.userId, + email: mockUser.email, }), ); }); From eb7aad0bf18294aa985bdb63353a232cdaa5f9c6 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:36:57 -0800 Subject: [PATCH 18/33] 10007: Extract adminUpdateUserAttributes to an updateUser function in user gateway --- .../verifyUserPendingEmailInteractor.test.ts | 30 +++--- .../user/verifyUserPendingEmailInteractor.ts | 18 +--- web-api/src/gateways/user/updateUser.test.ts | 96 +++++++++++++++++++ web-api/src/gateways/user/updateUser.ts | 42 ++++++++ web-api/src/getUserGateway.ts | 2 + 5 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 web-api/src/gateways/user/updateUser.test.ts create mode 100644 web-api/src/gateways/user/updateUser.ts diff --git a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts index 8cc3a703b8b..b8ac1515e6f 100644 --- a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts +++ b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.test.ts @@ -100,7 +100,7 @@ describe('verifyUserPendingEmailInteractor', () => { await expect( verifyUserPendingEmailInteractor(applicationContext, { - token: undefined, + token: undefined as any, }), ).rejects.toThrow('Tokens do not match'); }); @@ -118,23 +118,19 @@ describe('verifyUserPendingEmailInteractor', () => { }); it('should update the cognito email when tokens match', async () => { - await expect( - verifyUserPendingEmailInteractor(applicationContext, { - token: TOKEN, - }), - ).resolves.not.toThrow('Tokens do not match'); - - const adminUpdateUserAttributesResult = - applicationContext.getCognito().adminUpdateUserAttributes.mock - .calls[0][0]; - - expect(adminUpdateUserAttributesResult.UserAttributes[0]).toMatchObject({ - Name: 'email', - Value: 'other@example.com', - }); - expect(adminUpdateUserAttributesResult).toMatchObject({ - Username: 'test@example.com', + await verifyUserPendingEmailInteractor(applicationContext, { + token: TOKEN, }); + + expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalledWith( + applicationContext, + { + attributesToUpdate: { + email: 'other@example.com', + }, + email: 'test@example.com', + }, + ); }); it('should update the dynamo record with the new info', async () => { diff --git a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts index 9e02c33a880..b22c6b41dd5 100644 --- a/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts +++ b/web-api/src/business/useCases/user/verifyUserPendingEmailInteractor.ts @@ -50,19 +50,11 @@ export const verifyUserPendingEmailInteractor = async ( }, ); - await applicationContext.getCognito().adminUpdateUserAttributes({ - UserAttributes: [ - { - Name: 'email', - Value: updatedUser.email, - }, - { - Name: 'email_verified', - Value: 'true', - }, - ], - UserPoolId: process.env.USER_POOL_ID, - Username: user.email, + await applicationContext.getUserGateway().updateUser(applicationContext, { + attributesToUpdate: { + email: updatedUser.email, + }, + email: user.email, }); await applicationContext.getWorkerGateway().queueWork(applicationContext, { diff --git a/web-api/src/gateways/user/updateUser.test.ts b/web-api/src/gateways/user/updateUser.test.ts new file mode 100644 index 00000000000..c97bb468be6 --- /dev/null +++ b/web-api/src/gateways/user/updateUser.test.ts @@ -0,0 +1,96 @@ +import { ROLES } from '@shared/business/entities/EntityConstants'; +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { updateUser } from '@web-api/gateways/user/updateUser'; + +describe('updateUser', () => { + it('should update the user`s email in persistence when it is provided as an attribute to update', async () => { + const mockEmail = 'test@example.com'; + const mockUserPoolId = 'test'; + process.env.USER_POOL_ID = mockUserPoolId; + + await updateUser(applicationContext, { + attributesToUpdate: { + email: mockEmail, + }, + email: mockEmail, + }); + + expect( + applicationContext.getCognito().adminUpdateUserAttributes, + ).toHaveBeenCalledWith({ + UserAttributes: [ + { + Name: 'email', + Value: mockEmail, + }, + { + Name: 'email_verified', + Value: 'true', + }, + ], + UserPoolId: process.env.USER_POOL_ID, + Username: mockEmail, + }); + }); + + it('should update the user`s role in persistence when it is provided as an attribute to update', async () => { + const mockEmail = 'test@example.com'; + const mockUserPoolId = 'test'; + process.env.USER_POOL_ID = mockUserPoolId; + + await updateUser(applicationContext, { + attributesToUpdate: { + role: ROLES.petitioner, + }, + email: mockEmail, + }); + + expect( + applicationContext.getCognito().adminUpdateUserAttributes, + ).toHaveBeenCalledWith({ + UserAttributes: [ + { + Name: 'custom:role', + Value: ROLES.petitioner, + }, + ], + UserPoolId: process.env.USER_POOL_ID, + Username: mockEmail, + }); + }); + + it('should update the user`s role and email in persistence when they are both provided as attributes to update', async () => { + const mockEmail = 'test@example.com'; + const mockUserPoolId = 'test'; + process.env.USER_POOL_ID = mockUserPoolId; + + await updateUser(applicationContext, { + attributesToUpdate: { + email: mockEmail, + role: ROLES.petitioner, + }, + email: mockEmail, + }); + + expect( + applicationContext.getCognito().adminUpdateUserAttributes, + ).toHaveBeenCalledWith({ + UserAttributes: [ + { + Name: 'custom:role', + Value: ROLES.petitioner, + }, + { + Name: 'email', + Value: mockEmail, + }, + { + Name: 'email_verified', + Value: 'true', + }, + ], + UserPoolId: process.env.USER_POOL_ID, + Username: mockEmail, + }); + }); +}); diff --git a/web-api/src/gateways/user/updateUser.ts b/web-api/src/gateways/user/updateUser.ts new file mode 100644 index 00000000000..7dc34c06cdd --- /dev/null +++ b/web-api/src/gateways/user/updateUser.ts @@ -0,0 +1,42 @@ +import { AttributeType } from '@aws-sdk/client-cognito-identity-provider'; +import { Role } from '@shared/business/entities/EntityConstants'; +import { ServerApplicationContext } from '@web-api/applicationContext'; + +interface UserAttributes { + role?: Role; + email?: string; +} + +export async function updateUser( + applicationContext: ServerApplicationContext, + { + attributesToUpdate, + email, + }: { email: string; attributesToUpdate: UserAttributes }, +): Promise { + const formattedAttributesToUpdate: AttributeType[] = []; + + if (attributesToUpdate.role) { + formattedAttributesToUpdate.push({ + Name: 'custom:role', + Value: attributesToUpdate.role, + }); + } + + if (attributesToUpdate.email) { + formattedAttributesToUpdate.push({ + Name: 'email', + Value: attributesToUpdate.email, + }); + formattedAttributesToUpdate.push({ + Name: 'email_verified', + Value: 'true', + }); + } + + await applicationContext.getCognito().adminUpdateUserAttributes({ + UserAttributes: formattedAttributesToUpdate, + UserPoolId: process.env.USER_POOL_ID, + Username: email, + }); +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index 3beebcdd211..d78ac77d6ec 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -4,6 +4,7 @@ import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; +import { updateUser } from '@web-api/gateways/user/updateUser'; export const getUserGateway = () => ({ changePassword, @@ -12,4 +13,5 @@ export const getUserGateway = () => ({ getUserByEmail, initiateAuth, renewIdToken, + updateUser, }); From 98d6b2dc7887d4e7cf373303d3bf60bdb489a2e5 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:44:40 -0800 Subject: [PATCH 19/33] 10007: Refactor to use updateUser function --- .../users/createOrUpdatePractitionerUser.test.ts | 14 +++++--------- .../dynamo/users/createOrUpdatePractitionerUser.ts | 14 +++++--------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts index c2a3840ac85..044aabbfbf9 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.test.ts @@ -74,10 +74,6 @@ describe('createOrUpdatePractitionerUser', () => { User: { Username: userId }, }); - applicationContext - .getCognito() - .adminUpdateUserAttributes.mockResolvedValue({}); - applicationContext.getDocumentClient().put.mockResolvedValue(null); }); @@ -142,7 +138,7 @@ describe('createOrUpdatePractitionerUser', () => { ).not.toHaveBeenCalled(); expect(applicationContext.getCognito().adminGetUser).not.toHaveBeenCalled(); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); @@ -158,7 +154,7 @@ describe('createOrUpdatePractitionerUser', () => { applicationContext.getCognito().adminCreateUser.mock.calls[0][0].Username, ).toBe(privatePractitionerUserWithSection.email); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); @@ -190,7 +186,7 @@ describe('createOrUpdatePractitionerUser', () => { expect(applicationContext.getCognito().adminCreateUser).toHaveBeenCalled(); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); @@ -204,7 +200,7 @@ describe('createOrUpdatePractitionerUser', () => { expect(applicationContext.getCognito().adminCreateUser).toHaveBeenCalled(); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); @@ -223,7 +219,7 @@ describe('createOrUpdatePractitionerUser', () => { expect(applicationContext.getCognito().adminCreateUser).toHaveBeenCalled(); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); diff --git a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts index 96ea7a7d63b..997336b5cf7 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdatePractitionerUser.ts @@ -141,15 +141,11 @@ export const createOrUpdatePractitionerUser = async ({ userId = userIdAttribute?.Value!; } } else { - await applicationContext.getCognito().adminUpdateUserAttributes({ - UserAttributes: [ - { - Name: 'custom:role', - Value: user.role, - }, - ], - UserPoolId: process.env.USER_POOL_ID, - Username: userEmail, + await applicationContext.getUserGateway().updateUser(applicationContext, { + attributesToUpdate: { + role: user.role, + }, + email: userEmail, }); // eslint-disable-next-line prefer-destructuring From 4e1c61aa33cf6956e2469b6e8db3c1a931b1f954 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:47:58 -0800 Subject: [PATCH 20/33] 10007: Refactor to use updateUser function --- .../dynamo/users/createOrUpdateUser.test.ts | 6 ++---- .../dynamo/users/createOrUpdateUser.ts | 20 ++++++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts b/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts index fe2c933e9e2..0ae302d2baf 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdateUser.test.ts @@ -58,7 +58,7 @@ describe('createOrUpdateUser', () => { }); expect( - applicationContext.getCognito().adminUpdateUserAttributes, + applicationContext.getUserGateway().updateUser, ).not.toHaveBeenCalled(); }); @@ -77,9 +77,7 @@ describe('createOrUpdateUser', () => { expect( applicationContext.getCognito().adminCreateUser, ).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).toHaveBeenCalled(); + expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalled(); }); describe('createUserRecords', () => { diff --git a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts index 38eb8437197..ce1be563296 100644 --- a/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts +++ b/web-api/src/persistence/dynamo/users/createOrUpdateUser.ts @@ -181,25 +181,21 @@ export const createOrUpdateUser = async ({ UserPoolId: userPoolId, Username: user.email, }); - // replace sub here + userId = response.User!.Username; } else { const response = await applicationContext.getCognito().adminGetUser({ UserPoolId: userPoolId, Username: user.email, }); - await applicationContext.getCognito().adminUpdateUserAttributes({ - UserAttributes: [ - { - Name: 'custom:role', - Value: user.role, - }, - ], - UserPoolId: userPoolId, - // and here - Username: response.Username, + + await applicationContext.getUserGateway().updateUser(applicationContext, { + attributesToUpdate: { + role: user.role, + }, + email: user.email, }); - //and here + userId = response.Username; } From e469f635faf519b4347dfb55d7bbf6d77e4f7937 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Fri, 8 Mar 2024 15:51:52 -0800 Subject: [PATCH 21/33] 10007: Refactor to use updateUser function --- .../users/updatePractitionerUser.test.ts | 43 ++++++++++--------- .../dynamo/users/updatePractitionerUser.ts | 17 +++----- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts b/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts index 7a87771a2c2..b31701014a1 100644 --- a/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts @@ -29,17 +29,15 @@ describe('updatePractitionerUser', () => { }; beforeEach(() => { - applicationContext.getCognito().adminUpdateUserAttributes.mockReturnValue({ - promise: () => null, - }); + // applicationContext.getCgognito().adminUpdateUserAttributes.mockReturnValue({ + // promise: () => null, + // }); }); it("should log an error when an error occurs while updating the user's cognito attributes", async () => { applicationContext - .getCognito() - .adminUpdateUserAttributes.mockReturnValue( - Promise.reject(new Error('User not found')), - ); + .getUserGateway() + .updateUser.mockRejectedValue(new Error('User not found')); await expect( updatePractitionerUser({ @@ -47,6 +45,7 @@ describe('updatePractitionerUser', () => { user: updatedUser as any, }), ).rejects.toThrow('User not found'); + expect(applicationContext.logger.error).toHaveBeenCalled(); }); @@ -76,17 +75,19 @@ describe('updatePractitionerUser', () => { }); expect(applicationContext.logger.error).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).toHaveBeenCalledWith( - expect.objectContaining({ - Username: updatedEmail, - }), + expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalledWith( + applicationContext, + { + attributesToUpdate: { + role: updatedUser.role, + }, + email: updatedEmail, + }, ); }); it("should update an existing practitioner user's Cognito attributes using the users pending email", async () => { - updatedUser.email = undefined; + updatedUser.email = undefined as any; updatedUser.pendingEmail = pendingEmail; updateUserRecordsMock.mockImplementation(() => updatedUser); @@ -96,12 +97,14 @@ describe('updatePractitionerUser', () => { }); expect(applicationContext.logger.error).not.toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).toHaveBeenCalledWith( - expect.objectContaining({ - Username: pendingEmail, - }), + expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalledWith( + applicationContext, + { + attributesToUpdate: { + role: updatedUser.role, + }, + email: pendingEmail, + }, ); }); }); diff --git a/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts b/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts index a15f57ff8f6..a6a972cb173 100644 --- a/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/updatePractitionerUser.ts @@ -1,4 +1,5 @@ import { RawUser } from '@shared/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; import { getUserById } from './getUserById'; import { updateUserRecords } from './updateUserRecords'; @@ -6,7 +7,7 @@ export const updatePractitionerUser = async ({ applicationContext, user, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; user: RawUser; }) => { const { userId } = user; @@ -17,15 +18,11 @@ export const updatePractitionerUser = async ({ }); try { - await applicationContext.getCognito().adminUpdateUserAttributes({ - UserAttributes: [ - { - Name: 'custom:role', - Value: user.role, - }, - ], - UserPoolId: process.env.USER_POOL_ID, - Username: user.email ? user.email : user.pendingEmail, + await applicationContext.getUserGateway().updateUser(applicationContext, { + attributesToUpdate: { + role: user.role, + }, + email: user.email ? user.email : user.pendingEmail, }); } catch (error) { applicationContext.logger.error(error); From 391b9c6afc1dae9b7cefd648ee3d9b5348890419 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 07:38:18 -0700 Subject: [PATCH 22/33] 10007: Fixing tests --- .../persistence/dynamo/users/updatePractitionerUser.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts b/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts index b31701014a1..ec7f4af3e5d 100644 --- a/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/updatePractitionerUser.test.ts @@ -29,9 +29,7 @@ describe('updatePractitionerUser', () => { }; beforeEach(() => { - // applicationContext.getCgognito().adminUpdateUserAttributes.mockReturnValue({ - // promise: () => null, - // }); + applicationContext.getUserGateway().updateUser.mockResolvedValue(); }); it("should log an error when an error occurs while updating the user's cognito attributes", async () => { From f19a2e1e0c861f2d102835e831ebcedd03aff831 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 08:09:09 -0700 Subject: [PATCH 23/33] 10007: Extract adminCreateUser to createUser function in user gateway --- web-api/src/gateways/user/createUser.test.ts | 140 ++++++++++++++++++ web-api/src/gateways/user/createUser.ts | 79 ++++++++++ web-api/src/getUserGateway.ts | 2 + .../users/createNewPetitionerUser.test.ts | 92 +++--------- .../dynamo/users/createNewPetitionerUser.ts | 57 +++---- 5 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 web-api/src/gateways/user/createUser.test.ts create mode 100644 web-api/src/gateways/user/createUser.ts diff --git a/web-api/src/gateways/user/createUser.test.ts b/web-api/src/gateways/user/createUser.test.ts new file mode 100644 index 00000000000..61250ef04f1 --- /dev/null +++ b/web-api/src/gateways/user/createUser.test.ts @@ -0,0 +1,140 @@ +import { + DeliveryMediumType, + MessageActionType, +} from '@aws-sdk/client-cognito-identity-provider'; +import { ROLES } from '@shared/business/entities/EntityConstants'; +import { applicationContext } from '@shared/business/test/createTestApplicationContext'; +import { createUser } from '@web-api/gateways/user/createUser'; + +describe('createUser', () => { + const mockUser = { + email: 'petitioner@example.com', + entityName: 'User', + name: 'Bob Ross', + role: ROLES.petitioner, + section: 'petitioner', + userId: '2f92447e-3a0b-4cfe-95cb-810aef270c03', + }; + + it('should make a call to persistence to create a user with the provided attributes', async () => { + process.env.STAGE = 'prod'; + + await createUser(applicationContext, { + attributesToUpdate: { + email: mockUser.email, + name: mockUser.name, + role: mockUser.role, + userId: mockUser.userId, + }, + email: mockUser.email, + resendInvitationEmail: false, + }); + + expect( + applicationContext.getCognito().adminCreateUser, + ).toHaveBeenCalledWith({ + DesiredDeliveryMediums: [DeliveryMediumType.EMAIL], + MessageAction: undefined, + UserAttributes: [ + { + Name: 'custom:role', + Value: mockUser.role, + }, + { + Name: 'name', + Value: mockUser.name, + }, + { + Name: 'custom:userId', + Value: mockUser.userId, + }, + { + Name: 'email', + Value: mockUser.email, + }, + { + Name: 'email_verified', + Value: 'true', + }, + ], + UserPoolId: applicationContext.environment.userPoolId, + Username: mockUser.email, + }); + }); + + it('should resend an invitation email to the user when resendInvitationEmail is true', async () => { + process.env.STAGE = 'prod'; + + await createUser(applicationContext, { + attributesToUpdate: { + email: mockUser.email, + name: mockUser.name, + role: mockUser.role, + userId: mockUser.userId, + }, + email: mockUser.email, + resendInvitationEmail: true, + }); + + expect( + applicationContext.getCognito().adminCreateUser, + ).toHaveBeenCalledWith({ + DesiredDeliveryMediums: [DeliveryMediumType.EMAIL], + MessageAction: MessageActionType.RESEND, + UserAttributes: [ + { + Name: 'custom:role', + Value: mockUser.role, + }, + { + Name: 'name', + Value: mockUser.name, + }, + { + Name: 'custom:userId', + Value: mockUser.userId, + }, + { + Name: 'email', + Value: mockUser.email, + }, + { + Name: 'email_verified', + Value: 'true', + }, + ], + UserPoolId: applicationContext.environment.userPoolId, + Username: mockUser.email, + }); + }); + + it('should NOT set the user`s temporary password when environment is prod', async () => { + process.env.STAGE = 'prod'; + + await createUser(applicationContext, { + attributesToUpdate: {}, + email: mockUser.email, + resendInvitationEmail: false, + }); + + expect( + applicationContext.getCognito().adminCreateUser.mock.calls[0][0] + .TemporaryPassword, + ).toBe(undefined); + }); + + it('should set the user`s temporary password when the environment is NOT prod', async () => { + process.env.STAGE = 'test'; + + await createUser(applicationContext, { + attributesToUpdate: {}, + email: mockUser.email, + resendInvitationEmail: false, + }); + + expect( + applicationContext.getCognito().adminCreateUser.mock.calls[0][0] + .TemporaryPassword, + ).toBe(process.env.DEFAULT_ACCOUNT_PASS); + }); +}); diff --git a/web-api/src/gateways/user/createUser.ts b/web-api/src/gateways/user/createUser.ts new file mode 100644 index 00000000000..63cd99f8576 --- /dev/null +++ b/web-api/src/gateways/user/createUser.ts @@ -0,0 +1,79 @@ +import { + AdminCreateUserCommandInput, + AttributeType, + DeliveryMediumType, + MessageActionType, +} from '@aws-sdk/client-cognito-identity-provider'; +import { Role } from '@shared/business/entities/EntityConstants'; +import { ServerApplicationContext } from '@web-api/applicationContext'; + +interface UserAttributes { + role?: Role; + email?: string; + name?: string; + userId?: string; +} + +export async function createUser( + applicationContext: ServerApplicationContext, + { + attributesToUpdate, + email, + resendInvitationEmail, + }: { + email: string; + attributesToUpdate: UserAttributes; + resendInvitationEmail: boolean; + }, +): Promise { + const formattedAttributesToUpdate: AttributeType[] = []; + if (attributesToUpdate.role) { + formattedAttributesToUpdate.push({ + Name: 'custom:role', + Value: attributesToUpdate.role, + }); + } + + if (attributesToUpdate.name) { + formattedAttributesToUpdate.push({ + Name: 'name', + Value: attributesToUpdate.name, + }); + } + + if (attributesToUpdate.userId) { + formattedAttributesToUpdate.push({ + Name: 'custom:userId', + Value: attributesToUpdate.userId, + }); + } + + if (attributesToUpdate.email) { + formattedAttributesToUpdate.push({ + Name: 'email', + Value: attributesToUpdate.email, + }); + formattedAttributesToUpdate.push({ + Name: 'email_verified', + Value: 'true', + }); + } + + const messageAction = resendInvitationEmail + ? MessageActionType.RESEND + : undefined; + + const createUserArgs: AdminCreateUserCommandInput = { + DesiredDeliveryMediums: [DeliveryMediumType.EMAIL], + MessageAction: messageAction, + UserAttributes: formattedAttributesToUpdate, + UserPoolId: applicationContext.environment.userPoolId, + Username: email, + }; + + if (process.env.STAGE !== 'prod') { + createUserArgs.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; + } + + await applicationContext.getCognito().adminCreateUser(createUserArgs); +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index d78ac77d6ec..ed529d0091a 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,4 +1,5 @@ import { changePassword } from '@web-api/gateways/user/changePassword'; +import { createUser } from '@web-api/gateways/user/createUser'; import { disableUser } from '@web-api/gateways/user/disableUser'; import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; @@ -8,6 +9,7 @@ import { updateUser } from '@web-api/gateways/user/updateUser'; export const getUserGateway = () => ({ changePassword, + createUser, disableUser, forgotPassword, getUserByEmail, diff --git a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.test.ts b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.test.ts index 9036072b206..3f13522cade 100644 --- a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.test.ts @@ -2,96 +2,42 @@ import { ROLES } from '../../../../../shared/src/business/entities/EntityConstan import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { createNewPetitionerUser } from './createNewPetitionerUser'; -const originalEnvironment = process.env; - describe('createNewPetitionerUser', () => { - const mockEmail = 'petitioner@example.com'; - const mockName = 'Bob Ross'; - const mockUserId = 'e6df170d-bc7d-428b-b0f2-decb3f9b83a8'; const mockUser = { + entityName: 'User', name: 'Bob Ross', pendingEmail: 'petitioner@example.com', role: ROLES.petitioner, section: 'petitioner', - userId: mockUserId, + userId: 'e6df170d-bc7d-428b-b0f2-decb3f9b83a8', }; - afterEach(() => { - process.env = originalEnvironment; - }); - - it('should call adminCreateUser with the user email, name, and userId', async () => { + it('should make a call to create the specified user in persistence', async () => { await createNewPetitionerUser({ applicationContext, - user: { - name: mockName, - pendingEmail: mockEmail, - role: ROLES.petitioner, - section: 'petitioner', - userId: mockUserId, - } as any, + user: mockUser, }); - expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0], - ).toMatchObject({ - UserAttributes: expect.arrayContaining([ - { - Name: 'email', - Value: mockEmail, - }, - { - Name: 'name', - Value: mockName, - }, - { - Name: 'custom:userId', - Value: mockUserId, - }, - ]), - Username: mockEmail, - }); - }); - - it('should call client.put with the petitioner user record', async () => { - await createNewPetitionerUser({ + expect(applicationContext.getUserGateway().createUser).toHaveBeenCalledWith( applicationContext, - user: mockUser as any, - }); - + { + attributesToUpdate: { + email: mockUser.pendingEmail, + name: mockUser.name, + role: mockUser.role, + userId: mockUser.userId, + }, + email: mockUser.pendingEmail, + resendInvitationEmail: false, + }, + ); expect( applicationContext.getDocumentClient().put.mock.calls[0][0].Item, ).toMatchObject({ ...mockUser, - pk: `user|${mockUserId}`, - sk: `user|${mockUserId}`, - userId: mockUserId, + pk: `user|${mockUser.userId}`, + sk: `user|${mockUser.userId}`, + userId: mockUser.userId, }); }); - - it('should NOT add TemporaryPassword attribute if environment is prod', async () => { - process.env.STAGE = 'prod'; - await createNewPetitionerUser({ - applicationContext, - user: mockUser as any, - }); - - expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0] - .TemporaryPassword, - ).toBe(undefined); - }); - - it('should add TemporaryPassword attribute if environment is not prod', async () => { - process.env.STAGE = 'not prod'; - await createNewPetitionerUser({ - applicationContext, - user: mockUser as any, - }); - - expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0] - .TemporaryPassword, - ).toBe(process.env.DEFAULT_ACCOUNT_PASS); - }); }); diff --git a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts index 2b492b4735c..4d6d08c8ef1 100644 --- a/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createNewPetitionerUser.ts @@ -1,7 +1,7 @@ import * as client from '../../dynamodbClientService'; -import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; import { RawUser } from '@shared/business/entities/User'; +import { ServerApplicationContext } from '@web-api/applicationContext'; export const createUserRecords = async ({ applicationContext, @@ -32,50 +32,27 @@ export const createNewPetitionerUser = async ({ applicationContext, user, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; user: RawUser; -}) => { - const { userId } = user; - - const input: AdminCreateUserCommandInput = { - DesiredDeliveryMediums: ['EMAIL'], - UserAttributes: [ - { - Name: 'email_verified', - Value: 'True', - }, - { - Name: 'email', - Value: user.pendingEmail, - }, - { - Name: 'custom:role', - Value: ROLES.petitioner, - }, - { - Name: 'name', - Value: user.name, +}): Promise => { + const createUserPromise = applicationContext + .getUserGateway() + .createUser(applicationContext, { + attributesToUpdate: { + email: user.pendingEmail, + name: user.name, + role: ROLES.petitioner, + userId: user.userId, }, - { - Name: 'custom:userId', - Value: user.userId, - }, - ], - UserPoolId: process.env.USER_POOL_ID!, - Username: user.pendingEmail!, - }; - - if (process.env.STAGE !== 'prod') { - input.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; - } + email: user.pendingEmail!, + resendInvitationEmail: false, + }); - await applicationContext.getCognito().adminCreateUser(input); - - const newUser: RawUser = await createUserRecords({ + const createUserRecordsPromise = createUserRecords({ applicationContext, newUser: user, - userId, + userId: user.userId, }); - return newUser; + await Promise.all([createUserPromise, createUserRecordsPromise]); }; From 751dc21d43082db991763367b4df9e4287f03425 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 08:21:08 -0700 Subject: [PATCH 24/33] 10007: Move shared function to resend temporary password into use case helper, call user gateway function instead of cognito directly --- .../auth/resendTemporaryPassword.test.ts | 21 +++++++++++++++++ .../auth/resendTemporaryPassword.ts | 12 ++++++++++ .../auth/forgotPasswordInteractor.test.ts | 12 ++++------ .../useCases/auth/forgotPasswordInteractor.ts | 5 ++-- .../useCases/auth/loginInteractor.test.ts | 10 +++----- .../business/useCases/auth/loginInteractor.ts | 23 +++---------------- web-api/src/getUseCaseHelpers.ts | 2 ++ 7 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.test.ts create mode 100644 web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.ts diff --git a/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.test.ts b/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.test.ts new file mode 100644 index 00000000000..dc501efc72a --- /dev/null +++ b/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.test.ts @@ -0,0 +1,21 @@ +import { applicationContext } from '@shared/business/test/createTestApplicationContext'; +import { resendTemporaryPassword } from '@web-api/business/useCaseHelper/auth/resendTemporaryPassword'; + +describe('resendTemporaryPassword', () => { + it('should make a call to persistence to resend an invitation email to the provided email address', async () => { + const mockEmail = 'petitioner@example.com'; + + await resendTemporaryPassword(applicationContext, { + email: mockEmail, + }); + + expect(applicationContext.getUserGateway().createUser).toHaveBeenCalledWith( + applicationContext, + { + attributesToUpdate: {}, + email: mockEmail, + resendInvitationEmail: true, + }, + ); + }); +}); diff --git a/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.ts b/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.ts new file mode 100644 index 00000000000..184fa239a1f --- /dev/null +++ b/web-api/src/business/useCaseHelper/auth/resendTemporaryPassword.ts @@ -0,0 +1,12 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function resendTemporaryPassword( + applicationContext: ServerApplicationContext, + { email }: { email: string }, +): Promise { + await applicationContext.getUserGateway().createUser(applicationContext, { + attributesToUpdate: {}, + email, + resendInvitationEmail: true, + }); +} diff --git a/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts b/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts index 566e68d13aa..e3ac21472bf 100644 --- a/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts +++ b/web-api/src/business/useCases/auth/forgotPasswordInteractor.test.ts @@ -105,7 +105,7 @@ describe('forgotPasswordInteractor', () => { }); }); - it('should throw an UnauthorizedErrorresend a password change email when user`s account is in a force change password state (with TemporaryPassword on non-prod environments)', async () => { + it('should throw an UnauthorizedErrorr and resend a password change email when user`s account is in a force change password state (with TemporaryPassword on non-prod environments)', async () => { applicationContext.getUserGateway().getUserByEmail.mockResolvedValueOnce({ ...mockUser, accountStatus: UserStatusType.FORCE_CHANGE_PASSWORD, @@ -121,13 +121,9 @@ describe('forgotPasswordInteractor', () => { applicationContext.getUserGateway().getUserByEmail, ).toHaveBeenCalledWith(applicationContext, { email }); expect( - applicationContext.getCognito().adminCreateUser, - ).toHaveBeenCalledWith({ - DesiredDeliveryMediums: ['EMAIL'], - MessageAction: 'RESEND', - TemporaryPassword: process.env.DEFAULT_ACCOUNT_PASS, - UserPoolId: applicationContext.environment.userPoolId, - Username: email, + applicationContext.getUseCaseHelpers().resendTemporaryPassword, + ).toHaveBeenCalledWith(applicationContext, { + email, }); }); diff --git a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts index 795f296014f..f4422909db6 100644 --- a/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts +++ b/web-api/src/business/useCases/auth/forgotPasswordInteractor.ts @@ -1,7 +1,6 @@ import { ServerApplicationContext } from '@web-api/applicationContext'; import { UnauthorizedError } from '@web-api/errors/errors'; import { UserStatusType } from '@aws-sdk/client-cognito-identity-provider'; -import { resendTemporaryPassword } from '@web-api/business/useCases/auth/loginInteractor'; export const forgotPasswordInteractor = async ( applicationContext: ServerApplicationContext, @@ -31,7 +30,9 @@ export const forgotPasswordInteractor = async ( } if (user.accountStatus === UserStatusType.FORCE_CHANGE_PASSWORD) { - await resendTemporaryPassword(applicationContext, { email }); + await applicationContext + .getUseCaseHelpers() + .resendTemporaryPassword(applicationContext, { email }); throw new UnauthorizedError('User is unconfirmed'); //403 } diff --git a/web-api/src/business/useCases/auth/loginInteractor.test.ts b/web-api/src/business/useCases/auth/loginInteractor.test.ts index 92d17b15769..66c3e28681e 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.test.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.test.ts @@ -169,13 +169,9 @@ describe('loginInteractor', () => { ).rejects.toThrow(new UnauthorizedError('User temporary password expired')); expect( - applicationContext.getCognito().adminCreateUser.mock.calls[0][0], - ).toEqual({ - DesiredDeliveryMediums: ['EMAIL'], - MessageAction: 'RESEND', - TemporaryPassword: process.env.DEFAULT_ACCOUNT_PASS, - UserPoolId: applicationContext.environment.userPoolId, - Username: mockEmail, + applicationContext.getUseCaseHelpers().resendTemporaryPassword, + ).toHaveBeenCalledWith(applicationContext, { + email: mockEmail, }); }); diff --git a/web-api/src/business/useCases/auth/loginInteractor.ts b/web-api/src/business/useCases/auth/loginInteractor.ts index 604377dbb5c..551ec9d8e38 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.ts @@ -1,4 +1,3 @@ -import { AdminCreateUserCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { InvalidRequest, NotFoundError, @@ -48,7 +47,9 @@ export async function authErrorHandling( error.name === 'UserNotFoundException' ) { if (error.message?.includes('Temporary password has expired')) { - await resendTemporaryPassword(applicationContext, { email }); + await applicationContext + .getUseCaseHelpers() + .resendTemporaryPassword(applicationContext, { email }); throw new UnauthorizedError('User temporary password expired'); //403 } @@ -96,21 +97,3 @@ async function resendAccountConfirmation( userId: user.userId, }); } - -export async function resendTemporaryPassword( - applicationContext: ServerApplicationContext, - { email }: { email: string }, -): Promise { - const input: AdminCreateUserCommandInput = { - DesiredDeliveryMediums: ['EMAIL'], - MessageAction: 'RESEND', - UserPoolId: applicationContext.environment.userPoolId, - Username: email, - }; - - if (process.env.STAGE !== 'prod') { - input.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; - } - - await applicationContext.getCognito().adminCreateUser(input); -} diff --git a/web-api/src/getUseCaseHelpers.ts b/web-api/src/getUseCaseHelpers.ts index 29cdc3589de..dd8c5f0650c 100644 --- a/web-api/src/getUseCaseHelpers.ts +++ b/web-api/src/getUseCaseHelpers.ts @@ -29,6 +29,7 @@ import { getUserIdForNote } from '../../shared/src/business/useCaseHelper/getUse import { parseAndScrapePdfContents } from '../../shared/src/business/useCaseHelper/pdf/parseAndScrapePdfContents'; import { removeCounselFromRemovedPetitioner } from '../../shared/src/business/useCaseHelper/caseAssociation/removeCounselFromRemovedPetitioner'; import { removeCoversheet } from '../../shared/src/business/useCaseHelper/coverSheets/removeCoversheet'; +import { resendTemporaryPassword } from '@web-api/business/useCaseHelper/auth/resendTemporaryPassword'; import { saveFileAndGenerateUrl } from '../../shared/src/business/useCaseHelper/saveFileAndGenerateUrl'; import { sealInLowerEnvironment } from '../../shared/src/business/useCaseHelper/sealInLowerEnvironment'; import { sendEmailVerificationLink } from '../../shared/src/business/useCaseHelper/email/sendEmailVerificationLink'; @@ -77,6 +78,7 @@ const useCaseHelpers = { parseAndScrapePdfContents, removeCounselFromRemovedPetitioner, removeCoversheet, + resendTemporaryPassword, saveFileAndGenerateUrl, sealInLowerEnvironment, sendEmailVerificationLink, From 348decf7b7714f5c926ec73cc8f6b306fc5b1708 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 10:03:35 -0700 Subject: [PATCH 25/33] 10007: Call user gateway function instead of cognito directly --- .../auth/confirmSignUpInteractor.test.ts | 4 +--- .../useCases/auth/confirmSignUpInteractor.ts | 20 ++++++------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts index 6e3495db3e5..b857d96877d 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts @@ -55,9 +55,7 @@ describe('confirmSignUpInteractor', () => { expect( applicationContext.getCognito().adminConfirmSignUp, ).toHaveBeenCalled(); - expect( - applicationContext.getCognito().adminUpdateUserAttributes, - ).toHaveBeenCalled(); + expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalled(); expect( applicationContext.getUseCases().createPetitionerAccountInteractor, ).toHaveBeenCalled(); diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts index 05746346a55..a70385c744d 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts @@ -26,20 +26,12 @@ export const confirmSignUpInteractor = async ( }); const updatePetitionerAttributes = applicationContext - .getCognito() - .adminUpdateUserAttributes({ - UserAttributes: [ - { - Name: 'email_verified', - Value: 'true', - }, - { - Name: 'email', - Value: email, - }, - ], - UserPoolId: process.env.USER_POOL_ID, - Username: email, + .getUserGateway() + .updateUser(applicationContext, { + attributesToUpdate: { + email, + }, + email, }); await Promise.all([ From d704f46f68b10c3b90cc44430cc4829b999cd54f Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 10:18:14 -0700 Subject: [PATCH 26/33] 10007: Call user gateway function instead of cognito directly --- .../users/createNewPractitionerUser.test.ts | 97 +++++++------- .../dynamo/users/createNewPractitionerUser.ts | 118 +++++++----------- 2 files changed, 89 insertions(+), 126 deletions(-) diff --git a/web-api/src/persistence/dynamo/users/createNewPractitionerUser.test.ts b/web-api/src/persistence/dynamo/users/createNewPractitionerUser.test.ts index c3831d9c713..d7a52bc0b79 100644 --- a/web-api/src/persistence/dynamo/users/createNewPractitionerUser.test.ts +++ b/web-api/src/persistence/dynamo/users/createNewPractitionerUser.test.ts @@ -1,4 +1,10 @@ -import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants'; +import { + ADMISSIONS_STATUS_OPTIONS, + PRACTITIONER_TYPE_OPTIONS, + ROLES, + SERVICE_INDICATOR_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; +import { RawPractitioner } from '@shared/business/entities/Practitioner'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { createNewPractitionerUser } from './createNewPractitionerUser'; import { put } from '../../dynamodbClientService'; @@ -9,71 +15,60 @@ jest.mock('../../dynamodbClientService', () => ({ const mockPut = put as jest.Mock; describe('createNewPractitionerUser', () => { + const mockNewPractitionerUser: RawPractitioner = { + admissionsDate: '09/01/2020', + admissionsStatus: ADMISSIONS_STATUS_OPTIONS[0], + barNumber: 'tpp1234', + birthYear: '1990', + employer: 'Lawyers, INC', + entityName: 'Practitioner', + firstName: 'Test Private', + lastName: 'Practitioner', + name: 'Test Private Practitioner', + originalBarState: 'OR', + pendingEmail: 'practitioner@example.com', + practitionerType: PRACTITIONER_TYPE_OPTIONS[0], + role: ROLES.privatePractitioner, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_ELECTRONIC, + userId: '16c6e88c-b333-4eb7-981b-ee97f647c4db', + }; + it('should not log an error when creating a new cognito account for a practitioner user', async () => { await createNewPractitionerUser({ applicationContext, - user: { - barNumber: 'tpp1234', - name: 'Test Private Practitioner', - pendingEmail: 'practitioner@example.com', - role: ROLES.privatePractitioner, - section: 'practitioner', - userId: '123', - } as any, + user: mockNewPractitionerUser, }); - expect( - applicationContext.getCognito().adminCreateUser, - ).toHaveBeenCalledWith( - expect.objectContaining({ - UserAttributes: expect.arrayContaining([ - { - Name: 'email_verified', - Value: 'True', - }, - { - Name: 'email', - Value: 'practitioner@example.com', - }, - { - Name: 'custom:role', - Value: ROLES.privatePractitioner, - }, - { - Name: 'name', - Value: 'Test Private Practitioner', - }, - ]), - Username: 'practitioner@example.com', - }), + expect(applicationContext.getUserGateway().createUser).toHaveBeenCalledWith( + applicationContext, + { + attributesToUpdate: { + email: mockNewPractitionerUser.pendingEmail, + name: mockNewPractitionerUser.name, + role: mockNewPractitionerUser.role, + userId: mockNewPractitionerUser.userId, + }, + email: mockNewPractitionerUser.pendingEmail, + resendInvitationEmail: false, + }, ); }); describe('updateUserRecords', () => { - beforeEach(() => { - mockPut.mockReturnValue(null); - }); - - it('should put new records with uppercase name and bar number', async () => { + it('should create new records in persistence for the practitioner with uppercase name and bar number', async () => { await createNewPractitionerUser({ applicationContext, - user: { - barNumber: 'tpp1234', - email: 'practitioner@example.com', - name: 'Test Private Practitioner', - role: ROLES.privatePractitioner, - section: 'practitioner', - userId: '123', - } as any, + user: mockNewPractitionerUser, }); - const putItem2 = mockPut.mock.calls[1][0].Item; - const putItem3 = mockPut.mock.calls[2][0].Item; - - expect(putItem2.pk).toEqual( + const roleBasedRecord = mockPut.mock.calls[1][0].Item; + expect(roleBasedRecord.pk).toEqual( `${ROLES.privatePractitioner}|TEST PRIVATE PRACTITIONER`, ); - expect(putItem3.pk).toEqual(`${ROLES.privatePractitioner}|TPP1234`); + const barNumberBasedRecord = mockPut.mock.calls[2][0].Item; + expect(barNumberBasedRecord.pk).toEqual( + `${ROLES.privatePractitioner}|TPP1234`, + ); }); }); }); diff --git a/web-api/src/persistence/dynamo/users/createNewPractitionerUser.ts b/web-api/src/persistence/dynamo/users/createNewPractitionerUser.ts index 0f0d4333dc6..b5865a34642 100644 --- a/web-api/src/persistence/dynamo/users/createNewPractitionerUser.ts +++ b/web-api/src/persistence/dynamo/users/createNewPractitionerUser.ts @@ -1,8 +1,5 @@ -import { - AdminCreateUserCommandInput, - CognitoIdentityProvider, -} from '@aws-sdk/client-cognito-identity-provider'; import { RawPractitioner } from '@shared/business/entities/Practitioner'; +import { ServerApplicationContext } from '@web-api/applicationContext'; import { put } from '../../dynamodbClientService'; export const updateUserRecords = async ({ @@ -10,35 +7,35 @@ export const updateUserRecords = async ({ updatedUser, userId, }: { - applicationContext: IApplicationContext; - updatedUser: any; + applicationContext: ServerApplicationContext; + updatedUser: RawPractitioner; userId: string; -}) => { - await put({ - Item: { - ...updatedUser, - pk: `user|${userId}`, - sk: `user|${userId}`, - userId, - }, - applicationContext, - }); - - await put({ - Item: { - pk: `${updatedUser.role}|${updatedUser.name.toUpperCase()}`, - sk: `user|${userId}`, - }, - applicationContext, - }); - - await put({ - Item: { - pk: `${updatedUser.role}|${updatedUser.barNumber.toUpperCase()}`, - sk: `user|${userId}`, - }, - applicationContext, - }); +}): Promise => { + await Promise.all([ + put({ + Item: { + ...updatedUser, + pk: `user|${userId}`, + sk: `user|${userId}`, + userId, + }, + applicationContext, + }), + put({ + Item: { + pk: `${updatedUser.role}|${updatedUser.name.toUpperCase()}`, + sk: `user|${userId}`, + }, + applicationContext, + }), + put({ + Item: { + pk: `${updatedUser.role}|${updatedUser.barNumber.toUpperCase()}`, + sk: `user|${userId}`, + }, + applicationContext, + }), + ]); return { ...updatedUser, @@ -50,52 +47,23 @@ export const createNewPractitionerUser = async ({ applicationContext, user, }: { - applicationContext: IApplicationContext; + applicationContext: ServerApplicationContext; user: RawPractitioner; -}) => { - const { userId } = user; - - const cognito: CognitoIdentityProvider = applicationContext.getCognito(); - - const params: AdminCreateUserCommandInput = { - DesiredDeliveryMediums: ['EMAIL'], - UserAttributes: [ - { - Name: 'email_verified', - Value: 'True', - }, - { - Name: 'email', - Value: user.pendingEmail, - }, - { - Name: 'custom:role', - Value: user.role, - }, - { - Name: 'name', - Value: user.name, - }, - { - Name: 'custom:userId', - Value: user.userId, - }, - ], - UserPoolId: process.env.USER_POOL_ID, - Username: user.pendingEmail, - }; - - if (process.env.STAGE !== 'prod') { - params.TemporaryPassword = process.env.DEFAULT_ACCOUNT_PASS; - } - - await cognito.adminCreateUser(params); +}): Promise => { + await applicationContext.getUserGateway().createUser(applicationContext, { + attributesToUpdate: { + email: user.pendingEmail, + name: user.name, + role: user.role, + userId: user.userId, + }, + email: user.pendingEmail!, + resendInvitationEmail: false, + }); - const updatedUser = await updateUserRecords({ + return await updateUserRecords({ applicationContext, updatedUser: user, - userId, + userId: user.userId, }); - - return updatedUser; }; From ef48b3c94ff79b57766311f9cfe3c903e5fed8e1 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 10:25:05 -0700 Subject: [PATCH 27/33] 10007: Don't call an interactor from an interactor. Inline create petitioner into confirm sign up since that's the only place it's used --- .../createPetitionerAccountInteractor.test.ts | 45 ------------------- .../createPetitionerAccountInteractor.ts | 29 ------------ .../auth/confirmSignUpInteractor.test.ts | 9 ++-- .../useCases/auth/confirmSignUpInteractor.ts | 22 ++++++--- web-api/src/getUseCases.ts | 2 - .../persistence/dynamo/users/persistUser.ts | 6 +-- 6 files changed, 23 insertions(+), 90 deletions(-) delete mode 100644 shared/src/business/useCases/users/createPetitionerAccountInteractor.test.ts delete mode 100644 shared/src/business/useCases/users/createPetitionerAccountInteractor.ts diff --git a/shared/src/business/useCases/users/createPetitionerAccountInteractor.test.ts b/shared/src/business/useCases/users/createPetitionerAccountInteractor.test.ts deleted file mode 100644 index d3b23bf4b79..00000000000 --- a/shared/src/business/useCases/users/createPetitionerAccountInteractor.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ROLES } from '../../entities/EntityConstants'; -import { applicationContext } from '../../test/createTestApplicationContext'; -import { createPetitionerAccountInteractor } from './createPetitionerAccountInteractor'; - -describe('createPetitionerAccountInteractor', () => { - it('should attempt to persist the petitioner', async () => { - applicationContext - .getPersistenceGateway() - .persistUser.mockReturnValue(null); - - await createPetitionerAccountInteractor(applicationContext, { - email: 'test@example.com', - name: 'Cody', - userId: '2fa6da8d-4328-4a20-a5d7-b76637e1dc02', - }); - - expect( - applicationContext.getPersistenceGateway().persistUser.mock.calls[0][0] - .user, - ).toMatchObject({ - email: 'test@example.com', - entityName: 'User', - name: 'Cody', - role: ROLES.petitioner, - userId: '2fa6da8d-4328-4a20-a5d7-b76637e1dc02', - }); - }); - - it('should validate the user entity', async () => { - applicationContext - .getPersistenceGateway() - .persistUser.mockReturnValue(null); - - let error = null; - try { - await createPetitionerAccountInteractor(applicationContext, { - name: 'Cody', - userId: '2fa6da8d-4328-4a20-a5d7-b76637e1dc02', - } as any); - } catch (err) { - error = err; - } - expect(error).toBeDefined(); - }); -}); diff --git a/shared/src/business/useCases/users/createPetitionerAccountInteractor.ts b/shared/src/business/useCases/users/createPetitionerAccountInteractor.ts deleted file mode 100644 index f00761d635a..00000000000 --- a/shared/src/business/useCases/users/createPetitionerAccountInteractor.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ROLES } from '../../entities/EntityConstants'; -import { User } from '../../entities/User'; - -/** - * createPetitionerAccountInteractor - * - * @param {object} applicationContext the application context - * @param {object} providers the providers object - * @param {object} providers.user the user data - * @returns {Promise} the promise of the createUser call - */ -export const createPetitionerAccountInteractor = async ( - applicationContext: IApplicationContext, - { email, name, userId }: { email: string; name: string; userId: string }, -) => { - const userEntity = new User({ - email, - name, - role: ROLES.petitioner, - userId, - }); - - await applicationContext.getPersistenceGateway().persistUser({ - applicationContext, - user: userEntity.validate().toRawObject(), - }); - - return userEntity.validate().toRawObject(); -}; diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts index b857d96877d..94df6c630fc 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts @@ -42,9 +42,10 @@ describe('confirmSignUpInteractor', () => { .getPersistenceGateway() .getAccountConfirmationCode.mockResolvedValue(mockConfirmationCode); applicationContext.getCognito().adminConfirmSignUp.mockResolvedValue({}); - applicationContext - .getUserGateway() - .getUserByEmail.mockResolvedValue({ email: mockEmail }); + applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ + email: mockEmail, + name: 'Test Petitioner', + }); await confirmSignUpInteractor(applicationContext, { confirmationCode: mockConfirmationCode, @@ -57,7 +58,7 @@ describe('confirmSignUpInteractor', () => { ).toHaveBeenCalled(); expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalled(); expect( - applicationContext.getUseCases().createPetitionerAccountInteractor, + applicationContext.getPersistenceGateway().persistUser, ).toHaveBeenCalled(); }); }); diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts index a70385c744d..61f851a314a 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts @@ -1,5 +1,7 @@ import { InvalidRequest, NotFoundError } from '@web-api/errors/errors'; +import { ROLES } from '@shared/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; +import { User } from '@shared/business/entities/User'; export const confirmSignUpInteractor = async ( applicationContext: ServerApplicationContext, @@ -52,11 +54,17 @@ const createPetitionerUser = async ( throw new NotFoundError(`User not found with email: ${email}`); } - await applicationContext - .getUseCases() - .createPetitionerAccountInteractor(applicationContext, { - email, - name: user.name, - userId, - }); + const userEntity = new User({ + email, + name: user.name, + role: ROLES.petitioner, + userId, + }); + + await applicationContext.getPersistenceGateway().persistUser({ + applicationContext, + user: userEntity.validate().toRawObject(), + }); + + return userEntity.validate().toRawObject(); }; diff --git a/web-api/src/getUseCases.ts b/web-api/src/getUseCases.ts index 8f0a5550a23..2a67f325902 100644 --- a/web-api/src/getUseCases.ts +++ b/web-api/src/getUseCases.ts @@ -29,7 +29,6 @@ import { createCaseFromPaperInteractor } from '../../shared/src/business/useCase import { createCaseInteractor } from '../../shared/src/business/useCases/createCaseInteractor'; import { createCourtIssuedOrderPdfFromHtmlInteractor } from '../../shared/src/business/useCases/courtIssuedOrder/createCourtIssuedOrderPdfFromHtmlInteractor'; import { createMessageInteractor } from '../../shared/src/business/useCases/messages/createMessageInteractor'; -import { createPetitionerAccountInteractor } from '../../shared/src/business/useCases/users/createPetitionerAccountInteractor'; import { createPractitionerDocumentInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerDocumentInteractor'; import { createPractitionerUserInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerUserInteractor'; import { createTrialSessionInteractor } from '../../shared/src/business/useCases/trialSessions/createTrialSessionInteractor'; @@ -242,7 +241,6 @@ const useCases = { createCaseInteractor, createCourtIssuedOrderPdfFromHtmlInteractor, createMessageInteractor, - createPetitionerAccountInteractor, createPractitionerDocumentInteractor, createPractitionerUserInteractor, createTrialSessionInteractor, diff --git a/web-api/src/persistence/dynamo/users/persistUser.ts b/web-api/src/persistence/dynamo/users/persistUser.ts index bcc75424790..0605a684b16 100644 --- a/web-api/src/persistence/dynamo/users/persistUser.ts +++ b/web-api/src/persistence/dynamo/users/persistUser.ts @@ -1,5 +1,5 @@ -import * as client from '../../dynamodbClientService'; import { RawUser } from '@shared/business/entities/User'; +import { put } from '../../dynamodbClientService'; export const persistUser = async ({ applicationContext, @@ -7,8 +7,8 @@ export const persistUser = async ({ }: { applicationContext: IApplicationContext; user: RawUser; -}) => { - await client.put({ +}): Promise => { + await put({ Item: { ...user, pk: `user|${user.userId}`, From 513e7b4ca4936ec9c0d726e597156e5adeab68d3 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Mon, 11 Mar 2024 15:34:47 -0700 Subject: [PATCH 28/33] 10007: Fixing unit test --- web-api/src/business/useCases/auth/loginInteractor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-api/src/business/useCases/auth/loginInteractor.test.ts b/web-api/src/business/useCases/auth/loginInteractor.test.ts index 166e0d7496e..de919553514 100644 --- a/web-api/src/business/useCases/auth/loginInteractor.test.ts +++ b/web-api/src/business/useCases/auth/loginInteractor.test.ts @@ -129,7 +129,7 @@ describe('loginInteractor', () => { message: '', }); applicationContext - .getCognito() + .getUserGateway() .initiateAuth.mockRejectedValue(mockUnconfirmedAccountError); applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ email: mockEmail, From c895bc45551e68406992a98eecb7d3ac677be20f Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Tue, 12 Mar 2024 08:01:22 -0700 Subject: [PATCH 29/33] 10007: Extract confirm sign up to user gateway function --- .../auth/confirmSignUpInteractor.test.ts | 4 ++-- .../useCases/auth/confirmSignUpInteractor.ts | 5 ++--- .../src/gateways/user/changePassword.test.ts | 2 +- .../src/gateways/user/confirmSignUp.test.ts | 21 +++++++++++++++++++ web-api/src/gateways/user/confirmSignUp.ts | 11 ++++++++++ web-api/src/getUserGateway.ts | 2 ++ 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 web-api/src/gateways/user/confirmSignUp.test.ts create mode 100644 web-api/src/gateways/user/confirmSignUp.ts diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts index 94df6c630fc..3d163d68a56 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.test.ts @@ -41,7 +41,7 @@ describe('confirmSignUpInteractor', () => { applicationContext .getPersistenceGateway() .getAccountConfirmationCode.mockResolvedValue(mockConfirmationCode); - applicationContext.getCognito().adminConfirmSignUp.mockResolvedValue({}); + applicationContext.getUserGateway().confirmSignUp.mockResolvedValue({}); applicationContext.getUserGateway().getUserByEmail.mockResolvedValue({ email: mockEmail, name: 'Test Petitioner', @@ -54,7 +54,7 @@ describe('confirmSignUpInteractor', () => { }); expect( - applicationContext.getCognito().adminConfirmSignUp, + applicationContext.getUserGateway().confirmSignUp, ).toHaveBeenCalled(); expect(applicationContext.getUserGateway().updateUser).toHaveBeenCalled(); expect( diff --git a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts index 61f851a314a..6f2d2143aba 100644 --- a/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts +++ b/web-api/src/business/useCases/auth/confirmSignUpInteractor.ts @@ -22,9 +22,8 @@ export const confirmSignUpInteractor = async ( throw new InvalidRequest('Confirmation code expired'); } - await applicationContext.getCognito().adminConfirmSignUp({ - UserPoolId: process.env.USER_POOL_ID, - Username: email, + await applicationContext.getUserGateway().confirmSignUp(applicationContext, { + email, }); const updatePetitionerAttributes = applicationContext diff --git a/web-api/src/gateways/user/changePassword.test.ts b/web-api/src/gateways/user/changePassword.test.ts index feaeaf93ad5..f5388c571e7 100644 --- a/web-api/src/gateways/user/changePassword.test.ts +++ b/web-api/src/gateways/user/changePassword.test.ts @@ -2,7 +2,7 @@ import { applicationContext } from '../../../../shared/src/business/test/createT import { changePassword } from '@web-api/gateways/user/changePassword'; describe('changePassword', () => { - it('should make a call to disable the provided userId', async () => { + it('should make a call update the password for the account with the provided email', async () => { const mockEmail = 'test@example.com'; const mockNewPassword = 'P@ssw0rd'; const mockCode = 'afde08bd-7ccc-4163-9242-87f78cbb2452'; diff --git a/web-api/src/gateways/user/confirmSignUp.test.ts b/web-api/src/gateways/user/confirmSignUp.test.ts new file mode 100644 index 00000000000..9a2bce759c7 --- /dev/null +++ b/web-api/src/gateways/user/confirmSignUp.test.ts @@ -0,0 +1,21 @@ +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { confirmSignUp } from '@web-api/gateways/user/confirmSignUp'; + +describe('confirmSignUp', () => { + it('should make a call confirm ownership of the provided email', async () => { + const mockEmail = 'test@example.com'; + const mockUserPoolId = 'test'; + applicationContext.environment.userPoolId = mockUserPoolId; + + await confirmSignUp(applicationContext, { + email: mockEmail, + }); + + expect( + applicationContext.getCognito().adminConfirmSignUp, + ).toHaveBeenCalledWith({ + UserPoolId: mockUserPoolId, + Username: mockEmail, + }); + }); +}); diff --git a/web-api/src/gateways/user/confirmSignUp.ts b/web-api/src/gateways/user/confirmSignUp.ts new file mode 100644 index 00000000000..aab45a15207 --- /dev/null +++ b/web-api/src/gateways/user/confirmSignUp.ts @@ -0,0 +1,11 @@ +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function confirmSignUp( + applicationContext: ServerApplicationContext, + { email }: { email: string }, +): Promise { + await applicationContext.getCognito().adminConfirmSignUp({ + UserPoolId: applicationContext.environment.userPoolId, + Username: email, + }); +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index ed529d0091a..c5597efe5f4 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -1,4 +1,5 @@ import { changePassword } from '@web-api/gateways/user/changePassword'; +import { confirmSignUp } from '@web-api/gateways/user/confirmSignUp'; import { createUser } from '@web-api/gateways/user/createUser'; import { disableUser } from '@web-api/gateways/user/disableUser'; import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; @@ -9,6 +10,7 @@ import { updateUser } from '@web-api/gateways/user/updateUser'; export const getUserGateway = () => ({ changePassword, + confirmSignUp, createUser, disableUser, forgotPassword, From 19ef516a2d3bbaf30be26b9543e73e0e02ac0f0b Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Tue, 12 Mar 2024 08:19:00 -0700 Subject: [PATCH 30/33] 10007: Extract sign up to user gateway function --- .../auth/signUpUserInteractor.test.ts | 61 +++++++------------ .../useCases/auth/signUpUserInteractor.ts | 33 +++------- web-api/src/gateways/user/signUp.test.ts | 47 ++++++++++++++ web-api/src/gateways/user/signUp.ts | 45 ++++++++++++++ web-api/src/getUserGateway.ts | 2 + 5 files changed, 126 insertions(+), 62 deletions(-) create mode 100644 web-api/src/gateways/user/signUp.test.ts create mode 100644 web-api/src/gateways/user/signUp.ts diff --git a/web-api/src/business/useCases/auth/signUpUserInteractor.test.ts b/web-api/src/business/useCases/auth/signUpUserInteractor.test.ts index 106e450abea..3cf9c13b01f 100644 --- a/web-api/src/business/useCases/auth/signUpUserInteractor.test.ts +++ b/web-api/src/business/useCases/auth/signUpUserInteractor.test.ts @@ -6,15 +6,15 @@ import { signUpUserInteractor } from './signUpUserInteractor'; describe('signUpUserInteractor', () => { const email = 'example@example.com'; const name = 'Antoninus Sara'; - const userId = 'c3f56e3d-0e6e-44bb-98f1-7c4a91dca1b9'; + const mockUserId = 'c3f56e3d-0e6e-44bb-98f1-7c4a91dca1b9'; const password = 'Pa$$w0rd!'; const mockConfirmationCode = '09d0322d-12da-47c8-8d8b-cc76f97022c2'; const user = { confirmPassword: password, email, name, password }; beforeEach(() => { - applicationContext.getUniqueId.mockReturnValue(userId); - - applicationContext.getCognito().signUp.mockResolvedValue({}); + applicationContext + .getUserGateway() + .signUp.mockResolvedValue({ userId: mockUserId }); applicationContext .getUseCaseHelpers() @@ -32,34 +32,19 @@ describe('signUpUserInteractor', () => { user, }); - expect( - applicationContext.getCognito().signUp.mock.calls[0][0], - ).toMatchObject({ - Password: password, - UserAttributes: [ - { - Name: 'email', - Value: email, - }, - { - Name: 'name', - Value: name, - }, - { - Name: 'custom:userId', - Value: userId, - }, - { - Name: 'custom:role', - Value: ROLES.petitioner, - }, - ], - Username: email, - }); + expect(applicationContext.getUserGateway().signUp).toHaveBeenCalledWith( + applicationContext, + { + email, + name, + password, + role: ROLES.petitioner, + }, + ); expect(result).toEqual({ confirmationCode: mockConfirmationCode, email: user.email, - userId, + userId: mockUserId, }); }); @@ -76,7 +61,7 @@ describe('signUpUserInteractor', () => { expect(result).toEqual({ confirmationCode: undefined, email: user.email, - userId, + userId: mockUserId, }); }); @@ -84,7 +69,9 @@ describe('signUpUserInteractor', () => { applicationContext .getCognito() .listUsers.mockResolvedValue({ Users: undefined }); - applicationContext.getCognito().signUp.mockRejectedValue(new Error('abc')); + applicationContext + .getUserGateway() + .signUp.mockRejectedValue(new Error('abc')); await expect( signUpUserInteractor(applicationContext, { @@ -92,7 +79,7 @@ describe('signUpUserInteractor', () => { }), ).rejects.toThrow(); - expect(applicationContext.getCognito().signUp).toHaveBeenCalled(); + expect(applicationContext.getUserGateway().signUp).toHaveBeenCalled(); }); it('should throw an error when the new user is not valid', async () => { @@ -112,8 +99,6 @@ describe('signUpUserInteractor', () => { ).rejects.toThrow( 'The NewPetitionerUser entity was invalid. {"password":"Must contain number","confirmPassword":"Passwords must match"}', ); - - expect(applicationContext.getCognito().signUp).not.toHaveBeenCalled(); }); it('should throw an error when the provided email already exists for an account in the system and it has been confirmed', async () => { @@ -121,7 +106,7 @@ describe('signUpUserInteractor', () => { Users: [ { UserStatus: UserStatusType.CONFIRMED, - userId, + userId: mockUserId, }, ], }); @@ -132,7 +117,7 @@ describe('signUpUserInteractor', () => { }), ).rejects.toThrow('User already exists'); - expect(applicationContext.getCognito().signUp).not.toHaveBeenCalled(); + expect(applicationContext.getUserGateway().signUp).not.toHaveBeenCalled(); }); it('should throw an error when the provided email already exists for an account in the system and the account has not yet been confirmed', async () => { @@ -140,7 +125,7 @@ describe('signUpUserInteractor', () => { Users: [ { UserStatus: UserStatusType.UNCONFIRMED, - userId, + userId: mockUserId, }, ], }); @@ -151,6 +136,6 @@ describe('signUpUserInteractor', () => { }), ).rejects.toThrow('User exists, email unconfirmed'); - expect(applicationContext.getCognito().signUp).not.toHaveBeenCalled(); + expect(applicationContext.getUserGateway().signUp).not.toHaveBeenCalled(); }); }); diff --git a/web-api/src/business/useCases/auth/signUpUserInteractor.ts b/web-api/src/business/useCases/auth/signUpUserInteractor.ts index c7e15ae26bb..a29459fd9c9 100644 --- a/web-api/src/business/useCases/auth/signUpUserInteractor.ts +++ b/web-api/src/business/useCases/auth/signUpUserInteractor.ts @@ -45,30 +45,15 @@ export const signUpUserInteractor = async ( } const newUser = new NewPetitionerUser(user).validate().toRawObject(); - const userId = applicationContext.getUniqueId(); - await applicationContext.getCognito().signUp({ - ClientId: applicationContext.environment.cognitoClientId, - Password: newUser.password, - UserAttributes: [ - { - Name: 'email', - Value: newUser.email, - }, - { - Name: 'name', - Value: newUser.name, - }, - { - Name: 'custom:userId', - Value: userId, - }, - { - Name: 'custom:role', - Value: ROLES.petitioner, - }, - ], - Username: newUser.email, - }); + + const { userId } = await applicationContext + .getUserGateway() + .signUp(applicationContext, { + email: newUser.email, + name: newUser.name, + password: newUser.password, + role: ROLES.petitioner, + }); const { confirmationCode } = await applicationContext .getUseCaseHelpers() diff --git a/web-api/src/gateways/user/signUp.test.ts b/web-api/src/gateways/user/signUp.test.ts new file mode 100644 index 00000000000..554f0537d6e --- /dev/null +++ b/web-api/src/gateways/user/signUp.test.ts @@ -0,0 +1,47 @@ +import { ROLES } from '@shared/business/entities/EntityConstants'; +import { applicationContext } from '../../../../shared/src/business/test/createTestApplicationContext'; +import { signUp } from '@web-api/gateways/user/signUp'; + +describe('signUp', () => { + it('should make a call to disable the user with the provided email', async () => { + const mockEmail = 'test@example.com'; + const mockName = 'Test Petitioner'; + const mockPassword = 'P@ssword!'; + const mockRole = ROLES.petitioner; + const mockUserId = '2a1aa887-6350-48dc-bb3b-9fe699eae776'; + const mockCognitoClientId = 'test'; + applicationContext.environment.cognitoClientId = mockCognitoClientId; + applicationContext.getUniqueId.mockReturnValue(mockUserId); + + await signUp(applicationContext, { + email: mockEmail, + name: mockName, + password: mockPassword, + role: mockRole, + }); + + expect(applicationContext.getCognito().signUp).toHaveBeenCalledWith({ + ClientId: mockCognitoClientId, + Password: mockPassword, + UserAttributes: [ + { + Name: 'email', + Value: mockEmail, + }, + { + Name: 'name', + Value: mockName, + }, + { + Name: 'custom:userId', + Value: mockUserId, + }, + { + Name: 'custom:role', + Value: mockRole, + }, + ], + Username: mockEmail, + }); + }); +}); diff --git a/web-api/src/gateways/user/signUp.ts b/web-api/src/gateways/user/signUp.ts new file mode 100644 index 00000000000..3e441b999ce --- /dev/null +++ b/web-api/src/gateways/user/signUp.ts @@ -0,0 +1,45 @@ +import { Role } from '@shared/business/entities/EntityConstants'; +import { ServerApplicationContext } from '@web-api/applicationContext'; + +export async function signUp( + applicationContext: ServerApplicationContext, + { + email, + name, + password, + role, + }: { + password: string; + email: string; + name: string; + role: Role; + }, +): Promise<{ userId: string }> { + const userId = applicationContext.getUniqueId(); + + await applicationContext.getCognito().signUp({ + ClientId: applicationContext.environment.cognitoClientId, + Password: password, + UserAttributes: [ + { + Name: 'email', + Value: email, + }, + { + Name: 'name', + Value: name, + }, + { + Name: 'custom:userId', + Value: userId, + }, + { + Name: 'custom:role', + Value: role, + }, + ], + Username: email, + }); + + return { userId }; +} diff --git a/web-api/src/getUserGateway.ts b/web-api/src/getUserGateway.ts index c5597efe5f4..9528d27b501 100644 --- a/web-api/src/getUserGateway.ts +++ b/web-api/src/getUserGateway.ts @@ -6,6 +6,7 @@ import { forgotPassword } from '@web-api/gateways/user/forgotPassword'; import { getUserByEmail } from '@web-api/gateways/user/getUserByEmail'; import { initiateAuth } from '@web-api/gateways/user/initiateAuth'; import { renewIdToken } from '@web-api/gateways/user/renewIdToken'; +import { signUp } from '@web-api/gateways/user/signUp'; import { updateUser } from '@web-api/gateways/user/updateUser'; export const getUserGateway = () => ({ @@ -17,5 +18,6 @@ export const getUserGateway = () => ({ getUserByEmail, initiateAuth, renewIdToken, + signUp, updateUser, }); From 3d9a14711ea4dfa5b2e83edc7e1c4c9a2acff93a Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Tue, 12 Mar 2024 09:41:40 -0700 Subject: [PATCH 31/33] 10007: Refactor and simplify creating practitioner user --- .../createPractitionerUserInteractor.ts | 45 --------------- .../utilities/createPractitionerUser.ts | 16 ++---- .../createPractitionerUserProxy.ts | 10 +--- .../createPractitionerUserInteractor.test.ts | 57 +++++++++---------- .../createPractitionerUserInteractor.ts | 35 ++++++++++++ .../useCases/user/createUserInteractor.ts | 4 +- web-api/src/getUseCases.ts | 2 +- .../createPractitionerUserAction.test.ts | 26 +++------ .../actions/createPractitionerUserAction.ts | 9 +-- 9 files changed, 84 insertions(+), 120 deletions(-) delete mode 100644 shared/src/business/useCases/practitioners/createPractitionerUserInteractor.ts rename {shared/src/business/useCases/practitioners => web-api/src/business/useCases/practitioner}/createPractitionerUserInteractor.test.ts (55%) create mode 100644 web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.ts diff --git a/shared/src/business/useCases/practitioners/createPractitionerUserInteractor.ts b/shared/src/business/useCases/practitioners/createPractitionerUserInteractor.ts deleted file mode 100644 index f3d9653a250..00000000000 --- a/shared/src/business/useCases/practitioners/createPractitionerUserInteractor.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Practitioner, RawPractitioner } from '../../entities/Practitioner'; -import { - ROLE_PERMISSIONS, - isAuthorized, -} from '../../../authorization/authorizationClientService'; -import { UnauthorizedError } from '@web-api/errors/errors'; -import { createPractitionerUser } from '../../utilities/createPractitionerUser'; - -/** - * createPractitionerUserInteractor - * - * @param {object} applicationContext the application context - * @param {object} providers the providers object - * @param {object} providers.user the user data - * @returns {Promise} the promise of the createUser call - */ -export const createPractitionerUserInteractor = async ( - applicationContext: IApplicationContext, - { user }: { user: RawPractitioner }, -) => { - const requestUser = applicationContext.getCurrentUser(); - - if (!isAuthorized(requestUser, ROLE_PERMISSIONS.ADD_EDIT_PRACTITIONER_USER)) { - throw new UnauthorizedError('Unauthorized for creating practitioner user'); - } - - user.pendingEmail = user.email; - user.email = undefined; - - const practitioner = await createPractitionerUser({ - applicationContext, - user, - }); - - const createdUser = await applicationContext - .getPersistenceGateway() - .createOrUpdatePractitionerUser({ - applicationContext, - user: practitioner, - }); - - return new Practitioner(createdUser, { applicationContext }) - .validate() - .toRawObject(); -}; diff --git a/shared/src/business/utilities/createPractitionerUser.ts b/shared/src/business/utilities/createPractitionerUser.ts index 66d4569c845..174e41a577d 100644 --- a/shared/src/business/utilities/createPractitionerUser.ts +++ b/shared/src/business/utilities/createPractitionerUser.ts @@ -1,14 +1,10 @@ -import { Practitioner } from '../entities/Practitioner'; +import { Practitioner, RawPractitioner } from '../entities/Practitioner'; +import { ServerApplicationContext } from '@web-api/applicationContext'; -/** - * Create a new practitioner - * - * @param {object} providers the providers object - * @param {object} providers.applicationContext the application context - * @param {object} providers.user the user data - * @returns {object} new practitioner user - */ -export const createPractitionerUser = async ({ applicationContext, user }) => { +export const createPractitionerUser = async ( + applicationContext: ServerApplicationContext, + { user }: { user: RawPractitioner }, +): Promise => { const barNumber = user.barNumber || (await applicationContext.barNumberGenerator.createBarNumber({ diff --git a/shared/src/proxies/practitioners/createPractitionerUserProxy.ts b/shared/src/proxies/practitioners/createPractitionerUserProxy.ts index a5890811a3b..456a77d4658 100644 --- a/shared/src/proxies/practitioners/createPractitionerUserProxy.ts +++ b/shared/src/proxies/practitioners/createPractitionerUserProxy.ts @@ -1,17 +1,9 @@ import { post } from '../requests'; -/** - * createPractitionerUserInteractor - * - * @param {object} applicationContext the application context - * @param {object} providers the providers object - * @param {string} providers.user the user data - * @returns {Promise} the created user data - */ export const createPractitionerUserInteractor = ( applicationContext, { user }, -) => { +): Promise<{ barNumber: string }> => { return post({ applicationContext, body: { user }, diff --git a/shared/src/business/useCases/practitioners/createPractitionerUserInteractor.test.ts b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts similarity index 55% rename from shared/src/business/useCases/practitioners/createPractitionerUserInteractor.test.ts rename to web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts index eec9a09372d..4a581b0146d 100644 --- a/shared/src/business/useCases/practitioners/createPractitionerUserInteractor.test.ts +++ b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts @@ -1,15 +1,21 @@ -import { ROLES, SERVICE_INDICATOR_TYPES } from '../../entities/EntityConstants'; +import { + ROLES, + SERVICE_INDICATOR_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; +import { RawPractitioner } from '@shared/business/entities/Practitioner'; import { UnauthorizedError } from '@web-api/errors/errors'; -import { applicationContext } from '../../test/createTestApplicationContext'; +import { admissionsClerkUser, petitionerUser } from '@shared/test/mockUsers'; +import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { createPractitionerUserInteractor } from './createPractitionerUserInteractor'; -describe('create practitioner user', () => { - const mockUser = { +describe('createPractitionerUserInteractor', () => { + const mockUser: RawPractitioner = { admissionsDate: '2019-03-01', admissionsStatus: 'Active', barNumber: 'AT5678', - birthYear: 2019, + birthYear: '2019', employer: 'Private', + entityName: 'Practitioner', firmName: 'GW Law Offices', firstName: 'bob', lastName: 'sagot', @@ -17,36 +23,19 @@ describe('create practitioner user', () => { originalBarState: 'IL', practitionerType: 'Attorney', role: ROLES.privatePractitioner, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_PAPER, userId: '07044afe-641b-4d75-a84f-0698870b7650', - } as any; - - let testUser; + }; beforeEach(() => { - testUser = { - role: ROLES.admissionsClerk, - userId: 'admissionsclerk', - }; - - applicationContext.environment.stage = 'local'; - applicationContext.getCurrentUser.mockImplementation(() => testUser); + applicationContext.getCurrentUser.mockReturnValue(admissionsClerkUser); applicationContext .getPersistenceGateway() - .createOrUpdatePractitionerUser.mockResolvedValue(mockUser); - }); - - it('creates the practitioner user', async () => { - const user = await createPractitionerUserInteractor(applicationContext, { - user: mockUser, - }); - expect(user).not.toBeUndefined(); + .createOrUpdatePractitionerUser.mockResolvedValue(({ user }) => user); }); - it('throws unauthorized for a non-internal user', async () => { - testUser = { - role: ROLES.petitioner, - userId: '6a2a8f95-0223-442e-8e55-5f094c6bca15', - }; + it('should throw an error when the user is unauthorized to create a practitioner user', async () => { + applicationContext.getCurrentUser.mockReturnValue(petitionerUser); await expect( createPractitionerUserInteractor(applicationContext, { @@ -55,6 +44,17 @@ describe('create practitioner user', () => { ).rejects.toThrow(UnauthorizedError); }); + it('should return the practitioner`s bar number', async () => { + const { barNumber } = await createPractitionerUserInteractor( + applicationContext, + { + user: mockUser, + }, + ); + + expect(barNumber).toEqual(mockUser.barNumber); + }); + it('should set practitioner.pendingEmail to practitioner.email and set practitioner.email to undefined', async () => { const mockEmail = 'testing@example.com'; @@ -68,7 +68,6 @@ describe('create practitioner user', () => { const mockUserCall = applicationContext.getPersistenceGateway().createOrUpdatePractitionerUser .mock.calls[0][0].user; - expect(mockUserCall.email).toBeUndefined(); expect(mockUserCall.pendingEmail).toEqual(mockEmail); expect(mockUserCall.serviceIndicator).toEqual( diff --git a/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.ts b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.ts new file mode 100644 index 00000000000..42bda73d028 --- /dev/null +++ b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.ts @@ -0,0 +1,35 @@ +import { + ROLE_PERMISSIONS, + isAuthorized, +} from '../../../../../shared/src/authorization/authorizationClientService'; +import { RawPractitioner } from '../../../../../shared/src/business/entities/Practitioner'; +import { ServerApplicationContext } from '@web-api/applicationContext'; +import { UnauthorizedError } from '@web-api/errors/errors'; +import { createPractitionerUser } from '../../../../../shared/src/business/utilities/createPractitionerUser'; + +export const createPractitionerUserInteractor = async ( + applicationContext: ServerApplicationContext, + { user }: { user: RawPractitioner }, +): Promise<{ barNumber: string }> => { + const requestUser = applicationContext.getCurrentUser(); + + if (!isAuthorized(requestUser, ROLE_PERMISSIONS.ADD_EDIT_PRACTITIONER_USER)) { + throw new UnauthorizedError('Unauthorized for creating practitioner user'); + } + + user.pendingEmail = user.email; + user.email = undefined; + + const practitioner = await createPractitionerUser(applicationContext, { + user, + }); + + const createdUser = await applicationContext + .getPersistenceGateway() + .createOrUpdatePractitionerUser({ + applicationContext, + user: practitioner, + }); + + return { barNumber: createdUser.barNumber }; +}; diff --git a/web-api/src/business/useCases/user/createUserInteractor.ts b/web-api/src/business/useCases/user/createUserInteractor.ts index 1739f3acd6c..ed1502de9ca 100644 --- a/web-api/src/business/useCases/user/createUserInteractor.ts +++ b/web-api/src/business/useCases/user/createUserInteractor.ts @@ -40,7 +40,9 @@ export const createUserInteractor = async ( user.role === ROLES.inactivePractitioner ) { userEntity = new Practitioner( - await createPractitionerUser({ applicationContext, user }), + await createPractitionerUser(applicationContext, { + user: user as RawPractitioner, + }), ); } else { if (user.barNumber === '') { diff --git a/web-api/src/getUseCases.ts b/web-api/src/getUseCases.ts index 2a67f325902..0919a758456 100644 --- a/web-api/src/getUseCases.ts +++ b/web-api/src/getUseCases.ts @@ -30,7 +30,7 @@ import { createCaseInteractor } from '../../shared/src/business/useCases/createC import { createCourtIssuedOrderPdfFromHtmlInteractor } from '../../shared/src/business/useCases/courtIssuedOrder/createCourtIssuedOrderPdfFromHtmlInteractor'; import { createMessageInteractor } from '../../shared/src/business/useCases/messages/createMessageInteractor'; import { createPractitionerDocumentInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerDocumentInteractor'; -import { createPractitionerUserInteractor } from '../../shared/src/business/useCases/practitioners/createPractitionerUserInteractor'; +import { createPractitionerUserInteractor } from './business/useCases/practitioner/createPractitionerUserInteractor'; import { createTrialSessionInteractor } from '../../shared/src/business/useCases/trialSessions/createTrialSessionInteractor'; import { createUserInteractor } from './business/useCases/user/createUserInteractor'; import { deleteCaseDeadlineInteractor } from '../../shared/src/business/useCases/caseDeadline/deleteCaseDeadlineInteractor'; diff --git a/web-client/src/presenter/actions/createPractitionerUserAction.test.ts b/web-client/src/presenter/actions/createPractitionerUserAction.test.ts index b48f7ca8f77..660122d8781 100644 --- a/web-client/src/presenter/actions/createPractitionerUserAction.test.ts +++ b/web-client/src/presenter/actions/createPractitionerUserAction.test.ts @@ -4,13 +4,10 @@ import { presenter } from '../presenter-mock'; import { runAction } from '@web-client/presenter/test.cerebral'; describe('createPractitionerUserAction', () => { - let successMock; - let errorMock; - - beforeAll(() => { - successMock = jest.fn(); - errorMock = jest.fn(); + const successMock = jest.fn(); + const errorMock = jest.fn(); + beforeEach(() => { presenter.providers.applicationContext = applicationContext; presenter.providers.path = { @@ -24,9 +21,6 @@ describe('createPractitionerUserAction', () => { modules: { presenter, }, - props: { - computedDate: '2019-03-01T21:40:46.415Z', - }, state: { form: { confirmEmail: 'something@example.com', @@ -42,7 +36,7 @@ describe('createPractitionerUserAction', () => { }); }); - it('should return path.success with a success message and practitioner information when the practitioner user was successfully created', async () => { + it('should return path.success with a success message and practitioner bar number when the practitioner user was successfully created', async () => { const mockPractitioner = { barNumber: 'AB1234', name: 'Donna Harking', @@ -56,9 +50,7 @@ describe('createPractitionerUserAction', () => { presenter, }, state: { - form: { - user: {}, - }, + form: {}, }, }); @@ -67,11 +59,10 @@ describe('createPractitionerUserAction', () => { message: 'Practitioner added.', }, barNumber: mockPractitioner.barNumber, - practitionerUser: mockPractitioner, }); }); - it('should return path.error when the practitioner to create is invalid', async () => { + it('should return path.error when an error occurred while creating the practitioner', async () => { applicationContext .getUseCases() .createPractitionerUserInteractor.mockImplementation(() => { @@ -83,13 +74,10 @@ describe('createPractitionerUserAction', () => { presenter, }, state: { - form: { - user: {}, - }, + form: {}, }, }); - expect(errorMock).toHaveBeenCalled(); expect(errorMock).toHaveBeenCalledWith({ alertError: { message: 'Please try again.', diff --git a/web-client/src/presenter/actions/createPractitionerUserAction.ts b/web-client/src/presenter/actions/createPractitionerUserAction.ts index 39d7bca666c..148b7a5021a 100644 --- a/web-client/src/presenter/actions/createPractitionerUserAction.ts +++ b/web-client/src/presenter/actions/createPractitionerUserAction.ts @@ -7,21 +7,18 @@ export const createPractitionerUserAction = async ({ }: ActionProps) => { const practitioner = get(state.form); - practitioner.confirmEmail = undefined; - try { - const practitionerUser = await applicationContext + const { barNumber } = await applicationContext .getUseCases() .createPractitionerUserInteractor(applicationContext, { - user: practitioner, + user: { ...practitioner, confirmEmail: undefined }, }); return path.success({ alertSuccess: { message: 'Practitioner added.', }, - barNumber: practitionerUser.barNumber, - practitionerUser, + barNumber, }); } catch (err) { return path.error({ From f648af9649bf8e67eb1e0f819d7cb1f0b94bee5d Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Tue, 12 Mar 2024 10:54:34 -0700 Subject: [PATCH 32/33] 10007: Fixing unit test --- .../practitioner/createPractitionerUserInteractor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts index 4a581b0146d..8fb1aaf29bd 100644 --- a/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts +++ b/web-api/src/business/useCases/practitioner/createPractitionerUserInteractor.test.ts @@ -31,7 +31,7 @@ describe('createPractitionerUserInteractor', () => { applicationContext.getCurrentUser.mockReturnValue(admissionsClerkUser); applicationContext .getPersistenceGateway() - .createOrUpdatePractitionerUser.mockResolvedValue(({ user }) => user); + .createOrUpdatePractitionerUser.mockResolvedValue(mockUser); }); it('should throw an error when the user is unauthorized to create a practitioner user', async () => { From 210eb7c9bc8824e80811657b464afb6602b8cba6 Mon Sep 17 00:00:00 2001 From: Rachel Schneiderman Date: Wed, 13 Mar 2024 06:49:44 -0700 Subject: [PATCH 33/33] 10007: Fixing unit test --- .../utilities/createPractitionerUser.test.ts | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/shared/src/business/utilities/createPractitionerUser.test.ts b/shared/src/business/utilities/createPractitionerUser.test.ts index 8e8f205ba13..d1b39bcae97 100644 --- a/shared/src/business/utilities/createPractitionerUser.test.ts +++ b/shared/src/business/utilities/createPractitionerUser.test.ts @@ -1,52 +1,58 @@ -import { ROLES } from '../entities/EntityConstants'; +import { ROLES, SERVICE_INDICATOR_TYPES } from '../entities/EntityConstants'; +import { RawPractitioner } from '@shared/business/entities/Practitioner'; import { applicationContext } from '../test/createTestApplicationContext'; import { createPractitionerUser } from './createPractitionerUser'; describe('createPractitionerUser', () => { - const mockAdmissionsDate = '1876-02-19'; - it('should generate a bar number and userId when they are not provided', async () => { - const mockUser = { - admissionsDate: mockAdmissionsDate, - admissionsStatus: 'Active', - birthYear: '1993', - employer: 'DOJ', - firstName: 'Test', - lastName: 'IRSPractitioner', - originalBarState: 'CA', - practitionerType: 'Attorney', - role: ROLES.irsPractitioner, - }; - - const result = await createPractitionerUser({ + const { barNumber, userId } = await createPractitionerUser( applicationContext, - user: mockUser, - }); + { + user: { + admissionsDate: '1876-02-19', + admissionsStatus: 'Active', + barNumber: undefined, + birthYear: '1993', + employer: 'DOJ', + entityName: 'Practitioner', + firstName: 'Test', + lastName: 'IRSPractitioner', + name: 'Test IRSPractitioner', + originalBarState: 'CA', + practitionerType: 'Attorney', + role: ROLES.irsPractitioner, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_NONE, + userId: undefined, + } as unknown as RawPractitioner, + }, + ); - expect(result.barNumber).not.toBeUndefined(); - expect(result.userId).not.toBeUndefined(); + expect(barNumber).toBeDefined(); + expect(userId).toBeDefined(); }); it('should use provided bar number when it is provided', async () => { - const mockUser = { - admissionsDate: mockAdmissionsDate, - admissionsStatus: 'Active', - barNumber: '1', - birthYear: '1993', - employer: 'DOJ', - firstName: 'Test', - lastName: 'IRSPractitioner', - originalBarState: 'CA', - practitionerType: 'Attorney', - role: ROLES.irsPractitioner, - }; + const mockBarNumber = 'tp8172'; - const result = await createPractitionerUser({ - applicationContext, - user: mockUser, + const { barNumber } = await createPractitionerUser(applicationContext, { + user: { + admissionsDate: '1876-02-19', + admissionsStatus: 'Active', + barNumber: mockBarNumber, + birthYear: '1993', + employer: 'DOJ', + entityName: 'Practitioner', + firstName: 'Test', + lastName: 'IRSPractitioner', + name: 'Test IRSPractitioner', + originalBarState: 'CA', + practitionerType: 'Attorney', + role: ROLES.irsPractitioner, + serviceIndicator: SERVICE_INDICATOR_TYPES.SI_NONE, + userId: '70e686e6-3ab3-49f1-8d5b-edf1596d86ac', + }, }); - expect(result.barNumber).toBe('1'); - expect(result.userId).not.toBeUndefined(); + expect(barNumber).toBe(mockBarNumber); }); });