Skip to content

Commit

Permalink
refactor(api): log email change in audit logger
Browse files Browse the repository at this point in the history
  • Loading branch information
bpetetot committed Sep 23, 2024
1 parent 0adc492 commit f88e7d7
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 182 deletions.
4 changes: 3 additions & 1 deletion api/src/identity-access-management/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -45,8 +46,9 @@ const repositories = {
authenticationMethodRepository,
campaignParticipationRepository,
campaignRepository,
emailValidationDemandRepository,
campaignToJoinRepository: campaignRepositories.campaignToJoinRepository,
emailValidationDemandRepository,
eventLoggingJobRepository,
oidcProviderRepository,
organizationLearnerRepository,
refreshTokenRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -29,6 +36,18 @@ const updateUserEmailWithValidation = async function ({ code, userId, userEmailR
},
});

// Currently only used in Pix App, which is why app name is hard-coded for the audit log.
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 };
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 = '[email protected]';
const user = databaseBuilder.factory.buildUser({ email: '[email protected]' });
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 protected]', newEmail: '[email protected]' },
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 = '[email protected]';
const user = databaseBuilder.factory.buildUser({ email: '[email protected]' });
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 = '[email protected]';
const user = databaseBuilder.factory.buildUser({ email: '[email protected]' });
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 = '[email protected]';
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 = '[email protected]';
const user = databaseBuilder.factory.buildUser({ email: '[email protected]' });
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);
});
});
});

This file was deleted.

0 comments on commit f88e7d7

Please sign in to comment.