refactor health module, improve code coverage

This commit is contained in:
sbriat 2023-06-21 12:31:59 +02:00
parent b232247c93
commit 1989ff6e67
12 changed files with 63 additions and 266 deletions

View File

@ -14,6 +14,7 @@ import {
} from '@mobicoop/configuration-module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { RequestContextModule } from 'nestjs-request-context';
import { HealthModule } from '@modules/health/health.module';
@Module({
imports: [
@ -53,7 +54,7 @@ import { RequestContextModule } from 'nestjs-request-context';
propagateConfigurationQueue: 'ad-configuration-propagate',
}),
}),
// HealthModule,
HealthModule,
AdModule,
],
controllers: [],

View File

@ -3,7 +3,10 @@ import { AggregateRoot, Mapper, RepositoryPort } from '../ddd';
import { ObjectLiteral } from '../types';
import { LoggerPort } from '../ports/logger.port';
import { None, Option, Some } from 'oxide.ts';
import { PrismaRepositoryPort } from '../ports/prisma-repository.port';
import {
PrismaRawRepositoryPort,
PrismaRepositoryPort,
} from '../ports/prisma-repository.port';
import { Prisma } from '@prisma/client';
import { ConflictException, DatabaseErrorException } from '@libs/exceptions';
@ -14,6 +17,7 @@ export abstract class PrismaRepositoryBase<
{
protected constructor(
protected readonly prisma: PrismaRepositoryPort<Aggregate> | any,
protected readonly prismaRaw: PrismaRawRepositoryPort,
protected readonly mapper: Mapper<Aggregate, DbModel>,
protected readonly eventEmitter: EventEmitter2,
protected readonly logger: LoggerPort,
@ -43,9 +47,10 @@ export abstract class PrismaRepositoryBase<
async healthCheck(): Promise<boolean> {
try {
await this.prisma.$queryRaw`SELECT 1`;
await this.prismaRaw.$queryRaw`SELECT 1`;
return true;
} catch (e) {
console.log(e);
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseErrorException(e.message);
}

View File

@ -2,3 +2,10 @@ export interface PrismaRepositoryPort<Entity> {
findUnique(options: any): Promise<Entity>;
create(entity: any): Promise<Entity>;
}
export interface PrismaRawRepositoryPort {
$queryRaw<T = unknown>(
query: TemplateStringsArray,
...values: any[]
): Promise<T>;
}

View File

@ -47,5 +47,12 @@ import { PrismaService } from '@libs/db/prisma.service';
useClass: TimezoneFinder,
},
],
exports: [
PrismaService,
AdMapper,
AD_REPOSITORY,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
],
})
export class AdModule {}

View File

@ -66,6 +66,12 @@ export class AdRepository
mapper: AdMapper,
eventEmitter: EventEmitter2,
) {
super(prisma.ad, mapper, eventEmitter, new Logger(AdRepository.name));
super(
prisma.ad,
prisma,
mapper,
eventEmitter,
new Logger(AdRepository.name),
);
}
}

View File

@ -1,140 +0,0 @@
import { Day } from '../../../core/_old/types/day.enum';
import { CreateAdRequestDTO } from '../../../interface/commands/create-ad.request.dto';
import { ScheduleDTO } from '../../../core/dtos/schedule.dto';
import { FrequencyNormalizer } from '../../../core/entities/frequency.normalizer';
import { Frequency } from '../../../interface/commands/frequency.enum';
describe('recurrent normalizer transformer for punctual ad ', () => {
const frequencyNormalizer = new FrequencyNormalizer();
it('should transform punctual ad into recurrent ad', () => {
const punctualAd: CreateAdRequestDTO = {
userUuid: 'cb7ad514-ad0d-463f-9041-b79f9afe33aa',
frequency: Frequency.PUNCTUAL,
departureDate: new Date('2023-03-05T12:39:39+02:00'),
waypoints: [
{
position: 0,
lon: 48.68944505415954,
lat: 6.176510296462267,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
},
{
position: 1,
lon: 48.8566,
lat: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
},
],
};
// expect(frequencyNormalizer.fromDateResolver(punctualAd)).toStrictEqual(
// new Date('2023-03-05T12:39:39Z'),
// );
// expect(frequencyNormalizer.toDateResolver(punctualAd)).toStrictEqual(
// new Date('2023-03-05T12:39:39Z'),
// );
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.mon),
// ).toBeUndefined();
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.tue),
// ).toBeUndefined();
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.wed),
// ).toBeUndefined();
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.thu),
// ).toBeUndefined();
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.fri),
// ).toBeUndefined();
// expect(
// frequencyNormalizer.scheduleResolver(punctualAd, Day.sat),
// ).toBeUndefined();
expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.sun)).toBe(
'12:39',
);
});
// it('should leave recurrent ad as is', () => {
// const recurrentAd: CreateAdRequest = {
// userUuid: '',
// frequency: Frequency.RECURRENT,
// schedule: {
// mon: '08:30',
// tue: '08:30',
// wed: '09:00',
// fri: '09:00',
// },
// waypoints: [],
// };
// expect(frequencyNormalizer.fromDateResolver(recurrentAd)).toBe(
// recurrentAd.departure,
// );
// expect(frequencyNormalizer.toDateResolver(recurrentAd)).toBe(
// recurrentAd.departure,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.mon)).toBe(
// recurrentAd.schedule.mon,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.tue)).toBe(
// recurrentAd.schedule.tue,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.wed)).toBe(
// recurrentAd.schedule.wed,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.thu)).toBe(
// recurrentAd.schedule.thu,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.fri)).toBe(
// recurrentAd.schedule.fri,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.sat)).toBe(
// recurrentAd.schedule.sat,
// );
// expect(frequencyNormalizer.scheduleResolver(recurrentAd, Day.sun)).toBe(
// recurrentAd.schedule.sun,
// );
// });
// it('should pass for each day of the week of a deprarture ', () => {
// const punctualAd: CreateAdRequest = {
// userUuid: '',
// frequency: Frequency.PUNCTUAL,
// departure: undefined,
// schedule: {} as ScheduleDTO,
// waypoints: [],
// };
// punctualAd.departure = new Date('05-01-2023 ');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.mon)).toBe(
// '00:00',
// );
// punctualAd.departure = new Date('05-02-2023 06:32:45');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.tue)).toBe(
// '06:32',
// );
// punctualAd.departure = new Date('05-03-2023 10:21');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.wed)).toBe(
// '10:21',
// );
// punctualAd.departure = new Date('05-04-2023 11:06:00');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.thu)).toBe(
// '11:06',
// );
// punctualAd.departure = new Date('05-05-2023 05:20');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.fri)).toBe(
// '05:20',
// );
// punctualAd.departure = new Date('05-06-2023');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.sat)).toBe(
// '00:00',
// );
// punctualAd.departure = new Date('05-07-2023');
// expect(frequencyNormalizer.scheduleResolver(punctualAd, Day.sun)).toBe(
// '00:00',
// );
// });
});

View File

@ -1,15 +0,0 @@
import { intToFrequency } from '../../../core/dtos/validators/frequency.mapping';
import { Frequency } from '../../../interface/commands/frequency.enum';
describe('frequency mapping function ', () => {
it('should return punctual', () => {
expect(intToFrequency(1)).toBe(Frequency.PUNCTUAL);
});
it('should return recurrent', () => {
expect(intToFrequency(2)).toBe(Frequency.RECURRENT);
});
it('should throw an error if frequency is unknown', () => {
expect(() => intToFrequency(0)).toThrow();
expect(() => intToFrequency(3)).toThrow();
});
});

View File

@ -1,93 +0,0 @@
import { isPunctualOrRecurrent } from '../../../core/dtos/validators/is-punctual-or-recurrent';
import { Frequency } from '../../../interface/commands/frequency.enum';
describe('punctual or recurrent validators', () => {
describe('punctual case ', () => {
describe('valid cases', () => {
it('should validate with valid departure', () => {
expect(
isPunctualOrRecurrent({
value: undefined,
constraints: [],
targetName: '',
object: {
frequency: Frequency.PUNCTUAL,
departure: new Date('2023-02-01T18:00:00+02:00'),
},
property: '',
}),
).toBeTruthy();
});
});
describe('invalid cases ', () => {
it('should not validate without departure', () => {
expect(
isPunctualOrRecurrent({
value: undefined,
constraints: [],
targetName: '',
object: {
frequency: Frequency.PUNCTUAL,
},
property: '',
}),
).toBeFalsy();
});
});
});
describe('recurrent case ', () => {
describe('valid cases', () => {
it('should validate with valid from date, to date and non empty schedule', () => {
expect(
isPunctualOrRecurrent({
value: undefined,
constraints: [],
targetName: '',
object: {
frequency: Frequency.RECURRENT,
fromDate: new Date('2023-01-15'),
toDate: new Date('2023-06-30'),
schedule: {
mon: '08:30',
},
},
property: '',
}),
).toBeTruthy();
});
});
describe('invalid cases ', () => {
it('should not validate with empty schedule', () => {
expect(
isPunctualOrRecurrent({
value: undefined,
constraints: [],
targetName: '',
object: {
frequency: Frequency.RECURRENT,
fromDate: new Date('2023-01-15'),
toDate: new Date('2023-06-30'),
schedule: {},
},
property: '',
}),
).toBeFalsy();
});
it('should not validate with invalid from date to date and empty schedule and margin', () => {
expect(
isPunctualOrRecurrent({
value: undefined,
constraints: [],
targetName: '',
object: {
frequency: Frequency.RECURRENT,
departure: new Date('2023-10-20'),
toDate: new Date('2023-10-30'),
},
property: '',
}),
).toBeFalsy();
});
});
});
});

View File

@ -0,0 +1,15 @@
import { Frequency } from '@modules/ad/core/ad.types';
import { intToFrequency } from '@modules/ad/interface/grpc-controllers/dtos/validators/frequency.mapping';
describe('frequency mapping', () => {
it('should return punctual if frequency is 1', () => {
expect(intToFrequency(1)).toBe(Frequency.PUNCTUAL);
});
it('should return recurrent if frequency is 2', () => {
expect(intToFrequency(2)).toBe(Frequency.RECURRENT);
});
it('should throw an error if frequency is unknown', () => {
expect(() => intToFrequency(0)).toThrow();
expect(() => intToFrequency(3)).toThrow();
});
});

View File

@ -1,7 +1,8 @@
import { Waypoint } from '../../../interface/commands/waypoint';
import { hasValidPositionIndexes } from '../../../core/dtos/validators/waypoint-position';
describe('addresses position validators', () => {
const mockAddress1: Waypoint = {
import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/waypoint-position';
import { WaypointDTO } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
describe('addresses position validator', () => {
const mockAddress1: WaypointDTO = {
lon: 48.68944505415954,
lat: 6.176510296462267,
houseNumber: '5',
@ -10,33 +11,33 @@ describe('addresses position validators', () => {
postalCode: '54000',
country: 'France',
};
const mockAddress2: Waypoint = {
const mockAddress2: WaypointDTO = {
lon: 48.8566,
lat: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const mockAddress3: Waypoint = {
const mockAddress3: WaypointDTO = {
lon: 49.2628,
lat: 4.0347,
locality: 'Reims',
postalCode: '51454',
country: 'France',
};
it('should validate if none of position is definded ', () => {
it('should validate if no position is defined', () => {
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeTruthy();
});
it('should throw an error if position are partialy defined ', () => {
it('should not validate if only one position is defined', () => {
mockAddress1.position = 0;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeFalsy();
});
it('should throw an error if position are partialy defined ', () => {
it('should not validate if positions are partially defined', () => {
mockAddress1.position = 0;
mockAddress2.position = null;
mockAddress3.position = undefined;
@ -45,7 +46,7 @@ describe('addresses position validators', () => {
).toBeFalsy();
});
it('should throw an error if positions are not incremented ', () => {
it('should not validate if multiple positions have same value', () => {
mockAddress1.position = 0;
mockAddress2.position = 1;
mockAddress3.position = 1;
@ -53,7 +54,7 @@ describe('addresses position validators', () => {
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeFalsy();
});
it('should validate if all positions are defined and incremented', () => {
it('should validate if all positions are ordered', () => {
mockAddress1.position = 0;
mockAddress2.position = 1;
mockAddress3.position = 2;

View File

@ -8,9 +8,10 @@ import { RepositoriesHealthIndicatorUseCase } from './core/usecases/repositories
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';
@Module({
imports: [TerminusModule],
imports: [TerminusModule, AdModule],
controllers: [HealthGrpcController, HealthHttpController],
providers: [
RepositoriesHealthIndicatorUseCase,

View File

@ -12,7 +12,7 @@ const mockAdRepository = {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new DatabaseErrorException('an error occured in the database');
throw new DatabaseErrorException('An error occured in the database');
}),
};
@ -56,9 +56,11 @@ describe('RepositoriesHealthIndicatorUseCase', () => {
});
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);
});
});
});