Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-1888- check email in provisioning #4955

Merged
merged 13 commits into from
Apr 26, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable/email-already-exists.loggable';

describe('EmailAlreadyExistsLoggableException', () => {
describe('getLogMessage', () => {
const setup = () => {
const email = 'mock-email';
const systemId = '123';
const schoolId = '456';
const externalId = '789';

const exception = new EmailAlreadyExistsLoggable(email, systemId, schoolId, externalId);

return {
exception,
email,
systemId,
schoolId,
externalId,
};
};

it('should return the correct log message', () => {
const { exception, email, systemId, schoolId, externalId } = setup();

const message = exception.getLogMessage();

expect(message).toEqual({
type: 'EMAIL_ALREADY_EXISTS',
message: 'The Email to be provisioned already exists.',
stack: expect.any(String),
data: {
email,
systemId,
schoolId,
externalId,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HttpStatus } from '@nestjs/common';
import { BusinessError } from '@shared/common';
import { EntityId } from '@shared/domain/types';
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class EmailAlreadyExistsLoggable extends BusinessError implements Loggable {
constructor(
private readonly email: string,
private readonly systemId: EntityId,
private readonly schoolId?: string,
private readonly externalId?: string
) {
super(
{
type: 'EMAIL_ALREADY_EXISTS',
title: 'Email already Exists',
defaultMessage: 'The Email to be provisioned already exists.',
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
type: this.type,
message: this.message,
stack: this.stack,
data: {
email: this.email,
systemId: this.systemId,
schoolId: this.schoolId,
externalId: this.externalId,
},
};
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/provisioning/loggable/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './user-for-group-not-found.loggable';
export * from './school-for-group-not-found.loggable';
export * from './group-role-unknown.loggable';
export { EmailAlreadyExistsLoggable } from './email-already-exists.loggable';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { AccountService, AccountSave } from '@modules/account';
import { AccountSave, AccountService } from '@modules/account';
import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable';
import { RoleService } from '@modules/role';
import { RoleDto } from '@modules/role/service/dto/role.dto';
import { UserService } from '@modules/user';
Expand Down Expand Up @@ -71,6 +72,19 @@ describe(SchulconnexUserProvisioningService.name, () => {
},
'userId'
);
const otherUserWithSameEmail: UserDO = userDoFactory
.withRoles([{ id: 'existingRoleId', name: RoleName.USER }])
.buildWithId(
{
firstName: 'OtherFirstName',
lastName: 'OtherLastName',
email: 'email',
schoolId: 'OtherSchoolId',
externalId: 'OtherUserId',
birthday: new Date('2023-11-15'),
},
'otherId'
);
const savedUser: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).buildWithId(
{
firstName: 'firstName',
Expand Down Expand Up @@ -109,6 +123,7 @@ describe(SchulconnexUserProvisioningService.name, () => {

return {
existingUser,
otherUserWithSameEmail,
savedUser,
externalUser,
userRole,
Expand All @@ -123,6 +138,7 @@ describe(SchulconnexUserProvisioningService.name, () => {
const { externalUser, schoolId, savedUser, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(null);
userService.findByEmail.mockResolvedValue([]);

await service.provisionExternalUser(externalUser, systemId, schoolId);

Expand All @@ -133,6 +149,7 @@ describe(SchulconnexUserProvisioningService.name, () => {
const { externalUser, schoolId, savedUser, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(null);
userService.findByEmail.mockResolvedValue([]);

const result: UserDO = await service.provisionExternalUser(externalUser, systemId, schoolId);

Expand All @@ -149,6 +166,7 @@ describe(SchulconnexUserProvisioningService.name, () => {
} as AccountSave;

userService.findByExternalId.mockResolvedValue(null);
userService.findByEmail.mockResolvedValue([]);

await service.provisionExternalUser(externalUser, systemId, schoolId);

Expand All @@ -160,19 +178,34 @@ describe(SchulconnexUserProvisioningService.name, () => {
const { externalUser } = setupUser();

userService.findByExternalId.mockResolvedValue(null);
userService.findByEmail.mockResolvedValue([]);

const promise: Promise<UserDO> = service.provisionExternalUser(externalUser, 'systemId', undefined);

await expect(promise).rejects.toThrow(UnprocessableEntityException);
});
});

describe('when the external user has an email, that already exists in SVS', () => {
it('should throw EmailAlreadyExistsLoggable', async () => {
const { externalUser, systemId, schoolId, otherUserWithSameEmail } = setupUser();

userService.findByExternalId.mockResolvedValue(null);
userService.findByEmail.mockResolvedValue([otherUserWithSameEmail]);

const promise: Promise<UserDO> = service.provisionExternalUser(externalUser, systemId, schoolId);

await expect(promise).rejects.toThrow(EmailAlreadyExistsLoggable);
});
});
});

describe('when the user already exists', () => {
it('should call the user service to save the user', async () => {
const { externalUser, schoolId, existingUser, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(existingUser);
userService.findByEmail.mockResolvedValue([existingUser]);

await service.provisionExternalUser(externalUser, systemId, schoolId);

Expand All @@ -183,6 +216,7 @@ describe(SchulconnexUserProvisioningService.name, () => {
const { externalUser, schoolId, existingUser, savedUser, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(existingUser);
userService.findByEmail.mockResolvedValue([existingUser]);

const result: UserDO = await service.provisionExternalUser(externalUser, systemId, schoolId);

Expand All @@ -193,11 +227,25 @@ describe(SchulconnexUserProvisioningService.name, () => {
const { externalUser, schoolId, systemId, existingUser } = setupUser();

userService.findByExternalId.mockResolvedValue(existingUser);
userService.findByEmail.mockResolvedValue([existingUser]);

await service.provisionExternalUser(externalUser, systemId, schoolId);

expect(accountService.saveWithValidation).not.toHaveBeenCalled();
});

describe('when the external user has an email, that already exists ijn SVS', () => {
it('should throw EmailAlreadyExistsLoggable', async () => {
const { externalUser, systemId, schoolId, otherUserWithSameEmail, existingUser } = setupUser();

userService.findByExternalId.mockResolvedValue(existingUser);
userService.findByEmail.mockResolvedValue([otherUserWithSameEmail]);

const promise: Promise<UserDO> = service.provisionExternalUser(externalUser, systemId, schoolId);

await expect(promise).rejects.toThrow(EmailAlreadyExistsLoggable);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccountService, AccountSave } from '@modules/account';
import { AccountSave, AccountService } from '@modules/account';
import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable';
import { RoleDto, RoleService } from '@modules/role';
import { UserService } from '@modules/user';
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
Expand All @@ -20,13 +21,17 @@ export class SchulconnexUserProvisioningService {
systemId: EntityId,
schoolId?: string
): Promise<UserDO> {
const existingUser: UserDO | null = await this.userService.findByExternalId(externalUser.externalId, systemId);
if (externalUser.email) {
await this.checkUniqueEmail(externalUser.email, systemId, schoolId, existingUser?.externalId);
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
}

IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
let roleRefs: RoleReference[] | undefined;
if (externalUser.roles) {
const roles: RoleDto[] = await this.roleService.findByNames(externalUser.roles);
roleRefs = roles.map((role: RoleDto): RoleReference => new RoleReference({ id: role.id || '', name: role.name }));
}

const existingUser: UserDO | null = await this.userService.findByExternalId(externalUser.externalId, systemId);
let user: UserDO;
let createNewAccount = false;
if (existingUser) {
Expand Down Expand Up @@ -70,4 +75,18 @@ export class SchulconnexUserProvisioningService {

return savedUser;
}

private async checkUniqueEmail(
email: string,
systemId: string,
schoolId?: string,
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
externalId?: string
): Promise<void> {
const foundUsers: UserDO[] = await this.userService.findByEmail(email);
const unmatchedUsers: UserDO[] = foundUsers.filter((user: UserDO) => user.externalId !== externalId);
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved

if (unmatchedUsers.length || (!externalId && foundUsers.length)) {
throw new EmailAlreadyExistsLoggable(email, systemId, schoolId, externalId);
}
}
}
Loading