add health check rest service

This commit is contained in:
sbriat 2023-04-04 10:52:30 +02:00
parent 484ac4cf1a
commit 499404700f
30 changed files with 563 additions and 55 deletions

View File

@ -2,6 +2,7 @@
SERVICE_URL=0.0.0.0
SERVICE_PORT=5004
SERVICE_CONFIGURATION_DOMAIN=TERRITORY
HEALTH_SERVICE_PORT=6004
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=territory"

View File

@ -14,6 +14,7 @@ services:
command: npm run start:dev
ports:
- ${SERVICE_PORT:-5004}:${SERVICE_PORT:-5004}
- ${HEALTH_SERVICE_PORT:-6004}:${HEALTH_SERVICE_PORT:-6004}
networks:
v3-network:
aliases:

221
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@nestjs/cqrs": "^9.0.1",
"@nestjs/microservices": "^9.3.2",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.9.0",
"cache-manager": "^5.1.5",
"cache-manager-ioredis-yet": "^1.1.0",
@ -1973,6 +1974,71 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/@nestjs/terminus": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-9.2.2.tgz",
"integrity": "sha512-AWUA8XLcgxWUjUFYHDqi42M7CZn2e+DEWxP+MqNAbMzz4ybB5jGcFK5Fy8qwaNBoWg6KMF1JiXOOygGXgk9ydg==",
"dependencies": {
"boxen": "5.1.2",
"check-disk-space": "3.3.1"
},
"peerDependencies": {
"@grpc/grpc-js": "*",
"@grpc/proto-loader": "*",
"@mikro-orm/core": "*",
"@mikro-orm/nestjs": "*",
"@nestjs/axios": "*",
"@nestjs/common": "9.x",
"@nestjs/core": "9.x",
"@nestjs/microservices": "*",
"@nestjs/mongoose": "*",
"@nestjs/sequelize": "*",
"@nestjs/typeorm": "*",
"mongoose": "*",
"reflect-metadata": "0.1.x",
"rxjs": "7.x",
"sequelize": "*",
"typeorm": "*"
},
"peerDependenciesMeta": {
"@grpc/grpc-js": {
"optional": true
},
"@grpc/proto-loader": {
"optional": true
},
"@mikro-orm/core": {
"optional": true
},
"@mikro-orm/nestjs": {
"optional": true
},
"@nestjs/axios": {
"optional": true
},
"@nestjs/microservices": {
"optional": true
},
"@nestjs/mongoose": {
"optional": true
},
"@nestjs/sequelize": {
"optional": true
},
"@nestjs/typeorm": {
"optional": true
},
"mongoose": {
"optional": true
},
"sequelize": {
"optional": true
},
"typeorm": {
"optional": true
}
}
},
"node_modules/@nestjs/testing": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.3.2.tgz",
@ -2939,6 +3005,14 @@
"node": ">=10"
}
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"dependencies": {
"string-width": "^4.1.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -3301,6 +3375,53 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"dependencies": {
"ansi-align": "^3.0.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.0",
"cli-boxes": "^2.2.1",
"string-width": "^4.2.2",
"type-fest": "^0.20.2",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -3520,6 +3641,14 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"node_modules/check-disk-space": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.3.1.tgz",
"integrity": "sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -3592,6 +3721,17 @@
"validator": "^13.7.0"
}
},
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -8674,7 +8814,6 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -8971,6 +9110,17 @@
"node": ">= 8"
}
},
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
"dependencies": {
"string-width": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/windows-release": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz",
@ -10577,6 +10727,15 @@
}
}
},
"@nestjs/terminus": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-9.2.2.tgz",
"integrity": "sha512-AWUA8XLcgxWUjUFYHDqi42M7CZn2e+DEWxP+MqNAbMzz4ybB5jGcFK5Fy8qwaNBoWg6KMF1JiXOOygGXgk9ydg==",
"requires": {
"boxen": "5.1.2",
"check-disk-space": "3.3.1"
}
},
"@nestjs/testing": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.3.2.tgz",
@ -11367,6 +11526,14 @@
"url-parse": "~1.5.1"
}
},
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"requires": {
"string-width": "^4.1.0"
}
},
"ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -11655,6 +11822,37 @@
}
}
},
"boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"requires": {
"ansi-align": "^3.0.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.0",
"cli-boxes": "^2.2.1",
"string-width": "^4.2.2",
"type-fest": "^0.20.2",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"dependencies": {
"camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -11804,6 +12002,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"check-disk-space": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.3.1.tgz",
"integrity": "sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ=="
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -11853,6 +12056,11 @@
"validator": "^13.7.0"
}
},
"cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw=="
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -15622,8 +15830,7 @@
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
},
"type-is": {
"version": "1.6.18",
@ -15836,6 +16043,14 @@
"isexe": "^2.0.0"
}
},
"widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
"requires": {
"string-width": "^4.0.0"
}
},
"windows-release": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz",

View File

@ -44,6 +44,7 @@
"@nestjs/cqrs": "^9.0.1",
"@nestjs/microservices": "^9.3.2",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.9.0",
"cache-manager": "^5.1.5",
"cache-manager-ioredis-yet": "^1.1.0",
@ -89,6 +90,7 @@
"modulePathIgnorePatterns": [
".controller.ts",
".module.ts",
".request.ts",
"main.ts"
],
"rootDir": "src",

View File

@ -4,24 +4,24 @@ import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.GRPC,
options: {
package: ['territory', 'health'],
protoPath: [
join(
__dirname,
'modules/territory/adapters/primaries/territory.proto',
),
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
loader: { keepCase: true },
},
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
});
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
package: ['territory', 'health'],
protoPath: [
join(__dirname, 'modules/territory/adapters/primaries/territory.proto'),
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
loader: { keepCase: true },
},
);
await app.listen();
});
await app.startAllMicroservices();
await app.listen(process.env.HEALTH_SERVICE_PORT);
}
bootstrap();

View File

@ -236,4 +236,21 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
}
}
}
async healthCheck(): Promise<boolean> {
try {
await this._prisma.$queryRaw`SELECT 1`;
return true;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
e.code,
e.message,
);
} else {
throw new DatabaseException();
}
}
}
}

View File

@ -87,6 +87,23 @@ const mockPrismaService = {
.mockImplementationOnce((fields: object) => {
throw new Error('an unknown error');
}),
$queryRaw: jest
.fn()
.mockImplementationOnce(() => {
throw new PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
})
.mockImplementationOnce(() => {
return true;
})
.mockImplementation(() => {
throw new PrismaClientKnownRequestError('Database unavailable', {
code: 'code',
clientVersion: 'version',
});
}),
fake: {
create: jest
.fn()
@ -532,4 +549,23 @@ describe('PrismaRepository', () => {
).rejects.toBeInstanceOf(DatabaseException);
});
});
describe('healthCheck', () => {
it('should throw a DatabaseException for client error', async () => {
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
DatabaseException,
);
});
it('should return a healthy result', async () => {
const res = await fakeRepository.healthCheck();
expect(res).toBeTruthy();
});
it('should throw an exception if database is not available', async () => {
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
DatabaseException,
);
});
});
});

View File

@ -0,0 +1,42 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
enum ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
}
interface HealthCheckRequest {
service: string;
}
interface HealthCheckResponse {
status: ServingStatus;
}
@Controller()
export class HealthServerController {
constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
) {}
@GrpcMethod('Health', 'Check')
async check(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: HealthCheckRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
metadata: any,
): Promise<HealthCheckResponse> {
const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy(
'prisma',
);
return {
status:
healthCheck['prisma'].status == 'up'
? ServingStatus.SERVING
: ServingStatus.NOT_SERVING,
};
}
}

View File

@ -1,27 +1,34 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
import { Messager } from '../secondaries/messager';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
enum ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
}
interface HealthCheckRequest {
service: string;
}
interface HealthCheckResponse {
status: ServingStatus;
}
@Controller()
@Controller('health')
export class HealthController {
@GrpcMethod('Health', 'Check')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
check(data: HealthCheckRequest, metadata: any): HealthCheckResponse {
return {
status: ServingStatus.SERVING,
};
constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
private _healthCheckService: HealthCheckService,
private _messager: Messager,
) {}
@Get()
@HealthCheck()
async check() {
try {
return await this._healthCheckService.check([
async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'),
]);
} catch (error) {
const healthCheckResult: HealthCheckResult = error.response;
this._messager.publish(
'logging.territory.health.crit',
JSON.stringify(healthCheckResult.error),
);
throw error;
}
}
}

View File

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

View File

@ -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<string>('RMQ_EXCHANGE'));
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { TerritoriesRepository } from '../../../territory/adapters/secondaries/territories.repository';
@Injectable()
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
constructor(private readonly _repository: TerritoriesRepository) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await this._repository.healthCheck();
return this.getStatus(key, true);
} catch (e) {
throw new HealthCheckError('Prisma', {
prisma: e.message,
});
}
}
}

View File

@ -1,7 +1,34 @@
import { Module } from '@nestjs/common';
import { HealthServerController } from './adapters/primaries/health-server.controller';
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
import { TerritoriesRepository } from '../territory/adapters/secondaries/territories.repository';
import { DatabaseModule } from '../database/database.module';
import { HealthController } from './adapters/primaries/health.controller';
import { TerminusModule } from '@nestjs/terminus';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Messager } from './adapters/secondaries/messager';
@Module({
controllers: [HealthController],
imports: [
TerminusModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
DatabaseModule,
],
controllers: [HealthServerController, HealthController],
providers: [PrismaHealthIndicatorUseCase, TerritoriesRepository, Messager],
})
export class HealthModule {}

View File

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

View File

@ -0,0 +1,58 @@
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';
const mockTerritoriesRepository = {
healthCheck: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new PrismaClientKnownRequestError('Service unavailable', {
code: 'code',
clientVersion: 'version',
});
}),
};
describe('PrismaHealthIndicatorUseCase', () => {
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: TerritoriesRepository,
useValue: mockTerritoriesRepository,
},
PrismaHealthIndicatorUseCase,
],
}).compile();
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
PrismaHealthIndicatorUseCase,
);
});
it('should be defined', () => {
expect(prismaHealthIndicatorUseCase).toBeDefined();
});
describe('execute', () => {
it('should check health successfully', async () => {
const healthIndicatorResult: HealthIndicatorResult =
await prismaHealthIndicatorUseCase.isHealthy('prisma');
expect(healthIndicatorResult['prisma'].status).toBe('up');
});
it('should throw an error if database is unavailable', async () => {
await expect(
prismaHealthIndicatorUseCase.isHealthy('prisma'),
).rejects.toBeInstanceOf(HealthCheckError);
});
});
});

View File

@ -6,7 +6,7 @@ import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { Territory } from '../../domain/entities/territory';
import { FindAllTerritoriesForPointQuery } from '../../queries/find-all-territories-for-point.query';
import { TerritoryPresenter } from './territory.presenter';
import { ICollection } from '../../../databases/src/interfaces/collection.interface';
import { ICollection } from '../../../database/src/interfaces/collection.interface';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { FindAllTerritoriesForPointRequest } from '../../domain/dtos/find-all-territories-for-point.request';
import { FindAllTerritoriesRequest } from '../../domain/dtos/find-all-territories.request';
@ -15,7 +15,7 @@ import { FindTerritoryByUuidRequest } from '../../domain/dtos/find-territory-by-
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/databases/src/exceptions/database.exception';
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';

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { TerritoryRepository } from '../../../databases/src/domain/territory-repository';
import { TerritoryRepository } from '../../../database/src/domain/territory-repository';
import { Territory } from '../../domain/entities/territory';
@Injectable()

View File

@ -1,5 +1,5 @@
import { QueryHandler } from '@nestjs/cqrs';
import { ICollection } from 'src/modules/databases/src/interfaces/collection.interface';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository';
import { FindAllTerritoriesForPointQuery } from '../../queries/find-all-territories-for-point.query';
import { Territory } from '../entities/territory';

View File

@ -1,5 +1,5 @@
import { QueryHandler } from '@nestjs/cqrs';
import { ICollection } from 'src/modules/databases/src/interfaces/collection.interface';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository';
import { FindAllTerritoriesForPointsQuery } from '../../queries/find-all-territories-for-points.query';
import { Territory } from '../entities/territory';

View File

@ -1,5 +1,5 @@
import { QueryHandler } from '@nestjs/cqrs';
import { ICollection } from 'src/modules/databases/src/interfaces/collection.interface';
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';

View File

@ -4,7 +4,7 @@ import { CacheModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { redisStore } from 'cache-manager-ioredis-yet';
import { DatabaseModule } from '../databases/database.module';
import { DatabaseModule } from '../database/database.module';
import { TerritoryController } from './adapters/primaries/territory.controller';
import { TerritoriesRepository } from './adapters/secondaries/territories.repository';
import { Messager } from './adapters/secondaries/messager';

View File

@ -1,7 +1,7 @@
import { TestingModule, Test } from '@nestjs/testing';
import { DatabaseModule } from '../../../databases/database.module';
import { PrismaService } from '../../../databases/src/adapters/secondaries/prisma-service';
import { DatabaseException } from '../../../databases/src/exceptions/database.exception';
import { DatabaseModule } from '../../../database/database.module';
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service';
import { DatabaseException } from '../../../database/src/exceptions/database.exception';
import { TerritoriesRepository } from '../../adapters/secondaries/territories.repository';
import { v4 as uuidv4 } from 'uuid';
import { Territory } from '../../domain/entities/territory';