basic crud

This commit is contained in:
sbriat 2023-02-07 14:06:24 +01:00
parent 1da111bba9
commit 5b63d8ba1a
34 changed files with 1014 additions and 96 deletions

View File

@ -80,6 +80,11 @@
"json",
"ts"
],
"modulePathIgnorePatterns": [
".controller.ts",
".module.ts",
"main.ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {

View File

@ -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");

View File

@ -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

View File

@ -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<ICollection<Territory>> {
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<ICollection<Territory>> {
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<TerritoryPresenter> {
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<TerritoryPresenter> {
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<TerritoryPresenter> {
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<void> {
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({});
}
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
import { CreateTerritoryRequest } from '../domain/dtos/create-territory.request';
export class CreateTerritoryCommand {
readonly createTerritoryRequest: CreateTerritoryRequest;
constructor(request: CreateTerritoryRequest) {
this.createTerritoryRequest = request;
}
}

View File

@ -0,0 +1,7 @@
export class DeleteTerritoryCommand {
readonly uuid: string;
constructor(uuid: string) {
this.uuid = uuid;
}
}

View File

@ -0,0 +1,9 @@
import { UpdateTerritoryRequest } from '../domain/dtos/update-territory.request';
export class UpdateTerritoryCommand {
readonly updateTerritoryRequest: UpdateTerritoryRequest;
constructor(request: UpdateTerritoryRequest) {
this.updateTerritoryRequest = request;
}
}

View File

@ -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;
}

View File

@ -1,6 +1,6 @@
import { IsNotEmpty, IsNumber } from 'class-validator';
export class FindForPointRequest {
export class FindAllTerritoriesForPointRequest {
@IsNumber()
@IsNotEmpty()
lon: number;

View File

@ -0,0 +1,11 @@
import { IsInt, IsOptional } from 'class-validator';
export class FindAllTerritoriesRequest {
@IsInt()
@IsOptional()
page?: number;
@IsInt()
@IsOptional()
perPage?: number;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class FindTerritoryByUuidRequest {
@IsString()
@IsNotEmpty()
uuid: string;
}

View File

@ -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;
}

View File

@ -6,4 +6,7 @@ export class Territory {
@AutoMap()
name: string;
@AutoMap()
shape: string;
}

View File

@ -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<Territory> {
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;
}
}
}

View File

@ -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<Territory> {
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;
}
}
}

View File

@ -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<ICollection<Territory>> {
return this._repository.findForPoint(
new Point(findForPointQuery.point.lon, findForPointQuery.point.lat),
new Point(
findAllTerritoriesForPointQuery.point.lon,
findAllTerritoriesForPointQuery.point.lat,
),
);
}
}

View File

@ -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<ICollection<Territory>> {
return this._repository.findAll(
findAllTerritoriesQuery.page,
findAllTerritoriesQuery.perPage,
);
}
}

View File

@ -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<Territory> {
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;
}
}
}

View File

@ -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<Territory> {
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;
}
}
}

View File

@ -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()),
);
};
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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: [],
})

View File

@ -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>(
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);
});
});
});

View File

@ -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>(
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);
});
});
});

View File

@ -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>(
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);
});
});
});

View File

@ -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>(
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);
});
});
});

View File

@ -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>(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);
});
});
});

View File

@ -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>(
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);
});
});
});

View File

@ -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>(
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);
});
});
});

View File

@ -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(<UpdateTerritoryRequest>{}, metadata)
.catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
});
});