improve tests, handle coordinates precision

This commit is contained in:
sbriat
2023-06-23 11:37:26 +02:00
parent 4ad00b96c0
commit 211bee2c70
33 changed files with 354 additions and 168 deletions

View File

@@ -1,8 +1,8 @@
import { Paginated } from '../ddd';
export abstract class PaginatedResponseDto<T> extends Paginated<T> {
readonly count: number;
readonly limit: number;
readonly total: number;
readonly perPage: number;
readonly page: number;
abstract readonly data: readonly T[];
}

View File

@@ -2,13 +2,16 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { AggregateRoot, Mapper, RepositoryPort } from '../ddd';
import { ObjectLiteral } from '../types';
import { LoggerPort } from '../ports/logger.port';
import { None, Option, Some } from 'oxide.ts';
import {
PrismaRawRepositoryPort,
PrismaRepositoryPort,
} from '../ports/prisma-repository.port';
import { Prisma } from '@prisma/client';
import { ConflictException, DatabaseErrorException } from '@libs/exceptions';
import {
ConflictException,
DatabaseErrorException,
NotFoundException,
} from '@libs/exceptions';
export abstract class PrismaRepositoryBase<
Aggregate extends AggregateRoot<any>,
@@ -24,12 +27,13 @@ export abstract class PrismaRepositoryBase<
protected readonly logger: LoggerPort,
) {}
async findOneById(id: string, include?: any): Promise<Option<Aggregate>> {
async findOneById(id: string, include?: any): Promise<Aggregate> {
const entity = await this.prisma.findUnique({
where: { uuid: id },
include,
});
return entity ? Some(this.mapper.toDomain(entity)) : None;
if (entity) return this.mapper.toDomain(entity);
throw new NotFoundException('Record not found');
}
async insert(entity: Aggregate): Promise<void> {
@@ -52,7 +56,6 @@ export abstract class PrismaRepositoryBase<
await this.prisma.$queryRaw`SELECT 1`;
return true;
} catch (e) {
console.log(e);
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseErrorException(e.message);
}

View File

@@ -0,0 +1,31 @@
import { OrderBy, PaginatedQueryParams } from './repository.port';
/**
* Base class for regular queries
*/
export abstract class QueryBase {}
/**
* Base class for paginated queries
*/
export abstract class PaginatedQueryBase extends QueryBase {
perPage: number;
offset: number;
orderBy: OrderBy;
page: number;
constructor(props: PaginatedParams<PaginatedQueryBase>) {
super();
this.perPage = props.perPage || 10;
this.offset = props.page ? props.page * this.perPage : 0;
this.page = props.page || 0;
this.orderBy = props.orderBy || { field: true, param: 'desc' };
}
}
// Paginated query parameters
export type PaginatedParams<T> = Omit<
T,
'perPage' | 'offset' | 'orderBy' | 'page'
> &
Partial<Omit<PaginatedQueryParams, 'offset'>>;

View File

@@ -1,5 +1,3 @@
import { Option } from 'oxide.ts';
/* Most of repositories will probably need generic
save/find/delete operations, so it's easier
to have some shared interfaces.
@@ -8,14 +6,14 @@ import { Option } from 'oxide.ts';
*/
export class Paginated<T> {
readonly count: number;
readonly limit: number;
readonly total: number;
readonly perPage: number;
readonly page: number;
readonly data: readonly T[];
constructor(props: Paginated<T>) {
this.count = props.count;
this.limit = props.limit;
this.total = props.total;
this.perPage = props.perPage;
this.page = props.page;
this.data = props.data;
}
@@ -24,7 +22,7 @@ export class Paginated<T> {
export type OrderBy = { field: string | true; param: 'asc' | 'desc' };
export type PaginatedQueryParams = {
limit: number;
perPage: number;
page: number;
offset: number;
orderBy: OrderBy;
@@ -32,7 +30,7 @@ export type PaginatedQueryParams = {
export interface RepositoryPort<Entity> {
insert(entity: Entity | Entity[]): Promise<void>;
findOneById(id: string): Promise<Option<Entity>>;
findOneById(id: string, include?: any): Promise<Entity>;
healthCheck(): Promise<boolean>;
// findAll(): Promise<Entity[]>;
// findAllPaginated(params: PaginatedQueryParams): Promise<Paginated<Entity>>;

View File

@@ -62,49 +62,49 @@ export class AdMapper
fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate),
monTime: copy.schedule.mon
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.mon,
timezone,
)
: undefined,
tueTime: copy.schedule.tue
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.tue,
timezone,
)
: undefined,
wedTime: copy.schedule.wed
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.wed,
timezone,
)
: undefined,
thuTime: copy.schedule.thu
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.thu,
timezone,
)
: undefined,
friTime: copy.schedule.fri
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.fri,
timezone,
)
: undefined,
satTime: copy.schedule.sat
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.sat,
timezone,
)
: undefined,
sunTime: copy.schedule.sun
? this.timeConverter.dateTimeToUtc(
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.sun,
timezone,
@@ -143,6 +143,11 @@ export class AdMapper
};
toDomain = (record: AdReadModel): AdEntity => {
const timezone = this.timezoneFinder.timezones(
record.waypoints[0].lon,
record.waypoints[0].lat,
this.defaultParams.DEFAULT_TIMEZONE,
)[0];
const entity = new AdEntity({
id: record.uuid,
createdAt: new Date(record.createdAt),
@@ -152,13 +157,23 @@ export class AdMapper
driver: record.driver,
passenger: record.passenger,
frequency: Frequency[record.frequency],
fromDate: record.fromDate.toISOString(),
toDate: record.toDate.toISOString(),
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: {
mon: record.monTime?.toISOString(),
tue: record.tueTime?.toISOString(),
wed: record.wedTime?.toISOString(),
thu: record.thuTime?.toISOString(),
wed: record.wedTime
? this.timeConverter.utcDatetimeToLocalTime(
record.wedTime.toISOString(),
timezone,
)
: undefined,
thu: record.thuTime
? this.timeConverter.utcDatetimeToLocalTime(
record.thuTime.toISOString(),
timezone,
)
: undefined,
fri: record.friTime?.toISOString(),
sat: record.satTime?.toISOString(),
sun: record.sunTime?.toISOString(),
@@ -198,7 +213,27 @@ export class AdMapper
toResponse = (entity: AdEntity): AdResponseDto => {
const props = entity.getProps();
const response = new AdResponseDto(entity);
response.uuid = props.id;
response.userId = props.userId;
response.driver = props.driver;
response.passenger = props.passenger;
response.frequency = props.frequency;
response.fromDate = props.fromDate;
response.toDate = props.toDate;
response.schedule = { ...props.schedule };
response.marginDurations = { ...props.marginDurations };
response.seatsProposed = props.seatsProposed;
response.seatsRequested = props.seatsRequested;
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
position: waypoint.position,
name: waypoint.address.name,
houseNumber: waypoint.address.houseNumber,
street: waypoint.address.street,
postalCode: waypoint.address.postalCode,
locality: waypoint.address.locality,
country: waypoint.address.country,
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
}));
return response;
};

View File

@@ -20,12 +20,15 @@ import { CreateAdService } from './core/commands/create-ad/create-ad.service';
import { TimezoneFinder } from './infrastructure/timezone-finder';
import { PrismaService } from '@libs/db/prisma.service';
import { TimeConverter } from './infrastructure/time-converter';
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { FindAdByIdQueryHandler } from './core/queries/find-ad-by-id/find-ad-by-id.query-handler';
@Module({
imports: [CqrsModule],
controllers: [CreateAdGrpcController],
controllers: [CreateAdGrpcController, FindAdByIdGrpcController],
providers: [
CreateAdService,
FindAdByIdQueryHandler,
PrismaService,
AdMapper,
{

View File

@@ -1,8 +1,9 @@
export interface TimeConverterPort {
dateTimeToUtc(
localDateTimeToUtc(
date: string,
time: string,
timezone: string,
dst?: boolean,
): Date;
utcDatetimeToLocalTime(isoString: string, timezone: string): string;
}

View File

@@ -0,0 +1,17 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { FindAdByIdQuery } from './find-ad-by-id.query';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { Inject } from '@nestjs/common';
import { AdEntity } from '../../ad.entity';
@QueryHandler(FindAdByIdQuery)
export class FindAdByIdQueryHandler implements IQueryHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
) {}
async execute(query: FindAdByIdQuery): Promise<AdEntity> {
return await this.repository.findOneById(query.id, { waypoints: true });
}
}

View File

@@ -1,9 +1,10 @@
import { FindAdByIdRequestDTO } from '../../../interface/queries/find-ad-by-id/dtos/find-ad-by-id.request.dto';
import { QueryBase } from '@libs/ddd/query.base';
export class FindAdByIdQuery {
export class FindAdByIdQuery extends QueryBase {
readonly id: string;
constructor(findAdByIdRequestDTO: FindAdByIdRequestDTO) {
this.id = findAdByIdRequestDTO.id;
constructor(id: string) {
super();
this.id = id;
}
}

View File

@@ -4,7 +4,7 @@ import { DateTime, TimeZone } from 'timezonecomplete';
@Injectable()
export class TimeConverter implements TimeConverterPort {
dateTimeToUtc = (
localDateTimeToUtc = (
date: string,
time: string,
timezone: string,
@@ -21,4 +21,16 @@ export class TimeConverter implements TimeConverterPort {
return undefined;
}
};
utcDatetimeToLocalTime = (isoString: string, timezone: string): string => {
try {
return new DateTime(isoString)
.convert(TimeZone.zone(timezone))
.toString()
.split('T')[1]
.substring(0, 5);
} catch (e) {
return undefined;
}
};
}

View File

@@ -1,6 +0,0 @@
import { AutoMap } from '@automapper/classes';
export class AdPresenter {
@AutoMap()
id: string;
}

View File

@@ -1,5 +1,43 @@
import { ResponseBase } from '@libs/api/response.base';
import { Frequency } from '@modules/ad/core/ad.types';
export class AdResponseDto extends ResponseBase {
uuid: string;
userId: string;
driver: boolean;
passenger: boolean;
frequency: Frequency;
fromDate: string;
toDate: string;
schedule: {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
};
marginDurations: {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
};
seatsProposed: number;
seatsRequested: number;
strict: boolean;
waypoints: {
position: number;
name?: string;
houseNumber?: string;
street?: string;
postalCode?: string;
locality?: string;
country: string;
lon: number;
lat: number;
}[];
}

View File

@@ -2,8 +2,7 @@ import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { AdPresenter } from '../ad.presenter';
import { CreateAdRequestDTO } from './dtos/create-ad.request.dto';
import { CreateAdRequestDto } from './dtos/create-ad.request.dto';
import { CreateAdCommand } from '../../core/commands/create-ad/create-ad.command';
import { Result, match } from 'oxide.ts';
import { AggregateID } from '@libs/ddd';
@@ -21,7 +20,7 @@ export class CreateAdGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AdsService', 'Create')
async create(data: CreateAdRequestDTO): Promise<AdPresenter> {
async create(data: CreateAdRequestDto): Promise<IdResponse> {
const result: Result<AggregateID, AdAlreadyExistsError> =
await this.commandBus.execute(new CreateAdCommand(data));

View File

@@ -1,8 +1,8 @@
import { AutoMap } from '@automapper/classes';
import { IsOptional, IsString } from 'class-validator';
import { CoordinatesDTO } from './coordinates.dto';
import { CoordinatesDto as CoordinatesDto } from './coordinates.dto';
export class AddressDTO extends CoordinatesDTO {
export class AddressDto extends CoordinatesDto {
@IsOptional()
@AutoMap()
name?: string;

View File

@@ -1,11 +1,19 @@
import { AutoMap } from '@automapper/classes';
import { Transform } from 'class-transformer';
import { IsLatitude, IsLongitude } from 'class-validator';
import { toPrecision } from './validators/to-precision';
export class CoordinatesDTO {
export class CoordinatesDto {
@Transform(({ value }) => toPrecision(value, 6), {
toClassOnly: true,
})
@IsLongitude()
@AutoMap()
lon: number;
@Transform(({ value }) => toPrecision(value, 6), {
toClassOnly: true,
})
@IsLatitude()
@AutoMap()
lat: number;

View File

@@ -11,15 +11,15 @@ import {
IsISO8601,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { ScheduleDTO } from './schedule.dto';
import { MarginDurationsDTO } from './margin-durations.dto';
import { WaypointDTO } from './waypoint.dto';
import { ScheduleDto } from './schedule.dto';
import { MarginDurationsDto } from './margin-durations.dto';
import { WaypointDto } from './waypoint.dto';
import { intToFrequency } from './validators/frequency.mapping';
import { IsSchedule } from './validators/decorators/is-schedule.validator';
import { HasValidPositionIndexes } from './validators/decorators/valid-position-indexes.validator';
import { Frequency } from '@modules/ad/core/ad.types';
export class CreateAdRequestDTO {
export class CreateAdRequestDto {
@IsUUID(4)
@AutoMap()
userId: string;
@@ -55,17 +55,17 @@ export class CreateAdRequestDTO {
@AutoMap()
toDate: string;
@Type(() => ScheduleDTO)
@Type(() => ScheduleDto)
@IsSchedule()
@ValidateNested({ each: true })
@AutoMap()
schedule: ScheduleDTO;
schedule: ScheduleDto;
@IsOptional()
@Type(() => MarginDurationsDTO)
@Type(() => MarginDurationsDto)
@ValidateNested({ each: true })
@AutoMap()
marginDurations?: MarginDurationsDTO;
marginDurations?: MarginDurationsDto;
@IsOptional()
@IsInt()
@@ -82,10 +82,11 @@ export class CreateAdRequestDTO {
@AutoMap()
strict?: boolean;
@Type(() => WaypointDto)
@IsArray()
@ArrayMinSize(2)
@HasValidPositionIndexes()
@ValidateNested({ each: true })
@AutoMap()
waypoints: WaypointDTO[];
waypoints: WaypointDto[];
}

View File

@@ -1,6 +1,6 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class FindAdByIdRequestDTO {
export class FindAdByIdRequestDto {
@IsString()
@IsNotEmpty()
id: string;

View File

@@ -1,7 +1,7 @@
import { AutoMap } from '@automapper/classes';
import { IsInt, IsOptional } from 'class-validator';
export class MarginDurationsDTO {
export class MarginDurationsDto {
@IsOptional()
@IsInt()
@AutoMap()

View File

@@ -1,7 +1,7 @@
import { AutoMap } from '@automapper/classes';
import { IsOptional, IsMilitaryTime } from 'class-validator';
export class ScheduleDTO {
export class ScheduleDto {
@IsOptional()
@IsMilitaryTime()
@AutoMap()

View File

@@ -1,6 +1,6 @@
import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator';
import { hasValidPositionIndexes } from '../waypoint-position';
import { WaypointDTO } from '../../waypoint.dto';
import { WaypointDto } from '../../waypoint.dto';
export const HasValidPositionIndexes = (
validationOptions?: ValidationOptions,
@@ -10,7 +10,7 @@ export const HasValidPositionIndexes = (
name: '',
constraints: [],
validator: {
validate: (waypoints: WaypointDTO[]): boolean =>
validate: (waypoints: WaypointDto[]): boolean =>
hasValidPositionIndexes(waypoints),
defaultMessage: buildMessage(
() => `invalid waypoints positions`,

View File

@@ -0,0 +1,4 @@
export const toPrecision = (input: number, precision: number): number => {
const multiplier = 10 ** precision;
return Math.round((input + Number.EPSILON) * multiplier) / multiplier;
};

View File

@@ -1,6 +1,6 @@
import { WaypointDTO } from '../waypoint.dto';
import { WaypointDto } from '../waypoint.dto';
export const hasValidPositionIndexes = (waypoints: WaypointDTO[]): boolean => {
export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => {
if (!waypoints) return;
if (waypoints.every((waypoint) => waypoint.position === undefined))
return true;

View File

@@ -1,8 +1,8 @@
import { AutoMap } from '@automapper/classes';
import { IsInt, IsOptional } from 'class-validator';
import { AddressDTO } from './address.dto';
import { AddressDto } from './address.dto';
export class WaypointDTO extends AddressDTO {
export class WaypointDto extends AddressDto {
@IsOptional()
@IsInt()
@AutoMap()

View File

@@ -0,0 +1,38 @@
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '@utils/pipes/rpc.validation-pipe';
import { FindAdByIdRequestDto } from './dtos/find-ad-by-id.request.dto';
import { FindAdByIdQuery } from '@modules/ad/core/queries/find-ad-by-id/find-ad-by-id.query';
import { AdResponseDto } from '../dtos/ad.response.dto';
import { AdEntity } from '@modules/ad/core/ad.entity';
import { AdMapper } from '@modules/ad/ad.mapper';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class FindAdByIdGrpcController {
constructor(
protected readonly mapper: AdMapper,
private readonly queryBus: QueryBus,
) {}
@GrpcMethod('AdsService', 'FindOneById')
async findOnebyId(data: FindAdByIdRequestDto): Promise<AdResponseDto> {
try {
const ad: AdEntity = await this.queryBus.execute(
new FindAdByIdQuery(data.id),
);
return this.mapper.toResponse(ad);
} catch (e) {
throw new RpcException({
code: e.code,
message: e.message,
});
}
}
}

View File

@@ -1,37 +0,0 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '../../../../../utils/pipes/rpc.validation-pipe';
import { FindAdByIdRequestDTO } from './dtos/find-ad-by-id.request.dto';
import { AdPresenter } from '../../ad.presenter';
import { FindAdByIdQuery } from '../../../core/queries/find-ad-by-id/find-ad-by-id.query';
import { AdEntity } from '../../../core/ad.entity';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class FindAdByIdGrpcController {
constructor(
private readonly queryBus: QueryBus,
@InjectMapper() private readonly mapper: Mapper,
) {}
@GrpcMethod('AdsService', 'FindOneById')
async findOnebyId(data: FindAdByIdRequestDTO): Promise<AdPresenter> {
try {
const ad = await this.queryBus.execute(new FindAdByIdQuery(data));
return this.mapper.map(ad, AdEntity, AdPresenter);
} catch (e) {
throw new RpcException({
code: e.code,
message: e.message,
});
}
}
}

View File

@@ -3,10 +3,12 @@ import {
AD_REPOSITORY,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
@@ -274,6 +276,10 @@ describe('Ad Repository', () => {
provide: TIMEZONE_FINDER,
useClass: TimezoneFinder,
},
{
provide: TIME_CONVERTER,
useClass: TimeConverter,
},
],
}).compile();
prismaService = module.get<PrismaService>(PrismaService);
@@ -416,7 +422,7 @@ describe('Ad Repository', () => {
waypoints: true,
});
expect(result.unwrap().id).toBe(baseUuid.uuid);
expect(result.id).toBe(baseUuid.uuid);
});
// it('should return null', async () => {

View File

@@ -45,8 +45,8 @@ const adEntity: AdEntity = new AdEntity({
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.68944505415954,
lon: 6.176510296462267,
lat: 48.689445,
lon: 6.1765102,
},
},
},
@@ -103,8 +103,8 @@ const adReadModel: AdReadModel = {
locality: 'Nancy',
postalCode: '54000',
country: 'France',
lat: 48.68944505415954,
lon: 6.176510296462267,
lat: 48.689445,
lon: 6.1765102,
createdAt: now,
updatedAt: now,
},
@@ -163,12 +163,13 @@ const mockTimezoneFinder: TimezoneFinderPort = {
};
const mockTimeConverter: TimeConverterPort = {
dateTimeToUtc: jest
localDateTimeToUtc: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementation((datetime: Date, timezone: string, dst?: boolean) => {
return datetime;
}),
utcDatetimeToLocalTime: jest.fn(),
};
describe('Ad Mapper', () => {
@@ -208,13 +209,13 @@ describe('Ad Mapper', () => {
it('should map persisted data to domain entity', async () => {
const mapped: AdEntity = adMapper.toDomain(adReadModel);
expect(mapped.getProps().waypoints[0].address.coordinates.lat).toBe(
48.68944505415954,
48.689445,
);
expect(mapped.getProps().waypoints[1].address.coordinates.lon).toBe(2.3522);
});
it('should map domain entity to response', async () => {
const mapped: AdResponseDto = adMapper.toResponse(adEntity);
expect(mapped.uuid).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
});
});

View File

@@ -2,8 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { CreateAdService } from '@modules/ad/core/commands/create-ad/create-ad.service';
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
import { DefaultParamsProviderPort } from '@modules/ad/core/ports/default-params-provider.port';
import { WaypointDTO } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { CreateAdRequestDTO } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
import { Frequency } from '@modules/ad/core/ad.types';
import { CreateAdCommand } from '@modules/ad/core/commands/create-ad/create-ad.command';
import { Result } from 'oxide.ts';
@@ -12,7 +12,7 @@ import { AdAlreadyExistsError } from '@modules/ad/core/ad.errors';
import { AdEntity } from '@modules/ad/core/ad.entity';
import { ConflictException } from '@libs/exceptions';
const originWaypoint: WaypointDTO = {
const originWaypoint: WaypointDto = {
position: 0,
lon: 48.68944505415954,
lat: 6.176510296462267,
@@ -22,7 +22,7 @@ const originWaypoint: WaypointDTO = {
postalCode: '54000',
country: 'France',
};
const destinationWaypoint: WaypointDTO = {
const destinationWaypoint: WaypointDto = {
position: 1,
lon: 48.8566,
lat: 2.3522,
@@ -30,7 +30,7 @@ const destinationWaypoint: WaypointDTO = {
postalCode: '75000',
country: 'France',
};
const punctualCreateAdRequest: CreateAdRequestDTO = {
const punctualCreateAdRequest: CreateAdRequestDto = {
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
fromDate: '2023-12-21',
toDate: '2023-12-21',

View File

@@ -5,48 +5,81 @@ describe('Time Converter', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(timeConverter).toBeDefined();
});
it('should convert a paris datetime to utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.dateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime.toISOString()).toEqual('2023-06-22T06:00:00.000Z');
describe('localDateTimeToUtc', () => {
it('should convert a paris datetime to utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime.toISOString()).toEqual('2023-06-22T06:00:00.000Z');
});
it('should return undefined if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-16-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '28:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
'Foo/Bar',
);
expect(utcDatetime).toBeUndefined();
});
});
it('should return undefined if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-16-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.dateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '28:00';
const utcDatetime = timeConverter.dateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.dateTimeToUtc(
parisDate,
parisTime,
'Foo/Bar',
);
expect(utcDatetime).toBeUndefined();
describe('utcDatetimeToLocalTime', () => {
it('should convert an utc datetime isostring to a paris local time', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Europe/Paris',
);
expect(parisTime).toBe('08:25');
});
it('should return undefined if isostring input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = 'not_an_isostring';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Foo/Bar',
);
expect(parisTime).toBeUndefined();
});
});
});

View File

@@ -1,8 +1,8 @@
import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/waypoint-position';
import { WaypointDTO } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
describe('addresses position validator', () => {
const mockAddress1: WaypointDTO = {
const mockAddress1: WaypointDto = {
lon: 48.68944505415954,
lat: 6.176510296462267,
houseNumber: '5',
@@ -11,14 +11,14 @@ describe('addresses position validator', () => {
postalCode: '54000',
country: 'France',
};
const mockAddress2: WaypointDTO = {
const mockAddress2: WaypointDto = {
lon: 48.8566,
lat: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const mockAddress3: WaypointDTO = {
const mockAddress3: WaypointDto = {
lon: 49.2628,
lat: 4.0347,
locality: 'Reims',

View File

@@ -1,6 +1,6 @@
import { ArgumentMetadata } from '@nestjs/common';
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
import { FindAdByIdRequestDTO } from '../../../modules/ad/interface/queries/find-ad-by-id/dtos/find-ad-by-id.request.dto';
import { FindAdByIdRequestDto } from '../../../modules/ad/interface/grpc-controllers/dtos/find-ad-by-id.request.dto';
describe('RpcValidationPipe', () => {
it('should not validate request', async () => {
@@ -10,10 +10,10 @@ describe('RpcValidationPipe', () => {
});
const metadata: ArgumentMetadata = {
type: 'body',
metatype: FindAdByIdRequestDTO,
metatype: FindAdByIdRequestDto,
data: '',
};
await target.transform(<FindAdByIdRequestDTO>{}, metadata).catch((err) => {
await target.transform(<FindAdByIdRequestDto>{}, metadata).catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
});