From ce1843f07c48746f1c666d2d8237310f016224e0 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Wed, 18 Sep 2024 15:12:44 +0200 Subject: [PATCH] refactor(api): log email change in audit logger --- .../domain/usecases/index.js | 4 +- ...date-user-email-with-validation.usecase.js | 20 +- ...user-email-with-validation.usecase.test.js | 143 ++++++++++++++ ...user-email-with-validation.usecase.test.js | 180 ------------------ 4 files changed, 165 insertions(+), 182 deletions(-) create mode 100644 api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js delete mode 100644 api/tests/identity-access-management/unit/domain/usecases/update-user-email-with-validation.usecase.test.js diff --git a/api/src/identity-access-management/domain/usecases/index.js b/api/src/identity-access-management/domain/usecases/index.js index 2d05633b486..38cd89a6812 100644 --- a/api/src/identity-access-management/domain/usecases/index.js +++ b/api/src/identity-access-management/domain/usecases/index.js @@ -24,6 +24,7 @@ import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/ import { accountRecoveryDemandRepository } from '../../infrastructure/repositories/account-recovery-demand.repository.js'; import * as authenticationMethodRepository from '../../infrastructure/repositories/authentication-method.repository.js'; import { emailValidationDemandRepository } from '../../infrastructure/repositories/email-validation-demand.repository.js'; +import { eventLoggingJobRepository } from '../../infrastructure/repositories/jobs/event-logging-job.repository.js'; import { garAnonymizedBatchEventsLoggingJobRepository } from '../../infrastructure/repositories/jobs/gar-anonymized-batch-events-logging-job-repository.js'; import { oidcProviderRepository } from '../../infrastructure/repositories/oidc-provider-repository.js'; import { refreshTokenRepository } from '../../infrastructure/repositories/refresh-token.repository.js'; @@ -45,8 +46,9 @@ const repositories = { authenticationMethodRepository, campaignParticipationRepository, campaignRepository, - emailValidationDemandRepository, campaignToJoinRepository: campaignRepositories.campaignToJoinRepository, + emailValidationDemandRepository, + eventLoggingJobRepository, oidcProviderRepository, organizationLearnerRepository, refreshTokenRepository, diff --git a/api/src/identity-access-management/domain/usecases/update-user-email-with-validation.usecase.js b/api/src/identity-access-management/domain/usecases/update-user-email-with-validation.usecase.js index 275fc14cba7..f2b4a05e68c 100644 --- a/api/src/identity-access-management/domain/usecases/update-user-email-with-validation.usecase.js +++ b/api/src/identity-access-management/domain/usecases/update-user-email-with-validation.usecase.js @@ -3,8 +3,15 @@ import { InvalidVerificationCodeError, UserNotAuthorizedToUpdateEmailError, } from '../../../shared/domain/errors.js'; +import { EventLoggingJob } from '../models/jobs/EventLoggingJob.js'; -const updateUserEmailWithValidation = async function ({ code, userId, userEmailRepository, userRepository }) { +const updateUserEmailWithValidation = async function ({ + code, + userId, + userEmailRepository, + userRepository, + eventLoggingJobRepository, +}) { const user = await userRepository.get(userId); if (!user.email) { throw new UserNotAuthorizedToUpdateEmailError(); @@ -29,6 +36,17 @@ const updateUserEmailWithValidation = async function ({ code, userId, userEmailR }, }); + await eventLoggingJobRepository.performAsync( + new EventLoggingJob({ + client: 'PIX_APP', + action: 'EMAIL_CHANGED', + role: 'USER', + userId: user.id, + targetUserId: user.id, + data: { oldEmail: user.email, newEmail: emailModificationDemand.newEmail }, + }), + ); + return { email: emailModificationDemand.newEmail }; }; diff --git a/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js b/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js new file mode 100644 index 00000000000..8788b024e71 --- /dev/null +++ b/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js @@ -0,0 +1,143 @@ +import { expect } from 'chai'; + +import { EventLoggingJob } from '../../../../../src/identity-access-management/domain/models/jobs/EventLoggingJob.js'; +import { usecases } from '../../../../../src/identity-access-management/domain/usecases/index.js'; +import { userEmailRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/user-email.repository.js'; +import { + AlreadyRegisteredEmailError, + EmailModificationDemandNotFoundOrExpiredError, + InvalidVerificationCodeError, + UserNotAuthorizedToUpdateEmailError, +} from '../../../../../src/shared/domain/errors.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { catchErr, databaseBuilder, knex, sinon } from '../../../../test-helper.js'; + +const verifyEmailTemporaryStorage = temporaryStorage.withPrefix('verify-email:'); + +describe('Integration | Identity Access Management | Domain | UseCase | updateUserEmailWithValidation', function () { + let clock; + const now = new Date('2024-12-25'); + + beforeEach(function () { + verifyEmailTemporaryStorage.flushAll(); + clock = sinon.useFakeTimers({ now, toFake: ['Date'] }); + }); + + afterEach(async function () { + clock.restore(); + }); + + it('updates the user email checking the verification code', async function () { + // given + const code = 123; + const newEmail = 'new.email@example.net'; + const user = databaseBuilder.factory.buildUser({ email: 'email@example.net' }); + await databaseBuilder.commit(); + await userEmailRepository.saveEmailModificationDemand({ userId: user.id, code, newEmail }); + + // when + const result = await usecases.updateUserEmailWithValidation({ userId: user.id, code, newEmail }); + + // then + expect(result.email).to.equal(newEmail); + + const updatedUser = await knex('users').where({ id: user.id }).first(); + expect(updatedUser.email).to.equal(newEmail); + expect(updatedUser.emailConfirmedAt).to.not.be.null; + + await expect(EventLoggingJob.name).to.have.been.performed.withJobPayload({ + client: 'PIX_APP', + action: 'EMAIL_CHANGED', + role: 'USER', + userId: user.id, + targetUserId: user.id, + data: { oldEmail: 'email@example.net', newEmail: 'new.email@example.net' }, + occurredAt: '2024-12-25T00:00:00.000Z', + }); + }); + + context('when the verification code is invalid', function () { + it('throws an error', async function () { + // given + const code = 123; + const invalidCode = 456; + const newEmail = 'new.email@example.net'; + const user = databaseBuilder.factory.buildUser({ email: 'email@example.net' }); + await databaseBuilder.commit(); + await userEmailRepository.saveEmailModificationDemand({ userId: user.id, code, newEmail }); + + // when + const error = await catchErr(usecases.updateUserEmailWithValidation)({ + userId: user.id, + code: invalidCode, + newEmail, + }); + + // then + expect(error).to.be.instanceOf(InvalidVerificationCodeError); + }); + }); + + context('when the email modification demand is not found or expired', function () { + it('throws an error', async function () { + // given + const code = 123; + const newEmail = 'new.email@example.net'; + const user = databaseBuilder.factory.buildUser({ email: 'email@example.net' }); + await databaseBuilder.commit(); + + // when + const error = await catchErr(usecases.updateUserEmailWithValidation)({ + userId: user.id, + code, + newEmail, + }); + + // then + expect(error).to.be.instanceOf(EmailModificationDemandNotFoundOrExpiredError); + }); + }); + + context('when the user has no email', function () { + it('throws an error', async function () { + // given + const code = 123; + const newEmail = 'new.email@example.net'; + const user = databaseBuilder.factory.buildUser({ email: null }); + await databaseBuilder.commit(); + await userEmailRepository.saveEmailModificationDemand({ userId: user.id, code, newEmail }); + + // when + const error = await catchErr(usecases.updateUserEmailWithValidation)({ + userId: user.id, + code, + newEmail, + }); + + // then + expect(error).to.be.instanceOf(UserNotAuthorizedToUpdateEmailError); + }); + }); + + context('when the new email is already registered for an other user', function () { + it('throws an error', async function () { + // given + const code = 123; + const alreadyExistEmail = 'already.exist.email@example.net'; + const user = databaseBuilder.factory.buildUser({ email: 'email@example.net' }); + databaseBuilder.factory.buildUser({ email: alreadyExistEmail }); + await databaseBuilder.commit(); + await userEmailRepository.saveEmailModificationDemand({ userId: user.id, code, newEmail: alreadyExistEmail }); + + // when + const error = await catchErr(usecases.updateUserEmailWithValidation)({ + userId: user.id, + code, + newEmail: alreadyExistEmail, + }); + + // then + expect(error).to.be.instanceOf(AlreadyRegisteredEmailError); + }); + }); +}); diff --git a/api/tests/identity-access-management/unit/domain/usecases/update-user-email-with-validation.usecase.test.js b/api/tests/identity-access-management/unit/domain/usecases/update-user-email-with-validation.usecase.test.js deleted file mode 100644 index 487c810cd19..00000000000 --- a/api/tests/identity-access-management/unit/domain/usecases/update-user-email-with-validation.usecase.test.js +++ /dev/null @@ -1,180 +0,0 @@ -import { EmailModificationDemand } from '../../../../../src/identity-access-management/domain/models/EmailModificationDemand.js'; -import { updateUserEmailWithValidation } from '../../../../../src/identity-access-management/domain/usecases/update-user-email-with-validation.usecase.js'; -import { - AlreadyRegisteredEmailError, - EmailModificationDemandNotFoundOrExpiredError, - InvalidVerificationCodeError, -} from '../../../../../src/shared/domain/errors.js'; -import { UserNotAuthorizedToUpdateEmailError } from '../../../../../src/shared/domain/errors.js'; -import { catchErr, domainBuilder, expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Identity Access Management | Domain | UseCase | update-user-email-with-validation', function () { - let userEmailRepository; - let userRepository; - let clock; - - beforeEach(function () { - userEmailRepository = { - getEmailModificationDemandByUserId: sinon.stub(), - }; - userRepository = { - checkIfEmailIsAvailable: sinon.stub(), - get: sinon.stub(), - updateWithEmailConfirmed: sinon.stub(), - }; - }); - - it('should update email and set date for confirmed email', async function () { - // given - const userId = domainBuilder.buildUser().id; - const email = 'oldEmail@example.net'; - const newEmail = 'new_email@example.net'; - const code = '999999'; - const emailModificationDemand = new EmailModificationDemand({ - code, - newEmail, - }); - - const now = new Date(); - clock = sinon.useFakeTimers({ now, toFake: ['Date'] }); - - userRepository.get.withArgs(userId).resolves({ email }); - userEmailRepository.getEmailModificationDemandByUserId.withArgs(userId).resolves(emailModificationDemand); - userRepository.checkIfEmailIsAvailable.withArgs(newEmail).resolves(); - - // when - await updateUserEmailWithValidation({ - userId, - code, - userEmailRepository, - userRepository, - }); - - // then - expect(userRepository.updateWithEmailConfirmed).to.have.been.calledWithExactly({ - id: userId, - userAttributes: { email: newEmail, emailConfirmedAt: now }, - }); - clock.restore(); - }); - - it('should get email modification demand in temporary storage', async function () { - // given - const userId = domainBuilder.buildUser().id; - const email = 'oldEmail@example.net'; - const newEmail = 'new_email@example.net'; - const code = '999999'; - const emailModificationDemand = new EmailModificationDemand({ - code, - newEmail, - }); - - userRepository.get.withArgs(userId).resolves({ email }); - userEmailRepository.getEmailModificationDemandByUserId.withArgs(userId).resolves(emailModificationDemand); - - // when - await updateUserEmailWithValidation({ - userId, - code, - userEmailRepository, - userRepository, - }); - - // then - expect(userEmailRepository.getEmailModificationDemandByUserId).to.have.been.calledWithExactly(userId); - }); - - it('should throw UserNotAuthorizedToUpdateEmailError if user does not have an email', async function () { - // given - userRepository.get.resolves({}); - const userId = 1; - const code = '999999'; - - // when - const error = await catchErr(updateUserEmailWithValidation)({ - userId, - code, - userRepository, - userEmailRepository, - }); - - // then - expect(error).to.be.an.instanceOf(UserNotAuthorizedToUpdateEmailError); - }); - - it('should throw AlreadyRegisteredEmailError if email already exists', async function () { - // given - const userId = domainBuilder.buildUser().id; - const email = 'oldEmail@example.net'; - const newEmail = 'new_email@example.net'; - const code = '999999'; - const emailModificationDemand = new EmailModificationDemand({ - code, - newEmail, - }); - - userRepository.get.withArgs(userId).resolves({ email }); - userEmailRepository.getEmailModificationDemandByUserId.withArgs(userId).resolves(emailModificationDemand); - userRepository.checkIfEmailIsAvailable.withArgs(newEmail).rejects(new AlreadyRegisteredEmailError()); - - // when - const error = await catchErr(updateUserEmailWithValidation)({ - userId, - code, - userEmailRepository, - userRepository, - }); - - // then - expect(error).to.be.an.instanceOf(AlreadyRegisteredEmailError); - }); - - it('should throw InvalidVerificationCodeError if the code send does not match with then code saved in temporary storage', async function () { - // given - const userId = domainBuilder.buildUser().id; - const email = 'oldEmail@example.net'; - const newEmail = 'new_email@example.net'; - const code = '999999'; - const anotherCode = '444444'; - const emailModificationDemand = new EmailModificationDemand({ - code: anotherCode, - newEmail, - }); - - userRepository.get.withArgs(userId).resolves({ email }); - userEmailRepository.getEmailModificationDemandByUserId.withArgs(userId).resolves(emailModificationDemand); - - // when - const error = await catchErr(updateUserEmailWithValidation)({ - userId, - code, - userEmailRepository, - userRepository, - }); - - // then - expect(error).to.be.an.instanceOf(InvalidVerificationCodeError); - }); - - it('should throw EmailModificationDemandNotFoundOrExpiredError if no email modification demand match or is expired', async function () { - // given - const userId = domainBuilder.buildUser().id; - const anotherUserId = domainBuilder.buildUser().id; - const email = 'oldEmail@example.net'; - const code = '999999'; - - userRepository.get.withArgs(userId).resolves({ email }); - userEmailRepository.getEmailModificationDemandByUserId.withArgs(anotherUserId).resolves(null); - - // when - const error = await catchErr(updateUserEmailWithValidation)({ - userId, - code, - userEmailRepository, - userRepository, - }); - - // then - expect(error).to.be.an.instanceOf(EmailModificationDemandNotFoundOrExpiredError); - }); -});