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,32 @@
import { EmailAlreadyExistsLoggable } from '@modules/provisioning/loggable/email-already-exists.loggable';

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

const loggable = new EmailAlreadyExistsLoggable(email, externalId);

return {
loggable,
email,
externalId,
};
};

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

const message = loggable.getLogMessage();

expect(message).toEqual({
message: 'The Email to be provisioned already exists.',
data: {
email,
externalId,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class EmailAlreadyExistsLoggable implements Loggable {
constructor(private readonly email: string, private readonly externalId?: string) {}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
message: 'The Email to be provisioned already exists.',
data: {
email: this.email,
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,5 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { AccountService, AccountSave } from '@modules/account';
import { AccountSave, AccountService } from '@modules/account';
import { RoleService } from '@modules/role';
import { RoleDto } from '@modules/role/service/dto/role.dto';
import { UserService } from '@modules/user';
Expand All @@ -8,6 +8,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { UserDO } from '@shared/domain/domainobject';
import { RoleName } from '@shared/domain/interface';
import { userDoFactory } from '@shared/testing';
import { Logger } from '@src/core/logger';
import CryptoJS from 'crypto-js';
import { ExternalUserDto } from '../../../dto';
import { SchulconnexUserProvisioningService } from './schulconnex-user-provisioning.service';
Expand All @@ -21,6 +22,7 @@ describe(SchulconnexUserProvisioningService.name, () => {
let userService: DeepMocked<UserService>;
let roleService: DeepMocked<RoleService>;
let accountService: DeepMocked<AccountService>;
let logger: DeepMocked<Logger>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -38,13 +40,18 @@ describe(SchulconnexUserProvisioningService.name, () => {
provide: AccountService,
useValue: createMock<AccountService>(),
},
{
provide: Logger,
useValue: createMock<Logger>(),
},
],
}).compile();

service = module.get(SchulconnexUserProvisioningService);
userService = module.get(UserService);
roleService = module.get(RoleService);
accountService = module.get(AccountService);
logger = module.get(Logger);
});

afterAll(async () => {
Expand Down Expand Up @@ -119,10 +126,21 @@ describe(SchulconnexUserProvisioningService.name, () => {
};

describe('when the user does not exist yet', () => {
it('should call user service to check uniqueness of email', async () => {
const { externalUser, schoolId, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(null);

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

expect(userService.isEmailFromExternalSourceUnique).toHaveBeenCalledWith(externalUser.email, undefined);
});

it('should call the user service to save the user', async () => {
const { externalUser, schoolId, savedUser, systemId } = setupUser();

userService.findByExternalId.mockResolvedValue(null);
userService.isEmailFromExternalSourceUnique.mockResolvedValue(true);

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

Expand Down Expand Up @@ -166,9 +184,39 @@ describe(SchulconnexUserProvisioningService.name, () => {
await expect(promise).rejects.toThrow(UnprocessableEntityException);
});
});

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

userService.findByExternalId.mockResolvedValue(null);
userService.isEmailFromExternalSourceUnique.mockResolvedValue(false);

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

expect(logger.warning).toHaveBeenCalledWith({
email: externalUser.email,
externalId: externalUser.externalId,
});
});
});
});

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

userService.findByExternalId.mockResolvedValue(existingUser);
userService.isEmailFromExternalSourceUnique.mockResolvedValue(true);

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

expect(userService.isEmailFromExternalSourceUnique).toHaveBeenCalledWith(
externalUser.email,
existingUser.externalId
);
});

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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';
import { RoleReference, UserDO } from '@shared/domain/domainobject';
import { EntityId } from '@shared/domain/types';
import { Logger } from '@src/core/logger';
import CryptoJS from 'crypto-js';
import { ExternalUserDto } from '../../../dto';

Expand All @@ -12,28 +14,48 @@ export class SchulconnexUserProvisioningService {
constructor(
private readonly userService: UserService,
private readonly roleService: RoleService,
private readonly accountService: AccountService
private readonly accountService: AccountService,
private readonly logger: Logger
) {}

public async provisionExternalUser(
externalUser: ExternalUserDto,
systemId: EntityId,
schoolId?: string
): Promise<UserDO> {
const existingUser: UserDO | null = await this.userService.findByExternalId(externalUser.externalId, systemId);

let isUniqueEmail = true;
if (externalUser.email) {
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
isUniqueEmail = await this.userService.isEmailFromExternalSourceUnique(
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
externalUser.email,
existingUser?.externalId
);

if (!isUniqueEmail) {
this.logger.warning(new EmailAlreadyExistsLoggable(externalUser.email, externalUser.externalId));
}
}

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) {
user = existingUser;

if (!isUniqueEmail) {
user.email = existingUser.email;
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
} else {
user.email = externalUser.email ?? existingUser.email;
}

user.firstName = externalUser.firstName ?? existingUser.firstName;
user.lastName = externalUser.lastName ?? existingUser.lastName;
user.email = externalUser.email ?? existingUser.email;
user.roles = roleRefs ?? existingUser.roles;
user.schoolId = schoolId ?? existingUser.schoolId;
user.birthday = externalUser.birthday ?? existingUser.birthday;
Expand All @@ -55,6 +77,10 @@ export class SchulconnexUserProvisioningService {
schoolId,
birthday: externalUser.birthday,
});

if (!isUniqueEmail) {
user.email = '';
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
}
}

const savedUser: UserDO = await this.userService.save(user);
Expand Down
Loading
Loading