Merge branch 'useHealthPackage' into 'main'

Use health package

See merge request v3/service/ad!15
This commit is contained in:
Sylvain Briat 2023-07-18 14:52:17 +00:00
commit 51314695ef
28 changed files with 247 additions and 931 deletions

View File

@ -19,7 +19,7 @@ test:
- docker-compose -f docker-compose.ci.tools.yml -p ad-tools --env-file ci/.env.ci up -d - docker-compose -f docker-compose.ci.tools.yml -p ad-tools --env-file ci/.env.ci up -d
- sh ci/wait-up.sh - sh ci/wait-up.sh
- docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d - docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d
- docker exec -t v3-ad-api sh -c "npm run test:integration:ci" # - docker exec -t v3-ad-api sh -c "npm run test:integration:ci"
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
rules: rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

View File

@ -6,10 +6,11 @@ SERVICE_PORT=5006
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
# RABBIT MQ # RABBIT MQ
RMQ_URI=amqp://v3-broker:5672 RMQ_URI=amqp://v3-ad-broker:5672
# MESSAGE BROKER # MESSAGE BROKER
BROKER_IMAGE=rabbitmq:3-alpine BROKER_IMAGE=rabbitmq:3-alpine
# POSTGRES # POSTGRES
POSTGRES_IMAGE=postgis/postgis:15-3.3 POSTGRES_IMAGE=postgres:15.0

29
package-lock.json generated
View File

@ -1,19 +1,20 @@
{ {
"name": "@mobicoop/ad", "name": "@mobicoop/ad",
"version": "1.0.0", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@mobicoop/ad", "name": "@mobicoop/ad",
"version": "1.0.0", "version": "1.1.0",
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.8.14", "@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.6", "@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5", "@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.2.0", "@mobicoop/configuration-module": "^1.2.0",
"@mobicoop/ddd-library": "^0.0.1", "@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/health-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.2.0", "@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1", "@nestjs/config": "^2.3.1",
@ -1414,9 +1415,9 @@
} }
}, },
"node_modules/@mobicoop/ddd-library": { "node_modules/@mobicoop/ddd-library": {
"version": "0.0.1", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.0.1.tgz", "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.3.0.tgz",
"integrity": "sha512-T6g3pgodrMOZ1yrtNWylgu6EjRz3HPgcD9UoK0cJCvfiq9WjTH9TOZ6wKh9vIijiO+a5KJkZiKHbbjzuJvdwCg==", "integrity": "sha512-MoUDqlrDmJkumCFSyW9FY2DLbguT4rytFrmBt9tVNCr2Es6nlz4Ml3HVBwJTZrlJFU79XmiUQ5WAO0MHJt+nAg==",
"dependencies": { "dependencies": {
"@nestjs/event-emitter": "^1.4.2", "@nestjs/event-emitter": "^1.4.2",
"@nestjs/microservices": "^9.4.0", "@nestjs/microservices": "^9.4.0",
@ -1428,6 +1429,22 @@
"@nestjs/common": "^9.4.2" "@nestjs/common": "^9.4.2"
} }
}, },
"node_modules/@mobicoop/health-module": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mobicoop/health-module/-/health-module-1.1.0.tgz",
"integrity": "sha512-tSdvpwHxMOG7U3txm3sQDUkj1cWeGg9K68u8Y2BKgD8ocMRDufiCXY43ScFXKZqWm8jkcOU6XdwJm3pxvUOq4w==",
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.7",
"@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/message-broker-module": "^1.0.5",
"@nestjs/microservices": "^9.4.2",
"@nestjs/terminus": "^9.2.2"
},
"peerDependencies": {
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/message-broker-module": { "node_modules/@mobicoop/message-broker-module": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "@mobicoop/ad", "name": "@mobicoop/ad",
"version": "1.0.0", "version": "1.1.0",
"description": "Mobicoop V3 Ad", "description": "Mobicoop V3 Ad",
"author": "sbriat", "author": "sbriat",
"private": true, "private": true,
@ -17,18 +17,14 @@
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore", "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
"pretty:check": "./node_modules/.bin/prettier --check .", "pretty:check": "./node_modules/.bin/prettier --check .",
"pretty": "./node_modules/.bin/prettier --write .", "pretty": "./node_modules/.bin/prettier --write .",
"test": "npm run migrate:test && dotenv -e .env.test jest", "test": "npm run test:unit && npm run test:integration",
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose", "test:unit": "jest --testPathPattern 'tests/unit/' --verbose",
"test:unit:watch": "jest --testPathPattern 'tests/unit/' --verbose --watch",
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage", "test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
"test:integration:watch": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --watch", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"generate": "docker exec v3-ad-api sh -c 'npx prisma generate'", "migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
"migrate:init": "docker exec v3-ad-api sh -c 'npx prisma migrate dev --name init'",
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy", "migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy", "migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy" "migrate:deploy": "npx prisma migrate deploy"
@ -38,7 +34,8 @@
"@grpc/proto-loader": "^0.7.6", "@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5", "@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.2.0", "@mobicoop/configuration-module": "^1.2.0",
"@mobicoop/ddd-library": "^0.0.1", "@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/health-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.2.0", "@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1", "@nestjs/config": "^2.3.1",
@ -93,14 +90,10 @@
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
".module.ts", ".module.ts",
".dto.ts", ".dto.ts",
".constants.ts", ".di-tokens.ts",
".response.ts", ".response.ts",
".response.base.ts",
".port.ts", ".port.ts",
"libs/exceptions",
"libs/types",
"prisma.service.ts", "prisma.service.ts",
"convert-props-to-object.util.ts",
"main.ts" "main.ts"
], ],
"rootDir": "src", "rootDir": "src",
@ -114,19 +107,14 @@
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
".module.ts", ".module.ts",
".dto.ts", ".dto.ts",
".constants.ts", ".di-tokens.ts",
".response.ts", ".response.ts",
".response.base.ts",
".port.ts", ".port.ts",
"libs/exceptions",
"libs/types",
"prisma.service.ts", "prisma.service.ts",
"convert-props-to-object.util.ts",
"main.ts" "main.ts"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"moduleNameMapper": { "moduleNameMapper": {
"^@libs(.*)": "<rootDir>/libs/$1",
"^@modules(.*)": "<rootDir>/modules/$1", "^@modules(.*)": "<rootDir>/modules/$1",
"^@src(.*)": "<rootDir>$1" "^@src(.*)": "<rootDir>$1"
}, },

View File

@ -1 +0,0 @@
export const MESSAGE_PUBLISHER = Symbol();

View File

@ -7,28 +7,21 @@ import {
} from '@mobicoop/configuration-module'; } from '@mobicoop/configuration-module';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { RequestContextModule } from 'nestjs-request-context'; import { RequestContextModule } from 'nestjs-request-context';
import { HealthModule } from '@modules/health/health.module'; import { MessagerModule } from '@modules/messager/messager.module';
import { HealthModule } from '@mobicoop/health-module';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
import { import {
MessageBrokerModule, HealthModuleOptions,
MessageBrokerModuleOptions, ICheckRepository,
} from '@mobicoop/message-broker-module'; } from '@mobicoop/health-module/dist/core/domain/types/health.types';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
RequestContextModule, RequestContextModule,
MessageBrokerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<MessageBrokerModuleOptions> => ({
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
name: 'ad',
}),
}),
ConfigurationModule.forRootAsync({ ConfigurationModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
@ -50,8 +43,22 @@ import {
propagateConfigurationQueue: 'ad-configuration-propagate', propagateConfigurationQueue: 'ad-configuration-propagate',
}), }),
}), }),
HealthModule, HealthModule.forRootAsync({
imports: [AdModule, MessagerModule],
inject: [AD_REPOSITORY, MESSAGE_PUBLISHER],
useFactory: async (
adRepository: ICheckRepository,
messagePublisher: MessagePublisherPort,
): Promise<HealthModuleOptions> => ({
serviceName: 'ad',
criticalLoggingKey: 'logging.ad.health.crit',
checkRepositories: [adRepository],
messagePublisher,
}),
}),
AdModule, AdModule,
MessagerModule,
], ],
exports: [AdModule, MessagerModule],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,3 +1,4 @@
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER');
export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER');

View File

@ -2,12 +2,12 @@ import { Module, Provider } from '@nestjs/common';
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller'; import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
import { import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY, AD_REPOSITORY,
PARAMS_PROVIDER, PARAMS_PROVIDER,
TIMEZONE_FINDER, TIMEZONE_FINDER,
TIME_CONVERTER, TIME_CONVERTER,
} from './ad.di-tokens'; } from './ad.di-tokens';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
import { AdRepository } from './infrastructure/ad.repository'; import { AdRepository } from './infrastructure/ad.repository';
import { DefaultParamsProvider } from './infrastructure/default-params-provider'; import { DefaultParamsProvider } from './infrastructure/default-params-provider';
import { AdMapper } from './ad.mapper'; import { AdMapper } from './ad.mapper';
@ -17,7 +17,6 @@ import { TimeConverter } from './infrastructure/time-converter';
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller'; import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler'; import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { PublishLogMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-log-message-when-ad-is-created.domain-event-handler';
import { PrismaService } from './infrastructure/prisma.service'; import { PrismaService } from './infrastructure/prisma.service';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
@ -25,7 +24,6 @@ const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
const eventHandlers: Provider[] = [ const eventHandlers: Provider[] = [
PublishMessageWhenAdIsCreatedDomainEventHandler, PublishMessageWhenAdIsCreatedDomainEventHandler,
PublishLogMessageWhenAdIsCreatedDomainEventHandler,
]; ];
const commandHandlers: Provider[] = [CreateAdService]; const commandHandlers: Provider[] = [CreateAdService];
@ -41,16 +39,15 @@ const repositories: Provider[] = [
}, },
]; ];
const messageBrokers: Provider[] = [ const messagePublishers: Provider[] = [
{ {
provide: MESSAGE_PUBLISHER, provide: AD_MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher, useExisting: MessageBrokerPublisher,
}, },
]; ];
const orms: Provider[] = [PrismaService]; const orms: Provider[] = [PrismaService];
const utilities: Provider[] = [ const adapters: Provider[] = [
{ {
provide: PARAMS_PROVIDER, provide: PARAMS_PROVIDER,
useClass: DefaultParamsProvider, useClass: DefaultParamsProvider,
@ -74,9 +71,9 @@ const utilities: Provider[] = [
...queryHandlers, ...queryHandlers,
...mappers, ...mappers,
...repositories, ...repositories,
...messageBrokers, ...messagePublishers,
...orms, ...orms,
...utilities, ...adapters,
], ],
exports: [ exports: [
PrismaService, PrismaService,

View File

@ -1,21 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
@Injectable()
export class PublishLogMessageWhenAdIsCreatedDomainEventHandler {
constructor(
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
) {}
@OnEvent(AdCreatedDomainEvent.name, { async: true, promisify: true })
async handle(event: AdCreatedDomainEvent): Promise<any> {
this.messagePublisher.publish(
'logging.ad.created.info',
JSON.stringify(event),
);
}
}

View File

@ -1,13 +1,13 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events'; import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
import { MessagePublisherPort } from '@mobicoop/ddd-library'; import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
@Injectable() @Injectable()
export class PublishMessageWhenAdIsCreatedDomainEventHandler { export class PublishMessageWhenAdIsCreatedDomainEventHandler {
constructor( constructor(
@Inject(MESSAGE_PUBLISHER) @Inject(AD_MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort, private readonly messagePublisher: MessagePublisherPort,
) {} ) {}

View File

@ -1,10 +1,15 @@
import { Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { AdEntity } from '../core/domain/ad.entity'; import { AdEntity } from '../core/domain/ad.entity';
import { AdMapper } from '../ad.mapper'; import { AdMapper } from '../ad.mapper';
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
import { PrismaRepositoryBase } from '@mobicoop/ddd-library'; import {
LoggerBase,
MessagePublisherPort,
PrismaRepositoryBase,
} from '@mobicoop/ddd-library';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
export type AdBaseModel = { export type AdBaseModel = {
uuid: string; uuid: string;
@ -72,13 +77,19 @@ export class AdRepository
prisma: PrismaService, prisma: PrismaService,
mapper: AdMapper, mapper: AdMapper,
eventEmitter: EventEmitter2, eventEmitter: EventEmitter2,
@Inject(AD_MESSAGE_PUBLISHER)
protected readonly messagePublisher: MessagePublisherPort,
) { ) {
super( super(
prisma.ad, prisma.ad,
prisma, prisma,
mapper, mapper,
eventEmitter, eventEmitter,
new Logger(AdRepository.name), new LoggerBase({
logger: new Logger(AdRepository.name),
domain: 'ad',
messagePublisher,
}),
); );
} }
} }

View File

@ -1,10 +1,17 @@
import { import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY, AD_REPOSITORY,
PARAMS_PROVIDER, PARAMS_PROVIDER,
TIMEZONE_FINDER, TIMEZONE_FINDER,
TIME_CONVERTER, TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens'; } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper'; import { AdMapper } from '@modules/ad/ad.mapper';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
DefaultAdProps,
Frequency,
} from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider'; import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
@ -48,20 +55,6 @@ describe('Ad Repository', () => {
seatsRequested: 0, seatsRequested: 0,
strict: 'false', strict: 'false',
}; };
// const passengerAd = {
// driver: 'false',
// passenger: 'true',
// seatsProposed: 0,
// seatsRequested: 1,
// strict: 'false',
// };
// const driverAndPassengerAd = {
// driver: 'true',
// passenger: 'true',
// seatsProposed: 3,
// seatsRequested: 1,
// strict: 'false',
// };
const punctualAd = { const punctualAd = {
frequency: `'PUNCTUAL'`, frequency: `'PUNCTUAL'`,
fromDate: `'2023-01-01'`, fromDate: `'2023-01-01'`,
@ -81,25 +74,6 @@ describe('Ad Repository', () => {
satMargin: 900, satMargin: 900,
sunMargin: 900, sunMargin: 900,
}; };
// const recurrentAd = {
// frequency: `'RECURRENT'`,
// fromDate: `'2023-01-01'`,
// toDate: `'2023-12-31'`,
// monTime: `'2023-01-01T07:15:00Z'`,
// tueTime: `'2023-01-01T07:15:00Z'`,
// wedTime: `'2023-01-01T07:05:00Z'`,
// thuTime: `'2023-01-01T07:15:00Z'`,
// friTime: `'2023-01-01T07:15:00Z'`,
// satTime: 'NULL',
// sunTime: 'NULL',
// monMargin: 900,
// tueMargin: 900,
// wedMargin: 900,
// thuMargin: 900,
// friMargin: 900,
// satMargin: 900,
// sunMargin: 900,
// };
const originWaypoint = { const originWaypoint = {
position: 0, position: 0,
lon: 43.7102, lon: 43.7102,
@ -140,120 +114,15 @@ describe('Ad Repository', () => {
} }
}; };
// const createRecurrentDriverAds = async (nbToCreate = 10) => { const mockMessagePublisher = {
// const adToCreate = { publish: jest.fn().mockImplementation(),
// ...baseUuid, };
// ...baseUserUuid,
// ...driverAd,
// ...punctualAd,
// };
// for (let i = 0; i < nbToCreate; i++) {
// adToCreate.uuid = getSeed(i, baseUuid.uuid);
// await executeInsertCommand('ad', adToCreate);
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseOriginWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...originWaypoint,
// });
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...destinationWaypoint,
// });
// }
// };
// const createPunctualPassengerAds = async (nbToCreate = 10) => { const mockLogger = {
// const adToCreate = { log: jest.fn(),
// ...baseUuid, warn: jest.fn(),
// ...baseUserUuid, error: jest.fn(),
// ...passengerAd, };
// ...punctualAd,
// };
// for (let i = 0; i < nbToCreate; i++) {
// adToCreate.uuid = getSeed(i, baseUuid.uuid);
// await executeInsertCommand('ad', adToCreate);
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseOriginWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...originWaypoint,
// });
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...destinationWaypoint,
// });
// }
// };
// const createRecurrentPassengerAds = async (nbToCreate = 10) => {
// const adToCreate = {
// ...baseUuid,
// ...baseUserUuid,
// ...passengerAd,
// ...recurrentAd,
// };
// for (let i = 0; i < nbToCreate; i++) {
// adToCreate.uuid = getSeed(i, baseUuid.uuid);
// await executeInsertCommand('ad', adToCreate);
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseOriginWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...originWaypoint,
// });
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...destinationWaypoint,
// });
// }
// };
// const createPunctualDriverPassengerAds = async (nbToCreate = 10) => {
// const adToCreate = {
// ...baseUuid,
// ...baseUserUuid,
// ...driverAndPassengerAd,
// ...punctualAd,
// };
// for (let i = 0; i < nbToCreate; i++) {
// adToCreate.uuid = getSeed(i, baseUuid.uuid);
// await executeInsertCommand('ad', adToCreate);
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseOriginWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...originWaypoint,
// });
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...destinationWaypoint,
// });
// }
// };
// const createRecurrentDriverPassengerAds = async (nbToCreate = 10) => {
// const adToCreate = {
// ...baseUuid,
// ...baseUserUuid,
// ...driverAndPassengerAd,
// ...recurrentAd,
// };
// for (let i = 0; i < nbToCreate; i++) {
// adToCreate.uuid = getSeed(i, baseUuid.uuid);
// await executeInsertCommand('ad', adToCreate);
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseOriginWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...originWaypoint,
// });
// await executeInsertCommand('waypoint', {
// uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
// adUuid: adToCreate.uuid,
// ...destinationWaypoint,
// });
// }
// };
beforeAll(async () => { beforeAll(async () => {
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
@ -280,11 +149,20 @@ describe('Ad Repository', () => {
provide: TIME_CONVERTER, provide: TIME_CONVERTER,
useClass: TimeConverter, useClass: TimeConverter,
}, },
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
], ],
}).compile(); })
// disable logging
.setLogger(mockLogger)
.compile();
prismaService = module.get<PrismaService>(PrismaService); prismaService = module.get<PrismaService>(PrismaService);
adRepository = module.get<AdRepository>(AD_REPOSITORY); adRepository = module.get<AdRepository>(AD_REPOSITORY);
}); });
afterAll(async () => { afterAll(async () => {
await prismaService.$disconnect(); await prismaService.$disconnect();
}); });
@ -292,129 +170,7 @@ describe('Ad Repository', () => {
beforeEach(async () => { beforeEach(async () => {
await prismaService.ad.deleteMany(); await prismaService.ad.deleteMany();
}); });
// describe('findAll', () => {
// it('should return an empty data array', async () => {
// const res = await adRepository.findAll();
// expect(res).toEqual({
// data: [],
// total: 0,
// });
// });
// describe('drivers', () => {
// it('should return a data array with 8 punctual driver ads', async () => {
// await createPunctualDriverAds(8);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(8);
// expect(ads.total).toBe(8);
// expect(ads.data[0].driver).toBeTruthy();
// expect(ads.data[0].passenger).toBeFalsy();
// });
// it('should return a data array limited to 10 punctual driver ads', async () => {
// await createPunctualDriverAds(20);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(20);
// expect(ads.data[1].driver).toBeTruthy();
// expect(ads.data[1].passenger).toBeFalsy();
// });
// it('should return a data array with 8 recurrent driver ads', async () => {
// await createRecurrentDriverAds(8);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(8);
// expect(ads.total).toBe(8);
// expect(ads.data[2].driver).toBeTruthy();
// expect(ads.data[2].passenger).toBeFalsy();
// });
// it('should return a data array limited to 10 recurrent driver ads', async () => {
// await createRecurrentDriverAds(20);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(20);
// expect(ads.data[3].driver).toBeTruthy();
// expect(ads.data[3].passenger).toBeFalsy();
// });
// });
// describe('passengers', () => {
// it('should return a data array with 7 punctual passenger ads', async () => {
// await createPunctualPassengerAds(7);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(7);
// expect(ads.total).toBe(7);
// expect(ads.data[0].passenger).toBeTruthy();
// expect(ads.data[0].driver).toBeFalsy();
// });
// it('should return a data array limited to 10 punctual passenger ads', async () => {
// await createPunctualPassengerAds(15);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(15);
// expect(ads.data[1].passenger).toBeTruthy();
// expect(ads.data[1].driver).toBeFalsy();
// });
// it('should return a data array with 7 recurrent passenger ads', async () => {
// await createRecurrentPassengerAds(7);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(7);
// expect(ads.total).toBe(7);
// expect(ads.data[2].passenger).toBeTruthy();
// expect(ads.data[2].driver).toBeFalsy();
// });
// it('should return a data array limited to 10 recurrent passenger ads', async () => {
// await createRecurrentPassengerAds(15);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(15);
// expect(ads.data[3].passenger).toBeTruthy();
// expect(ads.data[3].driver).toBeFalsy();
// });
// });
// describe('drivers and passengers', () => {
// it('should return a data array with 6 punctual driver and passenger ads', async () => {
// await createPunctualDriverPassengerAds(6);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(6);
// expect(ads.total).toBe(6);
// expect(ads.data[0].passenger).toBeTruthy();
// expect(ads.data[0].driver).toBeTruthy();
// });
// it('should return a data array limited to 10 punctual driver and passenger ads', async () => {
// await createPunctualDriverPassengerAds(16);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(16);
// expect(ads.data[1].passenger).toBeTruthy();
// expect(ads.data[1].driver).toBeTruthy();
// });
// it('should return a data array with 6 recurrent driver and passenger ads', async () => {
// await createRecurrentDriverPassengerAds(6);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(6);
// expect(ads.total).toBe(6);
// expect(ads.data[2].passenger).toBeTruthy();
// expect(ads.data[2].driver).toBeTruthy();
// });
// it('should return a data array limited to 10 recurrent driver and passenger ads', async () => {
// await createRecurrentDriverPassengerAds(16);
// const ads = await adRepository.findAll();
// expect(ads.data.length).toBe(10);
// expect(ads.total).toBe(16);
// expect(ads.data[3].passenger).toBeTruthy();
// expect(ads.data[3].driver).toBeTruthy();
// });
// });
// });
describe('findOneById', () => { describe('findOneById', () => {
it('should return an ad', async () => { it('should return an ad', async () => {
await createPunctualDriverAds(1); await createPunctualDriverAds(1);
@ -424,80 +180,97 @@ describe('Ad Repository', () => {
expect(result.id).toBe(baseUuid.uuid); expect(result.id).toBe(baseUuid.uuid);
}); });
});
// it('should return null', async () => { describe('create', () => {
// const ad = await adRepository.findOneById( it('should create an ad', async () => {
// '544572be-11fb-4244-8235-587221fc9104', const beforeCount = await prismaService.ad.count();
// );
// expect(ad).toBeNull(); const createAdProps: CreateAdProps = {
userId: 'b4b56444-f8d3-4110-917c-e37bba77f383',
driver: true,
passenger: false,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-02-01',
toDate: '2023-02-01',
schedule: {
wed: '12:05',
},
marginDurations: {
wed: 900,
},
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
address: {
locality: 'Nice',
postalCode: '06000',
country: 'France',
coordinates: {
lon: 43.7102,
lat: 7.262,
},
},
},
{
position: 1,
address: {
locality: 'Marseille',
postalCode: '13000',
country: 'France',
coordinates: {
lon: 43.2965,
lat: 5.3698,
},
},
},
],
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDurations: {
mon: 900,
tue: 900,
wed: 900,
thu: 900,
fri: 900,
sat: 900,
sun: 900,
},
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
const adToCreate: AdEntity = AdEntity.create(
createAdProps,
defaultAdProps,
);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();
expect(afterCount - beforeCount).toBe(1);
});
// it('should throw a UniqueConstraintException if ad already exists', async () => {
// await prismaService.ad.create({
// data: {
// uuid: uuid,
// password: bcrypt.hashSync(`password`, 10),
// },
// });
// const authenticationToCreate: AuthenticationEntity =
// await AuthenticationEntity.create(createAuthenticationProps);
// await expect(
// authenticationRepository.insert(authenticationToCreate),
// ).rejects.toBeInstanceOf(UniqueConstraintException);
// }); // });
}); });
// describe('create', () => {
// it('should create a punctual ad', async () => {
// const beforeCount = await prismaService.ad.count();
// const adToCreate: AdDTO = new AdDTO();
// adToCreate.uuid = 'be459a29-7a41-4c0b-b371-abe90bfb6f00';
// adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200';
// adToCreate.driver = true;
// adToCreate.passenger = false;
// adToCreate.frequency = Frequency.PUNCTUAL;
// adToCreate.fromDate = new Date('05-22-2023 09:36');
// adToCreate.toDate = new Date('05-22-2023 09:36');
// adToCreate.monTime = '09:36';
// adToCreate.monMargin = 900;
// adToCreate.tueMargin = 900;
// adToCreate.wedMargin = 900;
// adToCreate.thuMargin = 900;
// adToCreate.friMargin = 900;
// adToCreate.satMargin = 900;
// adToCreate.sunMargin = 900;
// adToCreate.seatsProposed = 3;
// adToCreate.seatsRequested = 0;
// adToCreate.strict = false;
// adToCreate.waypoints = {
// create: [originWaypoint as Waypoint, destinationWaypoint as Waypoint],
// };
// const ad = await adRepository.create(adToCreate);
// const afterCount = await prismaService.ad.count();
// expect(afterCount - beforeCount).toBe(1);
// expect(ad.uuid).toBe('be459a29-7a41-4c0b-b371-abe90bfb6f00');
// });
// it('should create a recurrent ad', async () => {
// const beforeCount = await prismaService.ad.count();
// const adToCreate: AdDTO = new AdDTO();
// adToCreate.uuid = '137a26fa-4b38-48ba-aecf-1a75f6b20f3d';
// adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200';
// adToCreate.driver = true;
// adToCreate.passenger = false;
// adToCreate.frequency = Frequency.RECURRENT;
// adToCreate.fromDate = new Date('01-15-2023 ');
// adToCreate.toDate = new Date('10-31-2023');
// adToCreate.monTime = '07:30';
// adToCreate.friTime = '07:45';
// adToCreate.thuTime = '08:00';
// adToCreate.monMargin = 900;
// adToCreate.tueMargin = 900;
// adToCreate.wedMargin = 900;
// adToCreate.thuMargin = 900;
// adToCreate.friMargin = 900;
// adToCreate.satMargin = 900;
// adToCreate.sunMargin = 900;
// adToCreate.seatsProposed = 2;
// adToCreate.seatsRequested = 0;
// adToCreate.strict = false;
// adToCreate.waypoints = {
// create: [originWaypoint as Waypoint, destinationWaypoint as Waypoint],
// };
// const ad = await adRepository.create(adToCreate);
// const afterCount = await prismaService.ad.count();
// expect(afterCount - beforeCount).toBe(1);
// expect(ad.uuid).toBe('137a26fa-4b38-48ba-aecf-1a75f6b20f3d');
// });
// });
}); });

View File

@ -1,94 +0,0 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { PublishLogMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-log-message-when-ad-is-created.domain-event-handler';
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events';
import { Test, TestingModule } from '@nestjs/testing';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Publish log message when ad is created domain event handler', () => {
let publishLogMessageWhenAdIsCreatedDomainEventHandler: PublishLogMessageWhenAdIsCreatedDomainEventHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
PublishLogMessageWhenAdIsCreatedDomainEventHandler,
],
}).compile();
publishLogMessageWhenAdIsCreatedDomainEventHandler =
module.get<PublishLogMessageWhenAdIsCreatedDomainEventHandler>(
PublishLogMessageWhenAdIsCreatedDomainEventHandler,
);
});
it('should publish a log message', () => {
jest.spyOn(mockMessagePublisher, 'publish');
const adCreatedDomainEvent: AdCreatedDomainEvent = {
id: 'some-domain-event-id',
aggregateId: 'some-aggregate-id',
userId: 'some-user-id',
driver: false,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-28',
toDate: '2023-06-28',
monTime: undefined,
tueTime: undefined,
wedTime: '07:15',
thuTime: undefined,
friTime: undefined,
satTime: undefined,
sunTime: undefined,
monMarginDuration: 900,
tueMarginDuration: 900,
wedMarginDuration: 900,
thuMarginDuration: 900,
friMarginDuration: 900,
satMarginDuration: 900,
sunMarginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
lat: 48.689445,
lon: 6.1765102,
},
{
position: 1,
locality: 'Paris',
postalCode: '75000',
country: 'France',
lat: 48.8566,
lon: 2.3522,
},
],
metadata: {
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
correlationId: 'some-correlation-id',
},
};
publishLogMessageWhenAdIsCreatedDomainEventHandler.handle(
adCreatedDomainEvent,
);
expect(publishLogMessageWhenAdIsCreatedDomainEventHandler).toBeDefined();
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
'logging.ad.created.info',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","userId":"some-user-id","driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-06-28","toDate":"2023-06-28","wedTime":"07:15","monMarginDuration":900,"tueMarginDuration":900,"wedMarginDuration":900,"thuMarginDuration":900,"friMarginDuration":900,"satMarginDuration":900,"sunMarginDuration":900,"seatsProposed":3,"seatsRequested":1,"strict":false,"waypoints":[{"position":0,"houseNumber":"5","street":"Avenue Foch","locality":"Nancy","postalCode":"54000","country":"France","lat":48.689445,"lon":6.1765102},{"position":1,"locality":"Paris","postalCode":"75000","country":"France","lat":48.8566,"lon":2.3522}],"metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
);
});
});

View File

@ -2,7 +2,7 @@ import { Frequency } from '@modules/ad/core/domain/ad.types';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events'; import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { MESSAGE_PUBLISHER } from '@src/app.constants'; import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
const mockMessagePublisher = { const mockMessagePublisher = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
@ -15,7 +15,7 @@ describe('Publish message when ad is created domain event handler', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
{ {
provide: MESSAGE_PUBLISHER, provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher, useValue: mockMessagePublisher,
}, },
PublishMessageWhenAdIsCreatedDomainEventHandler, PublishMessageWhenAdIsCreatedDomainEventHandler,

View File

@ -50,6 +50,10 @@ const mockTimeConverter: TimeConverterPort = {
utcDatetimeToLocalTime: jest.fn(), utcDatetimeToLocalTime: jest.fn(),
}; };
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Ad repository', () => { describe('Ad repository', () => {
let prismaService: PrismaService; let prismaService: PrismaService;
let adMapper: AdMapper; let adMapper: AdMapper;
@ -82,7 +86,12 @@ describe('Ad repository', () => {
}); });
it('should be defined', () => { it('should be defined', () => {
expect( expect(
new AdRepository(prismaService, adMapper, eventEmitter), new AdRepository(
prismaService,
adMapper,
eventEmitter,
mockMessagePublisher,
),
).toBeDefined(); ).toBeDefined();
}); });
}); });

View File

@ -1,3 +0,0 @@
export interface CheckRepositoryPort {
healthCheck(): Promise<boolean>;
}

View File

@ -1,48 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthCheckResult,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { CheckRepositoryPort } from '../ports/check-repository.port';
import { AD_REPOSITORY } from '@modules/health/health.di-tokens';
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
import { LOGGING_AD_HEALTH_CRIT } from '@modules/health/health.constants';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
@Injectable()
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
private _checkRepositories: CheckRepositoryPort[];
constructor(
@Inject(AD_REPOSITORY)
private readonly adRepository: AdRepositoryPort,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
) {
super();
this._checkRepositories = [adRepository];
}
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
try {
await Promise.all(
this._checkRepositories.map(
async (checkRepository: CheckRepositoryPort) => {
await checkRepository.healthCheck();
},
),
);
return this.getStatus(key, true);
} catch (error) {
const healthCheckResult: HealthCheckResult = error;
this.messagePublisher.publish(
LOGGING_AD_HEALTH_CRIT,
JSON.stringify(healthCheckResult.error),
);
throw new HealthCheckError('Repository', {
repository: error.message,
});
}
};
}

View File

@ -1 +0,0 @@
export const LOGGING_AD_HEALTH_CRIT = 'logging.ad.health.crit';

View File

@ -1 +0,0 @@
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');

View File

@ -1,37 +0,0 @@
import { Module, Provider } from '@nestjs/common';
import { HealthHttpController } from './interface/http-controllers/health.http.controller';
import { TerminusModule } from '@nestjs/terminus';
import { MESSAGE_PUBLISHER } from 'src/app.constants';
import { RepositoriesHealthIndicatorUseCase } from './core/application/usecases/repositories.health-indicator.usecase';
import { AdRepository } from '../ad/infrastructure/ad.repository';
import { AD_REPOSITORY } from './health.di-tokens';
import { HealthGrpcController } from './interface/grpc-controllers/health.grpc.controller';
import { AdModule } from '@modules/ad/ad.module';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
const grpcControllers = [HealthGrpcController];
const httpControllers = [HealthHttpController];
const useCases: Provider[] = [RepositoriesHealthIndicatorUseCase];
const repositories: Provider[] = [
{
provide: AD_REPOSITORY,
useClass: AdRepository,
},
];
const messageBrokers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
},
];
@Module({
imports: [TerminusModule, AdModule],
controllers: [...grpcControllers, ...httpControllers],
providers: [...useCases, ...repositories, ...messageBrokers],
})
export class HealthModule {}

View File

@ -1,42 +0,0 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
export enum ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
}
interface HealthCheckRequest {
service: string;
}
interface HealthCheckResponse {
status: ServingStatus;
}
@Controller()
export class HealthGrpcController {
constructor(
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
) {}
@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.repositoriesHealthIndicatorUseCase.isHealthy(
'repositories',
);
return {
status:
healthCheck['repositories'].status == 'up'
? ServingStatus.SERVING
: ServingStatus.NOT_SERVING,
};
}
}

View File

@ -1,21 +0,0 @@
syntax = "proto3";
package health;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}

View File

@ -1,28 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
@Controller('health')
export class HealthHttpController {
constructor(
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
private readonly healthCheckService: HealthCheckService,
) {}
@Get()
@HealthCheck()
async check(): Promise<HealthCheckResult> {
try {
return await this.healthCheckService.check([
async () =>
this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
]);
} catch (error) {
throw error;
}
}
}

View File

@ -1,72 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import {
HealthGrpcController,
ServingStatus,
} from '@modules/health/interface/grpc-controllers/health.grpc.controller';
import { Test, TestingModule } from '@nestjs/testing';
const mockRepositoriesHealthIndicatorUseCase = {
isHealthy: jest
.fn()
.mockImplementationOnce(() => ({
repositories: {
status: 'up',
},
}))
.mockImplementationOnce(() => ({
repositories: {
status: 'down',
},
})),
};
describe('Health Grpc Controller', () => {
let healthGrpcController: HealthGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: RepositoriesHealthIndicatorUseCase,
useValue: mockRepositoriesHealthIndicatorUseCase,
},
HealthGrpcController,
],
}).compile();
healthGrpcController =
module.get<HealthGrpcController>(HealthGrpcController);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(healthGrpcController).toBeDefined();
});
it('should return a Serving status ', async () => {
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const servingStatus: { status: ServingStatus } =
await healthGrpcController.check();
expect(servingStatus).toEqual({
status: ServingStatus.SERVING,
});
expect(
mockRepositoriesHealthIndicatorUseCase.isHealthy,
).toHaveBeenCalledTimes(1);
});
it('should return a Not Serving status ', async () => {
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const servingStatus: { status: ServingStatus } =
await healthGrpcController.check();
expect(servingStatus).toEqual({
status: ServingStatus.NOT_SERVING,
});
expect(
mockRepositoriesHealthIndicatorUseCase.isHealthy,
).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,90 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import { HealthHttpController } from '@modules/health/interface/http-controllers/health.http.controller';
import { HealthCheckResult, HealthCheckService } from '@nestjs/terminus';
import { Test, TestingModule } from '@nestjs/testing';
const mockHealthCheckService = {
check: jest
.fn()
.mockImplementationOnce(() => ({
status: 'ok',
info: {
repositories: {
status: 'up',
},
},
error: {},
details: {
repositories: {
status: 'up',
},
},
}))
.mockImplementationOnce(() => ({
status: 'error',
info: {},
error: {
repository:
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
},
details: {
repository:
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
},
})),
};
const mockRepositoriesHealthIndicatorUseCase = {
isHealthy: jest.fn(),
};
describe('Health Http Controller', () => {
let healthHttpController: HealthHttpController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: HealthCheckService,
useValue: mockHealthCheckService,
},
{
provide: RepositoriesHealthIndicatorUseCase,
useValue: mockRepositoriesHealthIndicatorUseCase,
},
HealthHttpController,
],
}).compile();
healthHttpController =
module.get<HealthHttpController>(HealthHttpController);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(healthHttpController).toBeDefined();
});
it('should return an HealthCheckResult with Ok status ', async () => {
jest.spyOn(mockHealthCheckService, 'check');
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const healthCheckResult: HealthCheckResult =
await healthHttpController.check();
expect(healthCheckResult.status).toBe('ok');
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
});
it('should return an HealthCheckResult with Error status ', async () => {
jest.spyOn(mockHealthCheckService, 'check');
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const healthCheckResult: HealthCheckResult =
await healthHttpController.check();
expect(healthCheckResult.status).toBe('error');
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,66 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
import { AD_REPOSITORY } from '@modules/health/health.di-tokens';
import { MESSAGE_PUBLISHER } from '@src/app.constants';
import { DatabaseErrorException } from '@mobicoop/ddd-library';
const mockAdRepository = {
healthCheck: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new DatabaseErrorException('An error occured in the database');
}),
};
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('RepositoriesHealthIndicatorUseCase', () => {
let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RepositoriesHealthIndicatorUseCase,
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
{
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],
}).compile();
repositoriesHealthIndicatorUseCase =
module.get<RepositoriesHealthIndicatorUseCase>(
RepositoriesHealthIndicatorUseCase,
);
});
it('should be defined', () => {
expect(repositoriesHealthIndicatorUseCase).toBeDefined();
});
describe('execute', () => {
it('should check health successfully', async () => {
const healthIndicatorResult: HealthIndicatorResult =
await repositoriesHealthIndicatorUseCase.isHealthy('repositories');
expect(healthIndicatorResult['repositories'].status).toBe('up');
});
it('should throw an error if database is unavailable', async () => {
jest.spyOn(mockMessagePublisher, 'publish');
await expect(
repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
).rejects.toBeInstanceOf(HealthCheckError);
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1 @@
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');

View File

@ -0,0 +1,36 @@
import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
MessageBrokerPublisher,
} from '@mobicoop/message-broker-module';
import { ConfigModule, ConfigService } from '@nestjs/config';
const imports = [
MessageBrokerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<MessageBrokerModuleOptions> => ({
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
name: 'ad',
}),
}),
];
const providers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
},
];
@Module({
imports,
providers,
exports: [MESSAGE_PUBLISHER],
})
export class MessagerModule {}