diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler.ts b/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler.ts new file mode 100644 index 0000000..9109e03 --- /dev/null +++ b/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler.ts @@ -0,0 +1,25 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { Inject } from '@nestjs/common'; +import { AdEntity } from '../../../domain/ad.entity'; +import { FindAdsByUserIdQuery } from './find-ads-by-user-id.query'; + +@QueryHandler(FindAdsByUserIdQuery) +export class FindAdsByUserIdQueryHandler implements IQueryHandler { + constructor( + @Inject(AD_REPOSITORY) + private readonly repository: AdRepositoryPort, + ) {} + async execute(query: FindAdsByUserIdQuery): Promise { + return await this.repository.findAll( + { + userUuid: query.userId, + }, + { + waypoints: true, + schedule: true, + }, + ); + } +} diff --git a/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query.ts b/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query.ts new file mode 100644 index 0000000..0dd6cec --- /dev/null +++ b/src/modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query.ts @@ -0,0 +1,10 @@ +import { QueryBase } from '@mobicoop/ddd-library'; + +export class FindAdsByUserIdQuery extends QueryBase { + readonly userId: string; + + constructor(userId: string) { + super(); + this.userId = userId; + } +} diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index ed31d44..a3e994a 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -5,6 +5,7 @@ package ad; service AdService { rpc FindOneById(AdById) returns (Ad); rpc FindAllByIds(AdsById) returns (Ads); + rpc FindAllByUserId(UserById) returns (Ads); rpc Create(Ad) returns (AdById); rpc Update(Ad) returns (Ad); rpc Delete(AdById) returns (Empty); @@ -14,6 +15,10 @@ message AdById { string id = 1; } +message UserById { + string id = 1; +} + message AdsById { repeated string ids = 1; } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/find-ads-by-user-id.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/find-ads-by-user-id.request.dto.ts new file mode 100644 index 0000000..5ccf889 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/find-ads-by-user-id.request.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class FindAdsByUserIdRequestDto { + @IsString() + id: string; +} diff --git a/src/modules/ad/interface/grpc-controllers/find-ads-by-user-id.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/find-ads-by-user-id.grpc.controller.ts new file mode 100644 index 0000000..4c713cb --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/find-ads-by-user-id.grpc.controller.ts @@ -0,0 +1,44 @@ +import { Controller, UsePipes } from '@nestjs/common'; +import { QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { AdMapper } from '@modules/ad/ad.mapper'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; +import { GRPC_SERVICE_NAME } from '@src/app.constants'; +import { AdsResponseDto } from '../dtos/ads.response.dto'; +import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query'; +import { FindAdsByUserIdRequestDto } from './dtos/find-ads-by-user-id.request.dto'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class FindAdsByUserIdGrpcController { + constructor( + protected readonly mapper: AdMapper, + private readonly queryBus: QueryBus, + ) {} + + @GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByUserId') + async findAllByUserId( + data: FindAdsByUserIdRequestDto, + ): Promise { + try { + const ads: AdEntity[] = await this.queryBus.execute( + new FindAdsByUserIdQuery(data.id), + ); + return { + ads: ads.map((ad: AdEntity) => this.mapper.toResponse(ad)), + }; + } catch (e) { + throw new RpcException({ + code: RpcExceptionCode.UNKNOWN, + message: e.message, + }); + } + } +} diff --git a/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts b/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts new file mode 100644 index 0000000..300c84c --- /dev/null +++ b/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts @@ -0,0 +1,103 @@ +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Test, TestingModule } from '@nestjs/testing'; +import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler'; +import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query'; + +const originWaypointProps: WaypointProps = { + position: 0, + address: { + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + coordinates: { + lat: 48.689445, + lon: 6.17651, + }, + }, +}; +const destinationWaypointProps: WaypointProps = { + position: 1, + address: { + locality: 'Paris', + postalCode: '75000', + country: 'France', + coordinates: { + lat: 48.8566, + lon: 2.3522, + }, + }, +}; +const baseCreateAdProps = { + userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', + seatsProposed: 3, + seatsRequested: 1, + strict: false, + waypoints: [originWaypointProps, destinationWaypointProps], +}; +const punctualCreateAdProps = { + fromDate: '2023-06-22', + toDate: '2023-06-22', + schedule: [ + { + time: '08:30', + }, + ], + frequency: Frequency.PUNCTUAL, +}; +const punctualPassengerCreateAdProps: CreateAdProps = { + ...baseCreateAdProps, + ...punctualCreateAdProps, + driver: false, + passenger: true, +}; + +const ads: AdEntity[] = [ + AdEntity.create(punctualPassengerCreateAdProps), + AdEntity.create(punctualPassengerCreateAdProps), + AdEntity.create(punctualPassengerCreateAdProps), +]; + +const mockAdRepository = { + findAll: jest.fn().mockImplementation(() => ads), +}; + +describe('Find Ads By User Id Query Handler', () => { + let findAdsByUserIdQueryHandler: FindAdsByUserIdQueryHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + FindAdsByUserIdQueryHandler, + ], + }).compile(); + + findAdsByUserIdQueryHandler = module.get( + FindAdsByUserIdQueryHandler, + ); + }); + + it('should be defined', () => { + expect(findAdsByUserIdQueryHandler).toBeDefined(); + }); + + describe('execution', () => { + it('should return an ad', async () => { + const findAdsByIdsQuery = new FindAdsByUserIdQuery( + 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', + ); + const ads: AdEntity[] = + await findAdsByUserIdQueryHandler.execute(findAdsByIdsQuery); + expect(ads).toHaveLength(3); + expect(ads[1].getProps().fromDate).toBe('2023-06-22'); + }); + }); +}); diff --git a/tests/unit/ad/interface/find-ads-by-user-id.grpc.controller.spec.ts b/tests/unit/ad/interface/find-ads-by-user-id.grpc.controller.spec.ts new file mode 100644 index 0000000..7625563 --- /dev/null +++ b/tests/unit/ad/interface/find-ads-by-user-id.grpc.controller.spec.ts @@ -0,0 +1,124 @@ +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { AdMapper } from '@modules/ad/ad.mapper'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { FindAdsByUserIdGrpcController } from '@modules/ad/interface/grpc-controllers/find-ads-by-user-id.grpc.controller'; +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', + '200d61a8-d878-4378-a609-c19ea71633d3', + '200d61a8-d878-4378-a609-c19ea71633d4', + ]) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + +const mockAdMapper = { + toResponse: jest.fn().mockImplementationOnce(() => ({ + userId: '8cc90d1a-4a59-4289-a7d8-078f9db7857f', + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-06-27', + toDate: '2023-06-27', + schedule: { + tue: '07:15', + }, + marginDurations: { + mon: 900, + tue: 900, + wed: 900, + thu: 900, + fri: 900, + sat: 900, + sun: 900, + }, + seatsProposed: 3, + seatsRequested: 1, + waypoints: [ + { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + }, + { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }, + ], + })), +}; + +describe('Find Ads By User Id Grpc Controller', () => { + let findAdsByUserIdGrpcController: FindAdsByUserIdGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: QueryBus, + useValue: mockQueryBus, + }, + { + provide: AdMapper, + useValue: mockAdMapper, + }, + FindAdsByUserIdGrpcController, + ], + }).compile(); + + findAdsByUserIdGrpcController = module.get( + FindAdsByUserIdGrpcController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(findAdsByUserIdGrpcController).toBeDefined(); + }); + + it('should return ads', async () => { + jest.spyOn(mockQueryBus, 'execute'); + jest.spyOn(mockAdMapper, 'toResponse'); + const response = await findAdsByUserIdGrpcController.findAllByUserId({ + id: '8cc90d1a-4a59-4289-a7d8-078f9db7857f', + }); + expect(response.ads).toHaveLength(3); + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(3); + }); + + it('should throw a generic RpcException', async () => { + jest.spyOn(mockQueryBus, 'execute'); + jest.spyOn(mockAdMapper, 'toResponse'); + expect.assertions(4); + try { + await findAdsByUserIdGrpcController.findAllByUserId({ + id: '8cc90d1a-4a59-4289-a7d8-078f9db7857f', + }); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); + } + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0); + }); +});