diff --git a/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query-handler.ts b/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query-handler.ts new file mode 100644 index 0000000..3d1fb63 --- /dev/null +++ b/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query-handler.ts @@ -0,0 +1,17 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { FindUserByIdQuery } from './find-user-by-id.query'; +import { Inject } from '@nestjs/common'; +import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; +import { UserRepositoryPort } from '../../ports/user.repository.port'; +import { UserEntity } from '@modules/user/core/domain/user.entity'; + +@QueryHandler(FindUserByIdQuery) +export class FindUserByIdQueryHandler implements IQueryHandler { + constructor( + @Inject(USER_REPOSITORY) + private readonly userRepository: UserRepositoryPort, + ) {} + async execute(query: FindUserByIdQuery): Promise { + return await this.userRepository.findOneById(query.id); + } +} diff --git a/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query.ts b/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query.ts new file mode 100644 index 0000000..53dd77b --- /dev/null +++ b/src/modules/user/core/application/queries/find-user-by-id/find-user-by-id.query.ts @@ -0,0 +1,10 @@ +import { QueryBase } from '@mobicoop/ddd-library'; + +export class FindUserByIdQuery extends QueryBase { + readonly id: string; + + constructor(id: string) { + super(); + this.id = id; + } +} diff --git a/src/modules/user/interface/grpc-controllers/dtos/find-user-by-id.request.dto.ts b/src/modules/user/interface/grpc-controllers/dtos/find-user-by-id.request.dto.ts new file mode 100644 index 0000000..edafb1e --- /dev/null +++ b/src/modules/user/interface/grpc-controllers/dtos/find-user-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class FindUserByIdRequestDto { + @IsString() + @IsNotEmpty() + id: string; +} diff --git a/src/modules/user/interface/grpc-controllers/find-user-by-id.grpc.controller.ts b/src/modules/user/interface/grpc-controllers/find-user-by-id.grpc.controller.ts new file mode 100644 index 0000000..22cf460 --- /dev/null +++ b/src/modules/user/interface/grpc-controllers/find-user-by-id.grpc.controller.ts @@ -0,0 +1,46 @@ +import { Controller, UsePipes } from '@nestjs/common'; +import { QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { NotFoundException } from '@mobicoop/ddd-library'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; +import { UserMapper } from '@modules/user/user.mapper'; +import { FindUserByIdRequestDto } from './dtos/find-user-by-id.request.dto'; +import { UserResponseDto } from '../dtos/user.response.dto'; +import { UserEntity } from '@modules/user/core/domain/user.entity'; +import { FindUserByIdQuery } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class FindUserByIdGrpcController { + constructor( + protected readonly mapper: UserMapper, + private readonly queryBus: QueryBus, + ) {} + + @GrpcMethod('UserService', 'FindOneById') + async findOnebyId(data: FindUserByIdRequestDto): Promise { + try { + const user: UserEntity = await this.queryBus.execute( + new FindUserByIdQuery(data.id), + ); + return this.mapper.toResponse(user); + } catch (e) { + if (e instanceof NotFoundException) { + throw new RpcException({ + code: RpcExceptionCode.NOT_FOUND, + message: e.message, + }); + } + throw new RpcException({ + code: RpcExceptionCode.UNKNOWN, + message: e.message, + }); + } + } +} diff --git a/src/modules/user/tests/unit/core/find-user-by-id.query-handler.spec.ts b/src/modules/user/tests/unit/core/find-user-by-id.query-handler.spec.ts new file mode 100644 index 0000000..a2f3803 --- /dev/null +++ b/src/modules/user/tests/unit/core/find-user-by-id.query-handler.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserEntity } from '@modules/user/core/domain/user.entity'; +import { FindUserByIdQueryHandler } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query-handler'; +import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; +import { FindUserByIdQuery } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query'; + +const now = new Date('2023-06-21 06:00:00'); +const user: UserEntity = new UserEntity({ + id: 'c160cf8c-f057-4962-841f-3ad68346df44', + props: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + phone: '+33611223344', + }, + createdAt: now, + updatedAt: now, +}); + +const mockUserRepository = { + findOneById: jest.fn().mockImplementation(() => user), +}; + +describe('find-user-by-id.query-handler', () => { + let findUserByIdQueryHandler: FindUserByIdQueryHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_REPOSITORY, + useValue: mockUserRepository, + }, + FindUserByIdQueryHandler, + ], + }).compile(); + + findUserByIdQueryHandler = module.get( + FindUserByIdQueryHandler, + ); + }); + + it('should be defined', () => { + expect(findUserByIdQueryHandler).toBeDefined(); + }); + + describe('execution', () => { + it('should return a user', async () => { + const findUserbyIdQuery = new FindUserByIdQuery( + 'dd264806-13b4-4226-9b18-87adf0ad5dd1', + ); + const user: UserEntity = await findUserByIdQueryHandler.execute( + findUserbyIdQuery, + ); + expect(user.getProps().lastName).toBe('Doe'); + }); + }); +}); diff --git a/src/modules/user/tests/unit/interface/find-user-by-id.grpc.controller.spec.ts b/src/modules/user/tests/unit/interface/find-user-by-id.grpc.controller.spec.ts new file mode 100644 index 0000000..1f36cb7 --- /dev/null +++ b/src/modules/user/tests/unit/interface/find-user-by-id.grpc.controller.spec.ts @@ -0,0 +1,103 @@ +import { NotFoundException } from '@mobicoop/ddd-library'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { FindUserByIdGrpcController } from '@modules/user/interface/grpc-controllers/find-user-by-id.grpc.controller'; +import { UserMapper } from '@modules/user/user.mapper'; +import { QueryBus } from '@nestjs/cqrs'; +import { RpcException } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockQueryBus = { + execute: jest + .fn() + .mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2') + .mockImplementationOnce(() => { + throw new NotFoundException(); + }) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + +const mockUserMapper = { + toResponse: jest.fn().mockImplementationOnce(() => ({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + phone: '+33611223344', + })), +}; + +describe('Find User By Id Grpc Controller', () => { + let findUserbyIdGrpcController: FindUserByIdGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: QueryBus, + useValue: mockQueryBus, + }, + { + provide: UserMapper, + useValue: mockUserMapper, + }, + FindUserByIdGrpcController, + ], + }).compile(); + + findUserbyIdGrpcController = module.get( + FindUserByIdGrpcController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(findUserbyIdGrpcController).toBeDefined(); + }); + + it('should return a user', async () => { + jest.spyOn(mockQueryBus, 'execute'); + jest.spyOn(mockUserMapper, 'toResponse'); + const response = await findUserbyIdGrpcController.findOnebyId({ + id: '6dcf093c-c7db-4dae-8e9c-c715cebf83c7', + }); + expect(response.firstName).toBe('John'); + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if user is not found', async () => { + jest.spyOn(mockQueryBus, 'execute'); + jest.spyOn(mockUserMapper, 'toResponse'); + expect.assertions(4); + try { + await findUserbyIdGrpcController.findOnebyId({ + id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8', + }); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND); + } + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0); + }); + + it('should throw a generic RpcException', async () => { + jest.spyOn(mockQueryBus, 'execute'); + jest.spyOn(mockUserMapper, 'toResponse'); + expect.assertions(4); + try { + await findUserbyIdGrpcController.findOnebyId({ + id: '53c8e7ec-ef68-42bc-ba4c-5ef3effa60a6', + }); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); + } + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0); + }); +});