user updated message handler

This commit is contained in:
sbriat 2023-07-11 15:16:11 +02:00
parent 487ae9c38e
commit 85746fde48
10 changed files with 196 additions and 23 deletions

View File

@ -24,6 +24,16 @@ import {
uri: configService.get<string>('MESSAGE_BROKER_URI'), uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'), exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
name: 'auth', name: 'auth',
handlers: {
userUpdated: {
routingKey: 'user.updated',
queue: 'auth-user-updated',
},
userDeleted: {
routingKey: 'user.deleted',
queue: 'auth-user-deleted',
},
},
}), }),
}), }),
// AutomapperModule.forRoot({ strategyInitializer: classes() }), // AutomapperModule.forRoot({ strategyInitializer: classes() }),

View File

@ -39,8 +39,6 @@ export class AuthenticationMapper
})), })),
} }
: undefined, : undefined,
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
}; };
return record; return record;
}; };

View File

@ -25,6 +25,7 @@ import { UpdatePasswordGrpcController } from './interface/grpc-controllers/updat
import { UpdatePasswordService } from './core/application/commands/update-password/update-password.service'; import { UpdatePasswordService } from './core/application/commands/update-password/update-password.service';
import { ValidateAuthenticationGrpcController } from './interface/grpc-controllers/validate-authentication.grpc.controller'; import { ValidateAuthenticationGrpcController } from './interface/grpc-controllers/validate-authentication.grpc.controller';
import { ValidateAuthenticationQueryHandler } from './core/application/queries/validate-authentication/validate-authentication.query-handler'; import { ValidateAuthenticationQueryHandler } from './core/application/queries/validate-authentication/validate-authentication.query-handler';
import { UserUpdatedMessageHandler } from './interface/message-handlers/user-updated.message-handler';
const grpcControllers = [ const grpcControllers = [
CreateAuthenticationGrpcController, CreateAuthenticationGrpcController,
@ -36,6 +37,8 @@ const grpcControllers = [
ValidateAuthenticationGrpcController, ValidateAuthenticationGrpcController,
]; ];
const messageHandlers = [UserUpdatedMessageHandler];
const commandHandlers: Provider[] = [ const commandHandlers: Provider[] = [
CreateAuthenticationService, CreateAuthenticationService,
DeleteAuthenticationService, DeleteAuthenticationService,
@ -73,6 +76,7 @@ const orms: Provider[] = [PrismaService];
imports: [CqrsModule], imports: [CqrsModule],
controllers: [...grpcControllers], controllers: [...grpcControllers],
providers: [ providers: [
...messageHandlers,
...commandHandlers, ...commandHandlers,
...queryHandlers, ...queryHandlers,
...mappers, ...mappers,

View File

@ -11,15 +11,15 @@ import { PrismaService } from './prisma.service';
import { AuthenticationMapper } from '../authentication.mapper'; import { AuthenticationMapper } from '../authentication.mapper';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens'; import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
export type AuthenticationBaseModel = { type AuthenticationBaseModel = {
uuid: string; uuid: string;
password: string; password: string;
createdAt: Date;
updatedAt: Date;
}; };
export type AuthenticationReadModel = AuthenticationBaseModel & { export type AuthenticationReadModel = AuthenticationBaseModel & {
usernames: UsernameModel[]; usernames: UsernameModel[];
createdAt: Date;
updatedAt: Date;
}; };
export type AuthenticationWriteModel = AuthenticationBaseModel & { export type AuthenticationWriteModel = AuthenticationBaseModel & {

View File

@ -12,20 +12,29 @@ import { UsernameRepositoryPort } from '../core/application/ports/username.repos
import { UsernameMapper } from '../username.mapper'; import { UsernameMapper } from '../username.mapper';
import { Type } from '../core/domain/username.types'; import { Type } from '../core/domain/username.types';
export type UsernameModel = { type UsernameBaseModel = {
username: string; username: string;
authUuid: string; authUuid: string;
type: string; type: string;
createdAt: Date;
updatedAt: Date;
}; };
export type UsernameReadModel = UsernameBaseModel & {
createdAt?: Date;
updatedAt?: Date;
};
export type UsernameWriteModel = UsernameBaseModel;
/** /**
* Repository is used for retrieving/saving domain entities * Repository is used for retrieving/saving domain entities
* */ * */
@Injectable() @Injectable()
export class UsernameRepository export class UsernameRepository
extends PrismaRepositoryBase<UsernameEntity, UsernameModel, UsernameModel> extends PrismaRepositoryBase<
UsernameEntity,
UsernameReadModel,
UsernameWriteModel
>
implements UsernameRepositoryPort implements UsernameRepositoryPort
{ {
constructor( constructor(
@ -42,7 +51,7 @@ export class UsernameRepository
eventEmitter, eventEmitter,
new LoggerBase({ new LoggerBase({
logger: new Logger(UsernameRepository.name), logger: new Logger(UsernameRepository.name),
domain: 'auth', domain: 'auth.username',
messagePublisher, messagePublisher,
}), }),
); );

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { Type } from '@modules/authentication/core/domain/username.types';
import { UpdateUsernameCommand } from '@modules/authentication/core/application/commands/update-username/update-username.command';
@Injectable()
export class UserUpdatedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: 'userUpdated',
})
public async userUpdated(message: string) {
const updatedUser = JSON.parse(message);
try {
if (!updatedUser.hasOwnProperty('userId')) throw new Error();
if (updatedUser.hasOwnProperty('email') && updatedUser.email) {
await this.commandBus.execute(
new UpdateUsernameCommand({
userId: updatedUser.userId,
username: {
name: updatedUser.email,
type: Type.EMAIL,
},
}),
);
}
if (updatedUser.hasOwnProperty('phone') && updatedUser.phone) {
await this.commandBus.execute(
new UpdateUsernameCommand({
userId: updatedUser.userId,
username: {
name: updatedUser.phone,
type: Type.PHONE,
},
}),
);
}
} catch (e: any) {}
}
}

View File

@ -3,7 +3,7 @@ import { PrismaService } from '@modules/authentication/infrastructure/prisma.ser
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { import {
UsernameModel, UsernameReadModel,
UsernameRepository, UsernameRepository,
} from '@modules/authentication/infrastructure/username.repository'; } from '@modules/authentication/infrastructure/username.repository';
import { Type } from '@modules/authentication/core/domain/username.types'; import { Type } from '@modules/authentication/core/domain/username.types';
@ -13,7 +13,7 @@ const mockPrismaService = {
username: { username: {
findFirst: jest.fn().mockImplementation(async () => { findFirst: jest.fn().mockImplementation(async () => {
const now = new Date('2023-06-21 06:00:00'); const now = new Date('2023-06-21 06:00:00');
const record: UsernameModel = { const record: UsernameReadModel = {
authUuid: '330bd6de-1eb8-450b-8674-0e3c9209f048', authUuid: '330bd6de-1eb8-450b-8674-0e3c9209f048',
type: Type.EMAIL, type: Type.EMAIL,
username: 'john.doe@email.com', username: 'john.doe@email.com',

View File

@ -0,0 +1,100 @@
import { UserUpdatedMessageHandler } from '@modules/authentication/interface/message-handlers/user-updated.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const userEmailUpdatedMessage =
'{"userId":"2436d413-b7c7-429e-9792-b78edc17b3ca","email":"new-john.doe@email.com"}';
const userPhoneUpdatedMessage =
'{"userId":"2436d413-b7c7-429e-9792-b78edc17b3ca","phone":"+33611224455"}';
const userBirthDateUpdatedMessage =
'{"userId":"2436d413-b7c7-429e-9792-b78edc17b3ca","birthDate":"1976-10-23"}';
const userIdNotProvidedUpdatedMessage =
'{"user":"2436d413-b7c7-429e-9792-b78edc17b300","email":"new-john.doe@email.com"}';
const mockCommandBus = {
execute: jest
.fn()
.mockImplementationOnce(() => 'new-john.doe@email.com')
.mockImplementationOnce(() => '+33611224455'),
};
describe('User Updated Message Handler', () => {
let userUpdatedMessageHandler: UserUpdatedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
UserUpdatedMessageHandler,
],
}).compile();
userUpdatedMessageHandler = module.get<UserUpdatedMessageHandler>(
UserUpdatedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(userUpdatedMessageHandler).toBeDefined();
});
it('should update an email username', async () => {
jest.spyOn(mockCommandBus, 'execute');
await userUpdatedMessageHandler.userUpdated(userEmailUpdatedMessage);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should update a phone username', async () => {
jest.spyOn(mockCommandBus, 'execute');
await userUpdatedMessageHandler.userUpdated(userPhoneUpdatedMessage);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should not update a username if message does not contain email nor phone', async () => {
jest.spyOn(mockCommandBus, 'execute');
await userUpdatedMessageHandler.userUpdated(userBirthDateUpdatedMessage);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(0);
});
it('should not update a username if userId is unknown', async () => {
jest.spyOn(mockCommandBus, 'execute');
await userUpdatedMessageHandler.userUpdated(
userIdNotProvidedUpdatedMessage,
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(0);
});
// it('should throw a dedicated RpcException if username already exists', async () => {
// jest.spyOn(mockCommandBus, 'execute');
// expect.assertions(3);
// try {
// await updateUsernameGrpcController.updateUsername(updateUsernameRequest);
// } catch (e: any) {
// expect(e).toBeInstanceOf(RpcException);
// expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
// }
// expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
// });
// it('should throw a generic RpcException', async () => {
// jest.spyOn(mockCommandBus, 'execute');
// expect.assertions(3);
// try {
// await updateUsernameGrpcController.updateUsername(updateUsernameRequest);
// } catch (e: any) {
// expect(e).toBeInstanceOf(RpcException);
// expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
// }
// expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
// });
});

View File

@ -1,6 +1,9 @@
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity'; import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
import { Type } from '@modules/authentication/core/domain/username.types'; import { Type } from '@modules/authentication/core/domain/username.types';
import { UsernameModel } from '@modules/authentication/infrastructure/username.repository'; import {
UsernameReadModel,
UsernameWriteModel,
} from '@modules/authentication/infrastructure/username.repository';
import { UsernameResponseDto } from '@modules/authentication/interface/dtos/username.response.dto'; import { UsernameResponseDto } from '@modules/authentication/interface/dtos/username.response.dto';
import { UsernameMapper } from '@modules/authentication/username.mapper'; import { UsernameMapper } from '@modules/authentication/username.mapper';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
@ -16,7 +19,7 @@ const usernameEntity: UsernameEntity = new UsernameEntity({
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}); });
const usernameModel: UsernameModel = { const usernameReadModel: UsernameReadModel = {
authUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e', authUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
username: 'john.doe@email.com', username: 'john.doe@email.com',
type: Type.EMAIL, type: Type.EMAIL,
@ -39,13 +42,14 @@ describe('Username Mapper', () => {
}); });
it('should map domain entity to persistence data', async () => { it('should map domain entity to persistence data', async () => {
const mapped: UsernameModel = usernameMapper.toPersistence(usernameEntity); const mapped: UsernameWriteModel =
usernameMapper.toPersistence(usernameEntity);
expect(mapped.username).toBe('john.doe@email.com'); expect(mapped.username).toBe('john.doe@email.com');
expect(mapped.type).toBe(Type.EMAIL); expect(mapped.type).toBe(Type.EMAIL);
}); });
it('should map persisted data to domain entity', async () => { it('should map persisted data to domain entity', async () => {
const mapped: UsernameEntity = usernameMapper.toDomain(usernameModel); const mapped: UsernameEntity = usernameMapper.toDomain(usernameReadModel);
expect(mapped.getProps().name).toBe('john.doe@email.com'); expect(mapped.getProps().name).toBe('john.doe@email.com');
}); });

View File

@ -2,7 +2,10 @@ import { Mapper } from '@mobicoop/ddd-library';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Type } from './core/domain/username.types'; import { Type } from './core/domain/username.types';
import { UsernameEntity } from './core/domain/username.entity'; import { UsernameEntity } from './core/domain/username.entity';
import { UsernameModel } from './infrastructure/username.repository'; import {
UsernameReadModel,
UsernameWriteModel,
} from './infrastructure/username.repository';
import { UsernameResponseDto } from './interface/dtos/username.response.dto'; import { UsernameResponseDto } from './interface/dtos/username.response.dto';
/** /**
@ -15,21 +18,24 @@ import { UsernameResponseDto } from './interface/dtos/username.response.dto';
@Injectable() @Injectable()
export class UsernameMapper export class UsernameMapper
implements implements
Mapper<UsernameEntity, UsernameModel, UsernameModel, UsernameResponseDto> Mapper<
UsernameEntity,
UsernameReadModel,
UsernameWriteModel,
UsernameResponseDto
>
{ {
toPersistence = (entity: UsernameEntity): UsernameModel => { toPersistence = (entity: UsernameEntity): UsernameWriteModel => {
const copy = entity.getProps(); const copy = entity.getProps();
const record: UsernameModel = { const record: UsernameWriteModel = {
authUuid: copy.userId, authUuid: copy.userId,
username: copy.name, username: copy.name,
type: copy.type, type: copy.type,
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
}; };
return record; return record;
}; };
toDomain = (record: UsernameModel): UsernameEntity => { toDomain = (record: UsernameReadModel): UsernameEntity => {
const entity = new UsernameEntity({ const entity = new UsernameEntity({
id: record.username, id: record.username,
createdAt: new Date(record.createdAt), createdAt: new Date(record.createdAt),