From 3b0f4b8c49ba69bdc584d42aab1e04c097b009cc Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 6 Apr 2023 17:05:25 +0200 Subject: [PATCH] basic match action without functional behaviour --- package.json | 6 ++ src/app.module.ts | 2 + src/main.ts | 27 ++++----- .../prisma.health-indicator.usecase.spec.ts | 10 ++-- .../adapters/primaries/matcher.controller.ts | 3 +- .../matcher/adapters/primaries/matcher.proto | 7 ++- .../adapters/secondaries/message-broker.ts | 12 ++++ .../matcher/adapters/secondaries/messager.ts | 18 ++++++ .../matcher/domain/usecases/match.usecase.ts | 38 ++++++++++++ src/modules/matcher/matcher.module.ts | 35 +++++++++++ .../matcher/tests/unit/match.usecase.spec.ts | 59 +++++++++++++++++++ .../matcher/tests/unit/messager.spec.ts | 47 +++++++++++++++ .../unit/rpc-validation-pipe.usecase.spec.ts | 20 +++++++ 13 files changed, 262 insertions(+), 22 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/message-broker.ts create mode 100644 src/modules/matcher/adapters/secondaries/messager.ts create mode 100644 src/modules/matcher/domain/usecases/match.usecase.ts create mode 100644 src/modules/matcher/matcher.module.ts create mode 100644 src/modules/matcher/tests/unit/match.usecase.spec.ts create mode 100644 src/modules/matcher/tests/unit/messager.spec.ts create mode 100644 src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/package.json b/package.json index 9f0f230..ce6ad16 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,12 @@ "json", "ts" ], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/src/app.module.ts b/src/app.module.ts index e355295..69bca0e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ConfigurationModule } from './modules/configuration/configuration.module'; import { HealthModule } from './modules/health/health.module'; +import { MatcherModule } from './modules/matcher/matcher.module'; @Module({ imports: [ @@ -11,6 +12,7 @@ import { HealthModule } from './modules/health/health.module'; AutomapperModule.forRoot({ strategyInitializer: classes() }), ConfigurationModule, HealthModule, + MatcherModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 2749d0a..0daaf12 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -// import { join } from 'path'; +import { join } from 'path'; import { AppModule } from './app.module'; async function bootstrap() { @@ -8,19 +8,18 @@ async function bootstrap() { app.connectMicroservice({ transport: Transport.TCP, }); - // app.connectMicroservice({ - // transport: Transport.GRPC, - // options: { - // // package: ['matcher', 'health'], - // package: ['health'], - // protoPath: [ - // // join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'), - // join(__dirname, 'modules/health/adapters/primaries/health.proto'), - // ], - // url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, - // loader: { keepCase: true }, - // }, - // }); + app.connectMicroservice({ + transport: Transport.GRPC, + options: { + package: ['matcher', 'health'], + protoPath: [ + join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'), + join(__dirname, 'modules/health/adapters/primaries/health.proto'), + ], + url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, + loader: { keepCase: true }, + }, + }); await app.startAllMicroservices(); await app.listen(process.env.HEALTH_SERVICE_PORT); diff --git a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts index 971a836..7d3cf42 100644 --- a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts +++ b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; -import { TerritoriesRepository } from '../../../territory/adapters/secondaries/territories.repository'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; +import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; -const mockTerritoriesRepository = { +const mockAdRepository = { healthCheck: jest .fn() .mockImplementationOnce(() => { @@ -25,8 +25,8 @@ describe('PrismaHealthIndicatorUseCase', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: TerritoriesRepository, - useValue: mockTerritoriesRepository, + provide: AdRepository, + useValue: mockAdRepository, }, PrismaHealthIndicatorUseCase, ], diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 2d78ace..a7619fb 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -1,7 +1,7 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { QueryBus } from '@nestjs/cqrs'; import { GrpcMethod } from '@nestjs/microservices'; import { RpcValidationPipe } from 'src/modules/utils/pipes/rpc.validation-pipe'; import { MatchRequest } from '../../domain/dtos/match.request'; @@ -19,7 +19,6 @@ import { MatchPresenter } from '../secondaries/match.presenter'; @Controller() export class MatcherController { constructor( - private readonly _commandBus: CommandBus, private readonly _queryBus: QueryBus, @InjectMapper() private readonly _mapper: Mapper, ) {} diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index 8c18b46..fa642d5 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -7,7 +7,12 @@ service MatcherService { } message MatchRequest { - string uuid = 1; + repeated Point waypoints = 1; +} + +message Point { + float lon = 1; + float lat = 2; } message Match { diff --git a/src/modules/matcher/adapters/secondaries/message-broker.ts b/src/modules/matcher/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..594aa43 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/message-broker.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export abstract class IMessageBroker { + exchange: string; + + constructor(exchange: string) { + this.exchange = exchange; + } + + abstract publish(routingKey: string, message: string): void; +} diff --git a/src/modules/matcher/adapters/secondaries/messager.ts b/src/modules/matcher/adapters/secondaries/messager.ts new file mode 100644 index 0000000..0725261 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/messager.ts @@ -0,0 +1,18 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IMessageBroker } from './message-broker'; + +@Injectable() +export class Messager extends IMessageBroker { + constructor( + private readonly _amqpConnection: AmqpConnection, + configService: ConfigService, + ) { + super(configService.get('RMQ_EXCHANGE')); + } + + publish(routingKey: string, message: string): void { + this._amqpConnection.publish(this.exchange, routingKey, message); + } +} diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts new file mode 100644 index 0000000..e925367 --- /dev/null +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -0,0 +1,38 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { QueryHandler } from '@nestjs/cqrs'; +import { Messager } from '../../adapters/secondaries/messager'; +import { MatchQuery } from '../../queries/match.query'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { Match } from '../entities/match'; +import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; + +@QueryHandler(MatchQuery) +export class MatchUseCase { + constructor( + private readonly _repository: AdRepository, + private readonly _messager: Messager, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(matchQuery: MatchQuery): Promise> { + try { + const match = new Match(); + match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + this._messager.publish('matcher.match', 'match !'); + return { + data: [match], + total: 1, + }; + } catch (error) { + this._messager.publish( + 'logging.matcher.match.crit', + JSON.stringify({ + matchQuery, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts new file mode 100644 index 0000000..8071f27 --- /dev/null +++ b/src/modules/matcher/matcher.module.ts @@ -0,0 +1,35 @@ +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { DatabaseModule } from '../database/database.module'; +import { MatcherController } from './adapters/primaries/matcher.controller'; +import { MatchProfile } from './mappers/match.profile'; +import { AdRepository } from './adapters/secondaries/ad.repository'; +import { MatchUseCase } from './domain/usecases/match.usecase'; +import { Messager } from './adapters/secondaries/messager'; + +@Module({ + imports: [ + DatabaseModule, + CqrsModule, + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + exchanges: [ + { + name: configService.get('RMQ_EXCHANGE'), + type: 'topic', + }, + ], + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + }), + inject: [ConfigService], + }), + ], + controllers: [MatcherController], + providers: [MatchProfile, AdRepository, Messager, MatchUseCase], + exports: [], +}) +export class MatcherModule {} diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts new file mode 100644 index 0000000..b0b304f --- /dev/null +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Messager } from '../../adapters/secondaries/messager'; +import { MatchUseCase } from '../../domain/usecases/match.usecase'; +import { MatchRequest } from '../../domain/dtos/match.request'; +import { MatchQuery } from '../../queries/match.query'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { AutomapperModule } from '@automapper/nestjs'; +import { classes } from '@automapper/classes'; + +const mockAdRepository = {}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('MatchUseCase', () => { + let matchUseCase: MatchUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AdRepository, + useValue: mockAdRepository, + }, + { + provide: Messager, + useValue: mockMessager, + }, + MatchUseCase, + ], + }).compile(); + + matchUseCase = module.get(MatchUseCase); + }); + + it('should be defined', () => { + expect(matchUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should return matches', async () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.waypoints = [ + { + lon: 1.093912, + lat: 49.440041, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + const matches = await matchUseCase.execute(new MatchQuery(matchRequest)); + expect(matches.total).toBe(1); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/messager.spec.ts b/src/modules/matcher/tests/unit/messager.spec.ts new file mode 100644 index 0000000..0331332 --- /dev/null +++ b/src/modules/matcher/tests/unit/messager.spec.ts @@ -0,0 +1,47 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Messager } from '../../adapters/secondaries/messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +const mockConfigService = { + get: jest.fn().mockResolvedValue({ + RMQ_EXCHANGE: 'mobicoop', + }), +}; + +describe('Messager', () => { + let messager: Messager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + Messager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + messager = module.get(Messager); + }); + + it('should be defined', () => { + expect(messager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + messager.publish('test.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts new file mode 100644 index 0000000..911466c --- /dev/null +++ b/src/modules/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,20 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; +import { MatchRequest } from '../../../matcher/domain/dtos/match.request'; + +describe('RpcValidationPipe', () => { + it('should not validate request', async () => { + const target: RpcValidationPipe = new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }); + const metadata: ArgumentMetadata = { + type: 'body', + metatype: MatchRequest, + data: '', + }; + await target.transform({}, metadata).catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +});