From 659c1baea8c764cfc69c43245b9467c1629967b6 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 17:49:09 +0200 Subject: [PATCH] Implement the GRPC controller to update ads --- src/modules/ad/ad.module.ts | 2 + .../ad/interface/grpc-controllers/ad.proto | 2 +- .../dtos/update-ad.request.dto.ts | 7 ++ .../update-ad.grpc.controller.ts | 43 +++++++++++ tests/unit/ad/interface/ad.fixtures.ts | 43 +++++++++++ .../create-ad.grpc.controller.spec.ts | 51 ++----------- .../update-ad.grpc.controller.spec.ts | 76 +++++++++++++++++++ 7 files changed, 177 insertions(+), 47 deletions(-) create mode 100644 src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts create mode 100644 src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts create mode 100644 tests/unit/ad/interface/ad.fixtures.ts create mode 100644 tests/unit/ad/interface/update-ad.grpc.controller.spec.ts diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index c27a272..286670a 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -33,12 +33,14 @@ import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.g import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller'; import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller'; import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller'; +import { UpdateAdGrpcController } from './interface/grpc-controllers/update-ad.grpc.controller'; import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler'; import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler'; import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler'; const grpcControllers = [ CreateAdGrpcController, + UpdateAdGrpcController, DeleteAdGrpcController, FindAdByIdGrpcController, FindAdsByIdsGrpcController, diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index 581aa08..267d106 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -7,7 +7,7 @@ service AdService { rpc FindAllByIds(AdsById) returns (Ads); rpc FindAllByUserId(UserById) returns (Ads); rpc Create(Ad) returns (AdById); - rpc Update(Ad) returns (Ad); + rpc Update(Ad) returns (Empty); rpc Delete(AdById) returns (Empty); } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts new file mode 100644 index 0000000..2fffdb0 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts @@ -0,0 +1,7 @@ +import { IsUUID } from 'class-validator'; +import { CreateAdRequestDto } from './create-ad.request.dto'; + +export class UpdateAdRequestDto extends CreateAdRequestDto { + @IsUUID(4) + id: string; +} diff --git a/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts new file mode 100644 index 0000000..326a3df --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts @@ -0,0 +1,43 @@ +import { + NotFoundException, + RpcExceptionCode, + RpcValidationPipe, +} from '@mobicoop/ddd-library'; +import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command'; +import { Controller, UsePipes } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { GRPC_SERVICE_NAME } from '@src/app.constants'; +import { UpdateAdRequestDto } from './dtos/update-ad.request.dto'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class UpdateAdGrpcController { + constructor(private readonly commandBus: CommandBus) {} + + @GrpcMethod(GRPC_SERVICE_NAME, 'Update') + async update(data: UpdateAdRequestDto): Promise { + try { + const cmdProps = { + adId: data.id, + ...data, + }; + delete (cmdProps as { id?: string }).id; + + await this.commandBus.execute(new UpdateAdCommand(cmdProps)); + } catch (error) { + if (error instanceof NotFoundException) { + throw new RpcException({ + code: RpcExceptionCode.NOT_FOUND, + message: error.message, + }); + } + throw error; + } + } +} diff --git a/tests/unit/ad/interface/ad.fixtures.ts b/tests/unit/ad/interface/ad.fixtures.ts new file mode 100644 index 0000000..6e19747 --- /dev/null +++ b/tests/unit/ad/interface/ad.fixtures.ts @@ -0,0 +1,43 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; + +const originWaypoint: WaypointDto = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: WaypointDto = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; +export function punctualCreateAdRequest(): CreateAdRequestDto { + return { + userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4', + fromDate: '2023-12-21', + toDate: '2023-12-21', + schedule: [ + { + time: '08:15', + day: 4, + margin: 600, + }, + ], + driver: false, + passenger: true, + seatsRequested: 1, + seatsProposed: 3, + strict: false, + frequency: Frequency.PUNCTUAL, + waypoints: [originWaypoint, destinationWaypoint], + }; +} diff --git a/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts b/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts index b66b4b7..48feaeb 100644 --- a/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts +++ b/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts @@ -1,51 +1,10 @@ -import { IdResponse } from '@mobicoop/ddd-library'; -import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller'; -import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; -import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { CommandBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; - -const originWaypoint: WaypointDto = { - position: 0, - lat: 48.689445, - lon: 6.17651, - houseNumber: '5', - street: 'Avenue Foch', - locality: 'Nancy', - postalCode: '54000', - country: 'France', -}; -const destinationWaypoint: WaypointDto = { - position: 1, - lat: 48.8566, - lon: 2.3522, - locality: 'Paris', - postalCode: '75000', - country: 'France', -}; -const punctualCreateAdRequest: CreateAdRequestDto = { - userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4', - fromDate: '2023-12-21', - toDate: '2023-12-21', - schedule: [ - { - time: '08:15', - day: 4, - margin: 600, - }, - ], - driver: false, - passenger: true, - seatsRequested: 1, - seatsProposed: 3, - strict: false, - frequency: Frequency.PUNCTUAL, - waypoints: [originWaypoint, destinationWaypoint], -}; +import { punctualCreateAdRequest } from './ad.fixtures'; const mockCommandBus = { execute: jest @@ -89,7 +48,7 @@ describe('Create Ad Grpc Controller', () => { it('should create a new ad', async () => { jest.spyOn(mockCommandBus, 'execute'); const result: IdResponse = await createAdGrpcController.create( - punctualCreateAdRequest, + punctualCreateAdRequest(), ); expect(result).toBeInstanceOf(IdResponse); expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2'); @@ -100,7 +59,7 @@ describe('Create Ad Grpc Controller', () => { jest.spyOn(mockCommandBus, 'execute'); expect.assertions(3); try { - await createAdGrpcController.create(punctualCreateAdRequest); + await createAdGrpcController.create(punctualCreateAdRequest()); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS); @@ -112,7 +71,7 @@ describe('Create Ad Grpc Controller', () => { jest.spyOn(mockCommandBus, 'execute'); expect.assertions(3); try { - await createAdGrpcController.create(punctualCreateAdRequest); + await createAdGrpcController.create(punctualCreateAdRequest()); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); diff --git a/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts b/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts new file mode 100644 index 0000000..998e64f --- /dev/null +++ b/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts @@ -0,0 +1,76 @@ +import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library'; +import { UpdateAdGrpcController } from '@modules/ad/interface/grpc-controllers/update-ad.grpc.controller'; +import { CommandBus } from '@nestjs/cqrs'; +import { RpcException } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { punctualCreateAdRequest } from './ad.fixtures'; + +const validAdId = '200d61a8-d878-4378-a609-c19ea71633d2'; +const mockCommandBus = { + execute: jest.fn().mockImplementation(async (command) => { + if (command.adId === '') throw 'Ad id is empty'; + if (command.adId != validAdId) throw new NotFoundException(); + }), +}; + +describe('Update Ad GRPC Controller', () => { + let updateAdGrpcController: UpdateAdGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + UpdateAdGrpcController, + ], + }).compile(); + updateAdGrpcController = module.get( + UpdateAdGrpcController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(updateAdGrpcController).toBeDefined(); + }); + + it('should execute the update ad command', async () => { + await updateAdGrpcController.update({ + id: validAdId, + ...punctualCreateAdRequest(), + }); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if ad is not found', async () => { + expect.assertions(3); + try { + await updateAdGrpcController.update({ + id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8', + ...punctualCreateAdRequest(), + }); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should rethrow any other exceptions', async () => { + expect.assertions(2); + try { + await updateAdGrpcController.update({ + id: '', + ...punctualCreateAdRequest(), + }); + } catch (e: any) { + expect(e).toBe('Ad id is empty'); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +});