From 5b63d8ba1a1ebc83ec4653c6384406be5373f11d Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 7 Feb 2023 14:06:24 +0100 Subject: [PATCH] basic crud --- package.json | 5 + .../20230206113946_init/migration.sql | 1 + prisma/schema.prisma | 2 +- .../primaries/territories.controller.ts | 129 ++++++++++++++++-- .../adapters/primaries/territory.proto | 28 +++- .../commands/create-territory.command.ts | 9 ++ .../commands/delete-territory.command.ts | 7 + .../commands/update-territory.command.ts | 9 ++ .../domain/dtos/create-territory.request.ts | 19 +++ ...find-all-territories-for-point.request.ts} | 2 +- .../dtos/find-all-territories.request.ts | 11 ++ .../dtos/find-territory-by-uuid.request.ts | 7 + .../domain/dtos/update-territory.request.ts | 19 +++ .../territories/domain/entities/territory.ts | 3 + .../usecases/create-territory.usecase.ts | 50 +++++++ .../usecases/delete-territory.usecase.ts | 39 ++++++ ...find-all-territories-for-point.usecase.ts} | 13 +- .../usecases/find-all-territories.usecase.ts | 19 +++ .../find-territory-by-uuid.usecase.ts | 35 +++++ .../usecases/update-territory.usecase.ts | 52 +++++++ .../territories/mappers/territory.profile.ts | 13 +- .../find-all-territories-for-point.query.ts | 15 ++ .../queries/find-all-territories.query.ts | 11 ++ .../queries/find-for-point.query.ts | 10 -- .../queries/find-territory-by-uuid.query.ts | 9 ++ src/modules/territories/territories.module.ts | 14 +- .../unit/create-territory.usecase.spec.ts | 88 ++++++++++++ .../unit/delete-territory.usecase.spec.ts | 98 +++++++++++++ ...-all-territories-for-point.usecase.spec.ts | 68 +++++++++ .../unit/find-all-territories.usecase.spec.ts | 72 ++++++++++ .../tests/unit/find-for-point.usecase.spec.ts | 61 --------- .../find-territory-by-uuid.usecase.spec.ts | 80 +++++++++++ .../unit/update-territory.usecase.spec.ts | 90 ++++++++++++ .../unit/rpc-validation-pipe.usecase.spec.ts | 22 +++ 34 files changed, 1014 insertions(+), 96 deletions(-) create mode 100644 src/modules/territories/commands/create-territory.command.ts create mode 100644 src/modules/territories/commands/delete-territory.command.ts create mode 100644 src/modules/territories/commands/update-territory.command.ts create mode 100644 src/modules/territories/domain/dtos/create-territory.request.ts rename src/modules/territories/domain/dtos/{find-for-point.request.ts => find-all-territories-for-point.request.ts} (75%) create mode 100644 src/modules/territories/domain/dtos/find-all-territories.request.ts create mode 100644 src/modules/territories/domain/dtos/find-territory-by-uuid.request.ts create mode 100644 src/modules/territories/domain/dtos/update-territory.request.ts create mode 100644 src/modules/territories/domain/usecases/create-territory.usecase.ts create mode 100644 src/modules/territories/domain/usecases/delete-territory.usecase.ts rename src/modules/territories/domain/usecases/{find-for-point.usecase.ts => find-all-territories-for-point.usecase.ts} (55%) create mode 100644 src/modules/territories/domain/usecases/find-all-territories.usecase.ts create mode 100644 src/modules/territories/domain/usecases/find-territory-by-uuid.usecase.ts create mode 100644 src/modules/territories/domain/usecases/update-territory.usecase.ts create mode 100644 src/modules/territories/queries/find-all-territories-for-point.query.ts create mode 100644 src/modules/territories/queries/find-all-territories.query.ts delete mode 100644 src/modules/territories/queries/find-for-point.query.ts create mode 100644 src/modules/territories/queries/find-territory-by-uuid.query.ts create mode 100644 src/modules/territories/tests/unit/create-territory.usecase.spec.ts create mode 100644 src/modules/territories/tests/unit/delete-territory.usecase.spec.ts create mode 100644 src/modules/territories/tests/unit/find-all-territories-for-point.usecase.spec.ts create mode 100644 src/modules/territories/tests/unit/find-all-territories.usecase.spec.ts delete mode 100644 src/modules/territories/tests/unit/find-for-point.usecase.spec.ts create mode 100644 src/modules/territories/tests/unit/find-territory-by-uuid.usecase.spec.ts create mode 100644 src/modules/territories/tests/unit/update-territory.usecase.spec.ts create mode 100644 src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/package.json b/package.json index a112e7d..622d5b4 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "json", "ts" ], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/prisma/migrations/20230206113946_init/migration.sql b/prisma/migrations/20230206113946_init/migration.sql index e8203c5..c864a52 100644 --- a/prisma/migrations/20230206113946_init/migration.sql +++ b/prisma/migrations/20230206113946_init/migration.sql @@ -13,4 +13,5 @@ CREATE TABLE "territory" ( ); -- CreateIndex +CREATE UNIQUE INDEX "territory_name_key" ON "territory"("name"); CREATE INDEX "shape_idx" ON "territory" USING GIST ("shape"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3e51ead..f136850 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,7 @@ datasource db { model Territory { uuid String @id @default(uuid()) @db.Uuid - name String + name String @unique shape Unsupported("geometry") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/modules/territories/adapters/primaries/territories.controller.ts b/src/modules/territories/adapters/primaries/territories.controller.ts index 9fe7d43..550455f 100644 --- a/src/modules/territories/adapters/primaries/territories.controller.ts +++ b/src/modules/territories/adapters/primaries/territories.controller.ts @@ -7,14 +7,24 @@ import { UseInterceptors, UsePipes, } from '@nestjs/common'; -import { QueryBus } from '@nestjs/cqrs'; -import { GrpcMethod } from '@nestjs/microservices'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { Territory } from '../../domain/entities/territory'; -import { FindForPointQuery } from '../../queries/find-for-point.query'; +import { FindAllTerritoriesForPointQuery } from '../../queries/find-all-territories-for-point.query'; import { TerritoryPresenter } from './territory.presenter'; import { ICollection } from '../../../database/src/interfaces/collection.interface'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; -import { FindForPointRequest } from '../../domain/dtos/find-for-point.request'; +import { FindAllTerritoriesForPointRequest } from '../../domain/dtos/find-all-territories-for-point.request'; +import { FindAllTerritoriesRequest } from '../../domain/dtos/find-all-territories.request'; +import { FindAllTerritoriesQuery } from '../../queries/find-all-territories.query'; +import { FindTerritoryByUuidRequest } from '../../domain/dtos/find-territory-by-uuid.request'; +import { FindTerritoryByUuidQuery } from '../../queries/find-territory-by-uuid.query'; +import { CreateTerritoryRequest } from '../../domain/dtos/create-territory.request'; +import { CreateTerritoryCommand } from '../../commands/create-territory.command'; +import { DatabaseException } from 'src/modules/database/src/exceptions/database.exception'; +import { UpdateTerritoryRequest } from '../../domain/dtos/update-territory.request'; +import { UpdateTerritoryCommand } from '../../commands/update-territory.command'; +import { DeleteTerritoryCommand } from '../../commands/delete-territory.command'; @UsePipes( new RpcValidationPipe({ @@ -25,18 +35,19 @@ import { FindForPointRequest } from '../../domain/dtos/find-for-point.request'; @Controller() export class TerritoriesController { constructor( + private readonly _commandBus: CommandBus, private readonly _queryBus: QueryBus, @InjectMapper() private readonly _mapper: Mapper, ) {} - @GrpcMethod('TerritoriesService', 'FindForPoint') + @GrpcMethod('TerritoriesService', 'FindAllForPoint') @UseInterceptors(CacheInterceptor) - @CacheKey('TerritoriesServiceFindForPoint') - async findForPoint( - data: FindForPointRequest, + @CacheKey('TerritoriesServiceFindAllForPoint') + async findAllTerritoriesForPoint( + data: FindAllTerritoriesForPointRequest, ): Promise> { const territoryCollection = await this._queryBus.execute( - new FindForPointQuery(data), + new FindAllTerritoriesForPointQuery(data), ); return Promise.resolve({ data: territoryCollection.data.map((territory: Territory) => @@ -45,4 +56,104 @@ export class TerritoriesController { total: territoryCollection.total, }); } + + @GrpcMethod('TerritoriesService', 'FindAll') + @UseInterceptors(CacheInterceptor) + @CacheKey('TerritoriesServiceFindAll') + async findAll( + data: FindAllTerritoriesRequest, + ): Promise> { + const territoryCollection = await this._queryBus.execute( + new FindAllTerritoriesQuery(data), + ); + return Promise.resolve({ + data: territoryCollection.data.map((territory: Territory) => + this._mapper.map(territory, Territory, TerritoryPresenter), + ), + total: territoryCollection.total, + }); + } + + @GrpcMethod('TerritoriesService', 'FindOneByUuid') + @UseInterceptors(CacheInterceptor) + @CacheKey('TerritoriesServiceFindOneByUuid') + async findOneByUuid( + data: FindTerritoryByUuidRequest, + ): Promise { + try { + const territory = await this._queryBus.execute( + new FindTerritoryByUuidQuery(data), + ); + return this._mapper.map(territory, Territory, TerritoryPresenter); + } catch (error) { + throw new RpcException({ + code: 5, + message: 'Territory not found', + }); + } + } + + @GrpcMethod('TerritoriesService', 'Create') + async createTerritory( + data: CreateTerritoryRequest, + ): Promise { + try { + const territory = await this._commandBus.execute( + new CreateTerritoryCommand(data), + ); + return this._mapper.map(territory, Territory, TerritoryPresenter); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('Already exists')) { + throw new RpcException({ + code: 6, + message: 'Territory already exists', + }); + } + } + throw new RpcException({}); + } + } + + @GrpcMethod('TerritoriesService', 'Update') + async updateTerritory( + data: UpdateTerritoryRequest, + ): Promise { + try { + const territory = await this._commandBus.execute( + new UpdateTerritoryCommand(data), + ); + + return this._mapper.map(territory, Territory, TerritoryPresenter); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('not found')) { + throw new RpcException({ + code: 5, + message: 'Territory not found', + }); + } + } + throw new RpcException({}); + } + } + + @GrpcMethod('TerritoriesService', 'Delete') + async deleteTerritory(data: FindTerritoryByUuidRequest): Promise { + try { + await this._commandBus.execute(new DeleteTerritoryCommand(data.uuid)); + + return Promise.resolve(); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('not found')) { + throw new RpcException({ + code: 5, + message: 'Territory not found', + }); + } + } + throw new RpcException({}); + } + } } diff --git a/src/modules/territories/adapters/primaries/territory.proto b/src/modules/territories/adapters/primaries/territory.proto index 7582188..1d174c9 100644 --- a/src/modules/territories/adapters/primaries/territory.proto +++ b/src/modules/territories/adapters/primaries/territory.proto @@ -3,12 +3,27 @@ syntax = "proto3"; package territory; service TerritoriesService { - rpc FindForPoint(Point) returns (Territories); + rpc FindOneByUuid(TerritoryByUuid) returns (Territory); + rpc FindAll(TerritoryFilter) returns (Territories); + rpc FindAllForPoint(Point) returns (Territories); + rpc Create(Territory) returns (Territory); + rpc Update(Territory) returns (Territory); + rpc Delete(TerritoryByUuid) returns (Empty); } -message Point { - float lon = 1; - float lat = 2; +message TerritoryByUuid { + string uuid = 1; +} + +message Territory { + string uuid = 1; + string name = 2; + string shape = 3; +} + +message TerritoryFilter { + optional int32 page = 1; + optional int32 perPage = 2; } message Territories { @@ -16,6 +31,7 @@ message Territories { int32 total = 2; } -message Territory { - string name = 1; +message Point { + float lon = 1; + float lat = 2; } diff --git a/src/modules/territories/commands/create-territory.command.ts b/src/modules/territories/commands/create-territory.command.ts new file mode 100644 index 0000000..6190a90 --- /dev/null +++ b/src/modules/territories/commands/create-territory.command.ts @@ -0,0 +1,9 @@ +import { CreateTerritoryRequest } from '../domain/dtos/create-territory.request'; + +export class CreateTerritoryCommand { + readonly createTerritoryRequest: CreateTerritoryRequest; + + constructor(request: CreateTerritoryRequest) { + this.createTerritoryRequest = request; + } +} diff --git a/src/modules/territories/commands/delete-territory.command.ts b/src/modules/territories/commands/delete-territory.command.ts new file mode 100644 index 0000000..7ca19f2 --- /dev/null +++ b/src/modules/territories/commands/delete-territory.command.ts @@ -0,0 +1,7 @@ +export class DeleteTerritoryCommand { + readonly uuid: string; + + constructor(uuid: string) { + this.uuid = uuid; + } +} diff --git a/src/modules/territories/commands/update-territory.command.ts b/src/modules/territories/commands/update-territory.command.ts new file mode 100644 index 0000000..f945d72 --- /dev/null +++ b/src/modules/territories/commands/update-territory.command.ts @@ -0,0 +1,9 @@ +import { UpdateTerritoryRequest } from '../domain/dtos/update-territory.request'; + +export class UpdateTerritoryCommand { + readonly updateTerritoryRequest: UpdateTerritoryRequest; + + constructor(request: UpdateTerritoryRequest) { + this.updateTerritoryRequest = request; + } +} diff --git a/src/modules/territories/domain/dtos/create-territory.request.ts b/src/modules/territories/domain/dtos/create-territory.request.ts new file mode 100644 index 0000000..0ea36af --- /dev/null +++ b/src/modules/territories/domain/dtos/create-territory.request.ts @@ -0,0 +1,19 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateTerritoryRequest { + @IsString() + @IsOptional() + @AutoMap() + uuid?: string; + + @IsString() + @IsNotEmpty() + @AutoMap() + name: string; + + @IsString() + @IsNotEmpty() + @AutoMap() + shape: string; +} diff --git a/src/modules/territories/domain/dtos/find-for-point.request.ts b/src/modules/territories/domain/dtos/find-all-territories-for-point.request.ts similarity index 75% rename from src/modules/territories/domain/dtos/find-for-point.request.ts rename to src/modules/territories/domain/dtos/find-all-territories-for-point.request.ts index 79df476..40d7eb6 100644 --- a/src/modules/territories/domain/dtos/find-for-point.request.ts +++ b/src/modules/territories/domain/dtos/find-all-territories-for-point.request.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, IsNumber } from 'class-validator'; -export class FindForPointRequest { +export class FindAllTerritoriesForPointRequest { @IsNumber() @IsNotEmpty() lon: number; diff --git a/src/modules/territories/domain/dtos/find-all-territories.request.ts b/src/modules/territories/domain/dtos/find-all-territories.request.ts new file mode 100644 index 0000000..59f28ca --- /dev/null +++ b/src/modules/territories/domain/dtos/find-all-territories.request.ts @@ -0,0 +1,11 @@ +import { IsInt, IsOptional } from 'class-validator'; + +export class FindAllTerritoriesRequest { + @IsInt() + @IsOptional() + page?: number; + + @IsInt() + @IsOptional() + perPage?: number; +} diff --git a/src/modules/territories/domain/dtos/find-territory-by-uuid.request.ts b/src/modules/territories/domain/dtos/find-territory-by-uuid.request.ts new file mode 100644 index 0000000..2d9f382 --- /dev/null +++ b/src/modules/territories/domain/dtos/find-territory-by-uuid.request.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class FindTerritoryByUuidRequest { + @IsString() + @IsNotEmpty() + uuid: string; +} diff --git a/src/modules/territories/domain/dtos/update-territory.request.ts b/src/modules/territories/domain/dtos/update-territory.request.ts new file mode 100644 index 0000000..0d97070 --- /dev/null +++ b/src/modules/territories/domain/dtos/update-territory.request.ts @@ -0,0 +1,19 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class UpdateTerritoryRequest { + @IsString() + @IsNotEmpty() + @AutoMap() + uuid: string; + + @IsString() + @IsOptional() + @AutoMap() + name?: string; + + @IsString() + @IsOptional() + @AutoMap() + shape?: string; +} diff --git a/src/modules/territories/domain/entities/territory.ts b/src/modules/territories/domain/entities/territory.ts index 6e673f2..181915c 100644 --- a/src/modules/territories/domain/entities/territory.ts +++ b/src/modules/territories/domain/entities/territory.ts @@ -6,4 +6,7 @@ export class Territory { @AutoMap() name: string; + + @AutoMap() + shape: string; } diff --git a/src/modules/territories/domain/usecases/create-territory.usecase.ts b/src/modules/territories/domain/usecases/create-territory.usecase.ts new file mode 100644 index 0000000..0e05c2c --- /dev/null +++ b/src/modules/territories/domain/usecases/create-territory.usecase.ts @@ -0,0 +1,50 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { CommandHandler } from '@nestjs/cqrs'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { CreateTerritoryCommand } from '../../commands/create-territory.command'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { Territory } from '../entities/territory'; +import { CreateTerritoryRequest } from '../dtos/create-territory.request'; + +@CommandHandler(CreateTerritoryCommand) +export class CreateTerritoryUseCase { + constructor( + private readonly _repository: TerritoriesRepository, + private readonly _territoryMessager: TerritoryMessager, + private readonly _loggingMessager: LoggingMessager, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(command: CreateTerritoryCommand): Promise { + const entity = this._mapper.map( + command.createTerritoryRequest, + CreateTerritoryRequest, + Territory, + ); + + try { + const territory = await this._repository.create(entity); + this._territoryMessager.publish('create', JSON.stringify(territory)); + this._loggingMessager.publish( + 'territory.create.info', + JSON.stringify(territory), + ); + return territory; + } catch (error) { + let key = 'territory.create.crit'; + if (error.message.includes('Already exists')) { + key = 'territory.create.warning'; + } + this._loggingMessager.publish( + key, + JSON.stringify({ + command, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/territories/domain/usecases/delete-territory.usecase.ts b/src/modules/territories/domain/usecases/delete-territory.usecase.ts new file mode 100644 index 0000000..f59c5e2 --- /dev/null +++ b/src/modules/territories/domain/usecases/delete-territory.usecase.ts @@ -0,0 +1,39 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { DeleteTerritoryCommand } from '../../commands/delete-territory.command'; +import { Territory } from '../entities/territory'; + +@CommandHandler(DeleteTerritoryCommand) +export class DeleteTerritoryUseCase { + constructor( + private readonly _repository: TerritoriesRepository, + private readonly _territoryMessager: TerritoryMessager, + private readonly _loggingMessager: LoggingMessager, + ) {} + + async execute(command: DeleteTerritoryCommand): Promise { + try { + const territory = await this._repository.delete(command.uuid); + this._territoryMessager.publish( + 'delete', + JSON.stringify({ uuid: territory.uuid }), + ); + this._loggingMessager.publish( + 'territory.delete.info', + JSON.stringify({ uuid: territory.uuid }), + ); + return territory; + } catch (error) { + this._loggingMessager.publish( + 'territory.delete.crit', + JSON.stringify({ + command, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/territories/domain/usecases/find-for-point.usecase.ts b/src/modules/territories/domain/usecases/find-all-territories-for-point.usecase.ts similarity index 55% rename from src/modules/territories/domain/usecases/find-for-point.usecase.ts rename to src/modules/territories/domain/usecases/find-all-territories-for-point.usecase.ts index a385e39..c39701d 100644 --- a/src/modules/territories/domain/usecases/find-for-point.usecase.ts +++ b/src/modules/territories/domain/usecases/find-all-territories-for-point.usecase.ts @@ -1,19 +1,22 @@ import { QueryHandler } from '@nestjs/cqrs'; import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; -import { FindForPointQuery } from '../../queries/find-for-point.query'; +import { FindAllTerritoriesForPointQuery } from '../../queries/find-all-territories-for-point.query'; import { Territory } from '../entities/territory'; import { Point } from '../entities/point'; -@QueryHandler(FindForPointQuery) -export class FindForPointUseCase { +@QueryHandler(FindAllTerritoriesForPointQuery) +export class FindAllTerritoriesForPointUseCase { constructor(private readonly _repository: TerritoriesRepository) {} async execute( - findForPointQuery: FindForPointQuery, + findAllTerritoriesForPointQuery: FindAllTerritoriesForPointQuery, ): Promise> { return this._repository.findForPoint( - new Point(findForPointQuery.point.lon, findForPointQuery.point.lat), + new Point( + findAllTerritoriesForPointQuery.point.lon, + findAllTerritoriesForPointQuery.point.lat, + ), ); } } diff --git a/src/modules/territories/domain/usecases/find-all-territories.usecase.ts b/src/modules/territories/domain/usecases/find-all-territories.usecase.ts new file mode 100644 index 0000000..dadb3f9 --- /dev/null +++ b/src/modules/territories/domain/usecases/find-all-territories.usecase.ts @@ -0,0 +1,19 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { FindAllTerritoriesQuery } from '../../queries/find-all-territories.query'; +import { Territory } from '../entities/territory'; + +@QueryHandler(FindAllTerritoriesQuery) +export class FindAllTerritoriesUseCase { + constructor(private readonly _repository: TerritoriesRepository) {} + + async execute( + findAllTerritoriesQuery: FindAllTerritoriesQuery, + ): Promise> { + return this._repository.findAll( + findAllTerritoriesQuery.page, + findAllTerritoriesQuery.perPage, + ); + } +} diff --git a/src/modules/territories/domain/usecases/find-territory-by-uuid.usecase.ts b/src/modules/territories/domain/usecases/find-territory-by-uuid.usecase.ts new file mode 100644 index 0000000..7451ee2 --- /dev/null +++ b/src/modules/territories/domain/usecases/find-territory-by-uuid.usecase.ts @@ -0,0 +1,35 @@ +import { NotFoundException } from '@nestjs/common'; +import { QueryHandler } from '@nestjs/cqrs'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { FindTerritoryByUuidQuery } from '../../queries/find-territory-by-uuid.query'; +import { Territory } from '../entities/territory'; + +@QueryHandler(FindTerritoryByUuidQuery) +export class FindTerritoryByUuidUseCase { + constructor( + private readonly _repository: TerritoriesRepository, + private readonly _loggingMessager: LoggingMessager, + ) {} + + async execute( + findTerritoryByUuidQuery: FindTerritoryByUuidQuery, + ): Promise { + try { + const territory = await this._repository.findOneByUuid( + findTerritoryByUuidQuery.uuid, + ); + if (!territory) throw new NotFoundException(); + return territory; + } catch (error) { + this._loggingMessager.publish( + 'territory.read.warning', + JSON.stringify({ + query: findTerritoryByUuidQuery, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/territories/domain/usecases/update-territory.usecase.ts b/src/modules/territories/domain/usecases/update-territory.usecase.ts new file mode 100644 index 0000000..833de08 --- /dev/null +++ b/src/modules/territories/domain/usecases/update-territory.usecase.ts @@ -0,0 +1,52 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { CommandHandler } from '@nestjs/cqrs'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { UpdateTerritoryCommand } from '../../commands/update-territory.command'; +import { UpdateTerritoryRequest } from '../dtos/update-territory.request'; +import { Territory } from '../entities/territory'; + +@CommandHandler(UpdateTerritoryCommand) +export class UpdateTerritoryUseCase { + constructor( + private readonly _repository: TerritoriesRepository, + private readonly _territoryMessager: TerritoryMessager, + private readonly _loggingMessager: LoggingMessager, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(command: UpdateTerritoryCommand): Promise { + const entity = this._mapper.map( + command.updateTerritoryRequest, + UpdateTerritoryRequest, + Territory, + ); + + try { + const territory = await this._repository.update( + command.updateTerritoryRequest.uuid, + entity, + ); + this._territoryMessager.publish( + 'update', + JSON.stringify(command.updateTerritoryRequest), + ); + this._loggingMessager.publish( + 'territory.update.info', + JSON.stringify(command.updateTerritoryRequest), + ); + return territory; + } catch (error) { + this._loggingMessager.publish( + 'territory.update.crit', + JSON.stringify({ + command, + error, + }), + ); + throw error; + } + } +} diff --git a/src/modules/territories/mappers/territory.profile.ts b/src/modules/territories/mappers/territory.profile.ts index 872070b..cea7fa3 100644 --- a/src/modules/territories/mappers/territory.profile.ts +++ b/src/modules/territories/mappers/territory.profile.ts @@ -1,7 +1,9 @@ -import { createMap, Mapper } from '@automapper/core'; +import { createMap, forMember, ignore, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { TerritoryPresenter } from '../adapters/primaries/territory.presenter'; +import { CreateTerritoryRequest } from '../domain/dtos/create-territory.request'; +import { UpdateTerritoryRequest } from '../domain/dtos/update-territory.request'; import { Territory } from '../domain/entities/territory'; @Injectable() @@ -13,6 +15,15 @@ export class TerritoryProfile extends AutomapperProfile { override get profile() { return (mapper) => { createMap(mapper, Territory, TerritoryPresenter); + + createMap(mapper, CreateTerritoryRequest, Territory); + + createMap( + mapper, + UpdateTerritoryRequest, + Territory, + forMember((dest) => dest.uuid, ignore()), + ); }; } } diff --git a/src/modules/territories/queries/find-all-territories-for-point.query.ts b/src/modules/territories/queries/find-all-territories-for-point.query.ts new file mode 100644 index 0000000..1fec1df --- /dev/null +++ b/src/modules/territories/queries/find-all-territories-for-point.query.ts @@ -0,0 +1,15 @@ +import { FindAllTerritoriesForPointRequest } from '../domain/dtos/find-all-territories-for-point.request'; +import { Point } from '../domain/entities/point'; + +export class FindAllTerritoriesForPointQuery { + point: Point; + + constructor( + findAllTerritoriesForPointRequest?: FindAllTerritoriesForPointRequest, + ) { + this.point = new Point( + findAllTerritoriesForPointRequest.lon, + findAllTerritoriesForPointRequest.lat, + ); + } +} diff --git a/src/modules/territories/queries/find-all-territories.query.ts b/src/modules/territories/queries/find-all-territories.query.ts new file mode 100644 index 0000000..0885941 --- /dev/null +++ b/src/modules/territories/queries/find-all-territories.query.ts @@ -0,0 +1,11 @@ +import { FindAllTerritoriesRequest } from '../domain/dtos/find-all-territories.request'; + +export class FindAllTerritoriesQuery { + page: number; + perPage: number; + + constructor(findAllTerritoriesRequest?: FindAllTerritoriesRequest) { + this.page = findAllTerritoriesRequest?.page ?? 1; + this.perPage = findAllTerritoriesRequest?.perPage ?? 10; + } +} diff --git a/src/modules/territories/queries/find-for-point.query.ts b/src/modules/territories/queries/find-for-point.query.ts deleted file mode 100644 index 343bc6e..0000000 --- a/src/modules/territories/queries/find-for-point.query.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FindForPointRequest } from '../domain/dtos/find-for-point.request'; -import { Point } from '../domain/entities/point'; - -export class FindForPointQuery { - point: Point; - - constructor(findForPointRequest?: FindForPointRequest) { - this.point = new Point(findForPointRequest.lon, findForPointRequest.lat); - } -} diff --git a/src/modules/territories/queries/find-territory-by-uuid.query.ts b/src/modules/territories/queries/find-territory-by-uuid.query.ts new file mode 100644 index 0000000..1038873 --- /dev/null +++ b/src/modules/territories/queries/find-territory-by-uuid.query.ts @@ -0,0 +1,9 @@ +import { FindTerritoryByUuidRequest } from '../domain/dtos/find-territory-by-uuid.request'; + +export class FindTerritoryByUuidQuery { + readonly uuid: string; + + constructor(findTerritoryByUuidRequest: FindTerritoryByUuidRequest) { + this.uuid = findTerritoryByUuidRequest.uuid; + } +} diff --git a/src/modules/territories/territories.module.ts b/src/modules/territories/territories.module.ts index 59c3347..fc2a78b 100644 --- a/src/modules/territories/territories.module.ts +++ b/src/modules/territories/territories.module.ts @@ -9,7 +9,12 @@ import { TerritoriesController } from './adapters/primaries/territories.controll import { LoggingMessager } from './adapters/secondaries/logging.messager'; import { TerritoriesRepository } from './adapters/secondaries/territories.repository'; import { TerritoryMessager } from './adapters/secondaries/territory.messager'; -import { FindForPointUseCase } from './domain/usecases/find-for-point.usecase'; +import { CreateTerritoryUseCase } from './domain/usecases/create-territory.usecase'; +import { DeleteTerritoryUseCase } from './domain/usecases/delete-territory.usecase'; +import { FindAllTerritoriesForPointUseCase } from './domain/usecases/find-all-territories-for-point.usecase'; +import { FindAllTerritoriesUseCase } from './domain/usecases/find-all-territories.usecase'; +import { FindTerritoryByUuidUseCase } from './domain/usecases/find-territory-by-uuid.usecase'; +import { UpdateTerritoryUseCase } from './domain/usecases/update-territory.usecase'; import { TerritoryProfile } from './mappers/territory.profile'; @Module({ @@ -52,7 +57,12 @@ import { TerritoryProfile } from './mappers/territory.profile'; TerritoriesRepository, TerritoryMessager, LoggingMessager, - FindForPointUseCase, + FindAllTerritoriesForPointUseCase, + FindAllTerritoriesUseCase, + FindTerritoryByUuidUseCase, + CreateTerritoryUseCase, + UpdateTerritoryUseCase, + DeleteTerritoryUseCase, ], exports: [], }) diff --git a/src/modules/territories/tests/unit/create-territory.usecase.spec.ts b/src/modules/territories/tests/unit/create-territory.usecase.spec.ts new file mode 100644 index 0000000..db86c5e --- /dev/null +++ b/src/modules/territories/tests/unit/create-territory.usecase.spec.ts @@ -0,0 +1,88 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { CreateTerritoryCommand } from '../../commands/create-territory.command'; +import { CreateTerritoryRequest } from '../../domain/dtos/create-territory.request'; +import { Territory } from '../../domain/entities/territory'; +import { CreateTerritoryUseCase } from '../../domain/usecases/create-territory.usecase'; +import { TerritoryProfile } from '../../mappers/territory.profile'; + +const newTerritoryRequest: CreateTerritoryRequest = { + name: 'Grand Est', + shape: 'grand-est-binary-shape', +}; +const newTerritoryCommand: CreateTerritoryCommand = new CreateTerritoryCommand( + newTerritoryRequest, +); + +const mockTerritoriesRepository = { + create: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ + ...newTerritoryRequest, + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + }); + }) + .mockImplementation(() => { + throw new Error('Already exists'); + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('CreateTerritoryUseCase', () => { + let createTerritoryUseCase: CreateTerritoryUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + CreateTerritoryUseCase, + TerritoryProfile, + { + provide: TerritoryMessager, + useValue: mockMessager, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + ], + }).compile(); + + createTerritoryUseCase = module.get( + CreateTerritoryUseCase, + ); + }); + + it('should be defined', () => { + expect(createTerritoryUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should create and return a new territory', async () => { + const newTerritory: Territory = await createTerritoryUseCase.execute( + newTerritoryCommand, + ); + + expect(newTerritory.name).toBe(newTerritoryRequest.name); + expect(newTerritory.uuid).toBeDefined(); + }); + + it('should throw an error if territory already exists', async () => { + await expect( + createTerritoryUseCase.execute(newTerritoryCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/modules/territories/tests/unit/delete-territory.usecase.spec.ts b/src/modules/territories/tests/unit/delete-territory.usecase.spec.ts new file mode 100644 index 0000000..0d8afa1 --- /dev/null +++ b/src/modules/territories/tests/unit/delete-territory.usecase.spec.ts @@ -0,0 +1,98 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { DeleteTerritoryCommand } from '../../commands/delete-territory.command'; +import { DeleteTerritoryUseCase } from '../../domain/usecases/delete-territory.usecase'; + +const mockTerritories = [ + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + name: 'Grand Est', + shape: 'grand-est-binary-shape', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a92', + name: 'Nouvelle Aquitaine', + shape: 'nouvelle-aquitaine-binary-shape', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a93', + name: 'Occitanie', + shape: 'occitanie-binary-shape', + }, +]; + +const mockTerritoriesRepository = { + delete: jest + .fn() + .mockImplementationOnce((uuid: string) => { + let savedTerritory = {}; + mockTerritories.forEach((territory, index) => { + if (territory.uuid === uuid) { + savedTerritory = { ...territory }; + mockTerritories.splice(index, 1); + } + }); + return savedTerritory; + }) + .mockImplementation(() => { + throw new Error('Error'); + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('DeleteTerritoryUseCase', () => { + let deleteTerritoryUseCase: DeleteTerritoryUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + DeleteTerritoryUseCase, + { + provide: TerritoryMessager, + useValue: mockMessager, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + ], + }).compile(); + + deleteTerritoryUseCase = module.get( + DeleteTerritoryUseCase, + ); + }); + + it('should be defined', () => { + expect(deleteTerritoryUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should delete a territory', async () => { + const savedUuid = mockTerritories[0].uuid; + const deleteTerritoryCommand = new DeleteTerritoryCommand(savedUuid); + await deleteTerritoryUseCase.execute(deleteTerritoryCommand); + + const deletedTerritory = mockTerritories.find( + (territory) => territory.uuid === savedUuid, + ); + expect(deletedTerritory).toBeUndefined(); + }); + it('should throw an error if territory does not exist', async () => { + await expect( + deleteTerritoryUseCase.execute( + new DeleteTerritoryCommand('wrong uuid'), + ), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/modules/territories/tests/unit/find-all-territories-for-point.usecase.spec.ts b/src/modules/territories/tests/unit/find-all-territories-for-point.usecase.spec.ts new file mode 100644 index 0000000..0f40224 --- /dev/null +++ b/src/modules/territories/tests/unit/find-all-territories-for-point.usecase.spec.ts @@ -0,0 +1,68 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { FindAllTerritoriesForPointRequest } from '../../domain/dtos/find-all-territories-for-point.request'; +import { FindAllTerritoriesForPointUseCase } from '../../domain/usecases/find-all-territories-for-point.usecase'; +import { FindAllTerritoriesForPointQuery } from '../../queries/find-all-territories-for-point.query'; + +const findAllTerritoriesForPointRequest: FindAllTerritoriesForPointRequest = + new FindAllTerritoriesForPointRequest(); +findAllTerritoriesForPointRequest.lon = 6.181455; +findAllTerritoriesForPointRequest.lat = 48.685689; + +const findforPointQuery: FindAllTerritoriesForPointQuery = + new FindAllTerritoriesForPointQuery(findAllTerritoriesForPointRequest); + +const mockTerritories = [ + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + name: 'Nancy', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a92', + name: 'Meurthe-et-Moselle', + }, +]; + +const mockTerritoriesRepository = { + findForPoint: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementation((query?: FindAllTerritoriesForPointQuery) => { + return Promise.resolve(mockTerritories); + }), +}; + +describe('FindAllTerritoriesforPointUseCase', () => { + let findAllTerritoriesForPointUseCase: FindAllTerritoriesForPointUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + FindAllTerritoriesForPointUseCase, + ], + }).compile(); + + findAllTerritoriesForPointUseCase = + module.get( + FindAllTerritoriesForPointUseCase, + ); + }); + + it('should be defined', () => { + expect(findAllTerritoriesForPointUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should return an array filled with territories', async () => { + const territories = await findAllTerritoriesForPointUseCase.execute( + findforPointQuery, + ); + + expect(territories).toBe(mockTerritories); + }); + }); +}); diff --git a/src/modules/territories/tests/unit/find-all-territories.usecase.spec.ts b/src/modules/territories/tests/unit/find-all-territories.usecase.spec.ts new file mode 100644 index 0000000..4e4aaf4 --- /dev/null +++ b/src/modules/territories/tests/unit/find-all-territories.usecase.spec.ts @@ -0,0 +1,72 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { FindAllTerritoriesRequest } from '../../domain/dtos/find-all-territories.request'; +import { FindAllTerritoriesUseCase } from '../../domain/usecases/find-all-territories.usecase'; +import { FindAllTerritoriesQuery } from '../../queries/find-all-territories.query'; + +const findAllTerritoriesRequest: FindAllTerritoriesRequest = + new FindAllTerritoriesRequest(); +findAllTerritoriesRequest.page = 1; +findAllTerritoriesRequest.perPage = 10; + +const findAllTerritoriesQuery: FindAllTerritoriesQuery = + new FindAllTerritoriesQuery(findAllTerritoriesRequest); + +const mockTerritories = [ + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + name: 'Grand Est', + shape: 'grand-est-binary-shape', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a92', + name: 'Nouvelle Aquitaine', + shape: 'nouvelle-aquitaine-binary-shape', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a93', + name: 'Occitanie', + shape: 'occitanie-binary-shape', + }, +]; + +const mockTerritoriesRepository = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + findAll: jest.fn().mockImplementation((query?: FindAllTerritoriesQuery) => { + return Promise.resolve(mockTerritories); + }), +}; + +describe('FindAllTerritoriesUseCase', () => { + let findAllTerritoriesUseCase: FindAllTerritoriesUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + FindAllTerritoriesUseCase, + ], + }).compile(); + + findAllTerritoriesUseCase = module.get( + FindAllTerritoriesUseCase, + ); + }); + + it('should be defined', () => { + expect(findAllTerritoriesUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should return an array filled with territories', async () => { + const territories = await findAllTerritoriesUseCase.execute( + findAllTerritoriesQuery, + ); + + expect(territories).toBe(mockTerritories); + }); + }); +}); diff --git a/src/modules/territories/tests/unit/find-for-point.usecase.spec.ts b/src/modules/territories/tests/unit/find-for-point.usecase.spec.ts deleted file mode 100644 index 6f6bc2a..0000000 --- a/src/modules/territories/tests/unit/find-for-point.usecase.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; -import { FindForPointRequest } from '../../domain/dtos/find-for-point.request'; -import { FindForPointUseCase } from '../../domain/usecases/find-for-point.usecase'; -import { FindForPointQuery } from '../../queries/find-for-point.query'; - -const findForPointRequest: FindForPointRequest = new FindForPointRequest(); -findForPointRequest.lon = 6.181455; -findForPointRequest.lat = 48.685689; - -const findforPointQuery: FindForPointQuery = new FindForPointQuery( - findForPointRequest, -); - -const mockTerritories = [ - { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - name: 'Nancy', - }, - { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a92', - name: 'Meurthe-et-Moselle', - }, -]; - -const mockTerritoriesRepository = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - findForPoint: jest.fn().mockImplementation((query?: FindForPointQuery) => { - return Promise.resolve(mockTerritories); - }), -}; - -describe('FindforPointUseCase', () => { - let findForPointUseCase: FindForPointUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: TerritoriesRepository, - useValue: mockTerritoriesRepository, - }, - FindForPointUseCase, - ], - }).compile(); - - findForPointUseCase = module.get(FindForPointUseCase); - }); - - it('should be defined', () => { - expect(findForPointUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should return an array filled with territories', async () => { - const territories = await findForPointUseCase.execute(findforPointQuery); - - expect(territories).toBe(mockTerritories); - }); - }); -}); diff --git a/src/modules/territories/tests/unit/find-territory-by-uuid.usecase.spec.ts b/src/modules/territories/tests/unit/find-territory-by-uuid.usecase.spec.ts new file mode 100644 index 0000000..5006fef --- /dev/null +++ b/src/modules/territories/tests/unit/find-territory-by-uuid.usecase.spec.ts @@ -0,0 +1,80 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { FindTerritoryByUuidRequest } from '../../domain/dtos/find-territory-by-uuid.request'; +import { FindTerritoryByUuidUseCase } from '../../domain/usecases/find-territory-by-uuid.usecase'; +import { FindTerritoryByUuidQuery } from '../../queries/find-territory-by-uuid.query'; + +const mockTerritory = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + name: 'Grand Est', + shape: 'grand-est-binary-shape', +}; + +const mockTerritoriesRepository = { + findOneByUuid: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((query?: FindTerritoryByUuidQuery) => { + return Promise.resolve(mockTerritory); + }) + .mockImplementation(() => { + return Promise.resolve(undefined); + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('FindTerritoryByUuidUseCase', () => { + let findTerritoryByUuidUseCase: FindTerritoryByUuidUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + FindTerritoryByUuidUseCase, + ], + }).compile(); + + findTerritoryByUuidUseCase = module.get( + FindTerritoryByUuidUseCase, + ); + }); + + it('should be defined', () => { + expect(findTerritoryByUuidUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should return a territory', async () => { + const findTerritoryByUuidRequest: FindTerritoryByUuidRequest = + new FindTerritoryByUuidRequest(); + findTerritoryByUuidRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; + const territory = await findTerritoryByUuidUseCase.execute( + new FindTerritoryByUuidQuery(findTerritoryByUuidRequest), + ); + expect(territory).toBe(mockTerritory); + }); + it('should throw an error if territory does not exist', async () => { + const findTerritoryByUuidRequest: FindTerritoryByUuidRequest = + new FindTerritoryByUuidRequest(); + findTerritoryByUuidRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90'; + await expect( + findTerritoryByUuidUseCase.execute( + new FindTerritoryByUuidQuery(findTerritoryByUuidRequest), + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/src/modules/territories/tests/unit/update-territory.usecase.spec.ts b/src/modules/territories/tests/unit/update-territory.usecase.spec.ts new file mode 100644 index 0000000..b13876b --- /dev/null +++ b/src/modules/territories/tests/unit/update-territory.usecase.spec.ts @@ -0,0 +1,90 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; +import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository'; +import { TerritoryMessager } from '../../adapters/secondaries/territory.messager'; +import { UpdateTerritoryCommand } from '../../commands/update-territory.command'; +import { UpdateTerritoryRequest } from '../../domain/dtos/update-territory.request'; +import { Territory } from '../../domain/entities/territory'; +import { UpdateTerritoryUseCase } from '../../domain/usecases/update-territory.usecase'; +import { TerritoryProfile } from '../../mappers/territory.profile'; + +const originalTerritory: Territory = new Territory(); +originalTerritory.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +originalTerritory.name = 'Grand Est'; + +const updateTerritoryRequest: UpdateTerritoryRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + name: 'Grand-Est', +}; + +const updateTerritoryCommand: UpdateTerritoryCommand = + new UpdateTerritoryCommand(updateTerritoryRequest); + +const mockTerritoriesRepository = { + update: jest + .fn() + .mockImplementationOnce((uuid: string, params: any) => { + originalTerritory.name = params.name; + + return Promise.resolve(originalTerritory); + }) + .mockImplementation(() => { + throw new Error('Error'); + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('UpdateTerritoryUseCase', () => { + let updateTerritoryUseCase: UpdateTerritoryUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + + providers: [ + { + provide: TerritoriesRepository, + useValue: mockTerritoriesRepository, + }, + UpdateTerritoryUseCase, + TerritoryProfile, + { + provide: TerritoryMessager, + useValue: mockMessager, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + ], + }).compile(); + + updateTerritoryUseCase = module.get( + UpdateTerritoryUseCase, + ); + }); + + it('should be defined', () => { + expect(updateTerritoryUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should update a territory', async () => { + const updatedTerritory: Territory = await updateTerritoryUseCase.execute( + updateTerritoryCommand, + ); + + expect(updatedTerritory.name).toBe(updateTerritoryRequest.name); + }); + it('should throw an error if territory does not exist', async () => { + await expect( + updateTerritoryUseCase.execute(updateTerritoryCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts new file mode 100644 index 0000000..46695ff --- /dev/null +++ b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,22 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { UpdateTerritoryRequest } from '../../../modules/territories/domain/dtos/update-territory.request'; +import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; + +describe('RpcValidationPipe', () => { + it('should not validate request', async () => { + const target: RpcValidationPipe = new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }); + const metadata: ArgumentMetadata = { + type: 'body', + metatype: UpdateTerritoryRequest, + data: '', + }; + await target + .transform({}, metadata) + .catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +});