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

@ -38,8 +38,8 @@ CREATE TABLE "waypoint" (
"uuid" UUID NOT NULL,
"adUuid" UUID NOT NULL,
"position" SMALLINT NOT NULL,
"lon" DOUBLE PRECISION NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lon" DECIMAL(9,6) NOT NULL,
"lat" DECIMAL(8,6) NOT NULL,
"name" TEXT,
"houseNumber" TEXT,
"street" TEXT,

View File

@ -47,8 +47,8 @@ model Waypoint {
uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid
position Int @db.SmallInt
lon Float
lat Float
lon Decimal @db.Decimal(9, 6)
lat Decimal @db.Decimal(8, 6)
name String?
houseNumber String?
street String?

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