From a98e5b3c83f33af0c237764ac814a032289cd5db Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 25 Aug 2023 09:53:41 +0200 Subject: [PATCH] basic match query --- .../ad/interface/message-handlers/ad.types.ts | 1 - .../application/queries/match/match.query.ts | 27 ++++ .../matcher/core/application/types/address.ts | 10 ++ .../core/application/types/coordinates.ts | 4 + .../core/application/types/schedule-item.ts | 5 + .../core/application/types/waypoint.ts | 5 + .../matcher/core/domain/match.types.ts | 8 ++ .../dtos/match.paginated.response.dto.ts | 6 + .../interface/dtos/match.response.dto.ts | 3 + .../grpc-controllers/dtos/address.dto.ts | 26 ++++ .../grpc-controllers/dtos/coordinates.dto.ts | 9 ++ .../dtos/match.request.dto.ts | 119 ++++++++++++++++++ .../dtos/schedule-item.dto.ts | 16 +++ .../decorators/has-day.decorator.ts | 34 +++++ .../has-valid-position-indexes.decorator.ts | 22 ++++ .../decorators/is-after-or-equal.decorator.ts | 43 +++++++ .../has-valid-position-indexes.validator.ts | 17 +++ .../grpc-controllers/dtos/waypoint.dto.ts | 8 ++ .../grpc-controllers/match.grpc-controller.ts | 37 ++++++ .../unit/interface/has-day.decorator.spec.ts | 60 +++++++++ ...s-valid-position-indexes.decorator.spec.ts | 62 +++++++++ ...s-valid-position-indexes.validator.spec.ts | 75 +++++++++++ .../is-after-or-equal.decorator.spec.ts | 45 +++++++ .../interface/match.grpc.controller.spec.ts | 82 ++++++++++++ 24 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 src/modules/matcher/core/application/queries/match/match.query.ts create mode 100644 src/modules/matcher/core/application/types/address.ts create mode 100644 src/modules/matcher/core/application/types/coordinates.ts create mode 100644 src/modules/matcher/core/application/types/schedule-item.ts create mode 100644 src/modules/matcher/core/application/types/waypoint.ts create mode 100644 src/modules/matcher/core/domain/match.types.ts create mode 100644 src/modules/matcher/interface/dtos/match.paginated.response.dto.ts create mode 100644 src/modules/matcher/interface/dtos/match.response.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts diff --git a/src/modules/ad/interface/message-handlers/ad.types.ts b/src/modules/ad/interface/message-handlers/ad.types.ts index 1deb105..5bc7e88 100644 --- a/src/modules/ad/interface/message-handlers/ad.types.ts +++ b/src/modules/ad/interface/message-handlers/ad.types.ts @@ -2,7 +2,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; export type Ad = { id: string; - userId: string; driver: boolean; passenger: boolean; frequency: Frequency; diff --git a/src/modules/matcher/core/application/queries/match/match.query.ts b/src/modules/matcher/core/application/queries/match/match.query.ts new file mode 100644 index 0000000..73531c7 --- /dev/null +++ b/src/modules/matcher/core/application/queries/match/match.query.ts @@ -0,0 +1,27 @@ +import { QueryBase } from '@mobicoop/ddd-library'; +import { Frequency } from '@modules/matcher/core/domain/match.types'; +import { ScheduleItem } from '../../types/schedule-item'; +import { Waypoint } from '../../types/waypoint'; + +export class MatchQuery extends QueryBase { + readonly driver?: boolean; + readonly passenger?: boolean; + readonly frequency?: Frequency; + readonly fromDate: string; + readonly toDate: string; + readonly schedule: ScheduleItem[]; + readonly strict?: boolean; + readonly waypoints: Waypoint[]; + + constructor(props: MatchQuery) { + super(); + this.driver = props.driver; + this.passenger = props.passenger; + this.frequency = props.frequency; + this.fromDate = props.fromDate; + this.toDate = props.toDate; + this.schedule = props.schedule; + this.strict = props.strict; + this.waypoints = props.waypoints; + } +} diff --git a/src/modules/matcher/core/application/types/address.ts b/src/modules/matcher/core/application/types/address.ts new file mode 100644 index 0000000..bdf8e6b --- /dev/null +++ b/src/modules/matcher/core/application/types/address.ts @@ -0,0 +1,10 @@ +import { Coordinates } from './coordinates'; + +export type Address = { + name?: string; + houseNumber?: string; + street?: string; + locality?: string; + postalCode?: string; + country: string; +} & Coordinates; diff --git a/src/modules/matcher/core/application/types/coordinates.ts b/src/modules/matcher/core/application/types/coordinates.ts new file mode 100644 index 0000000..8e149ed --- /dev/null +++ b/src/modules/matcher/core/application/types/coordinates.ts @@ -0,0 +1,4 @@ +export type Coordinates = { + lon: number; + lat: number; +}; diff --git a/src/modules/matcher/core/application/types/schedule-item.ts b/src/modules/matcher/core/application/types/schedule-item.ts new file mode 100644 index 0000000..a40e06d --- /dev/null +++ b/src/modules/matcher/core/application/types/schedule-item.ts @@ -0,0 +1,5 @@ +export type ScheduleItem = { + day?: number; + time: string; + margin?: number; +}; diff --git a/src/modules/matcher/core/application/types/waypoint.ts b/src/modules/matcher/core/application/types/waypoint.ts new file mode 100644 index 0000000..c73e024 --- /dev/null +++ b/src/modules/matcher/core/application/types/waypoint.ts @@ -0,0 +1,5 @@ +import { Address } from './address'; + +export type Waypoint = { + position?: number; +} & Address; diff --git a/src/modules/matcher/core/domain/match.types.ts b/src/modules/matcher/core/domain/match.types.ts new file mode 100644 index 0000000..89be65e --- /dev/null +++ b/src/modules/matcher/core/domain/match.types.ts @@ -0,0 +1,8 @@ +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} + +export enum AlgorithmType { + CLASSIC = 'CLASSIC', +} diff --git a/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts b/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts new file mode 100644 index 0000000..52f75c8 --- /dev/null +++ b/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts @@ -0,0 +1,6 @@ +import { PaginatedResponseDto } from '@mobicoop/ddd-library'; +import { MatchResponseDto } from './match.response.dto'; + +export class MatchPaginatedResponseDto extends PaginatedResponseDto { + readonly data: readonly MatchResponseDto[]; +} diff --git a/src/modules/matcher/interface/dtos/match.response.dto.ts b/src/modules/matcher/interface/dtos/match.response.dto.ts new file mode 100644 index 0000000..a69fc89 --- /dev/null +++ b/src/modules/matcher/interface/dtos/match.response.dto.ts @@ -0,0 +1,3 @@ +export class MatchResponseDto { + adId: string; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts new file mode 100644 index 0000000..9659d96 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts @@ -0,0 +1,26 @@ +import { IsOptional, IsString } from 'class-validator'; +import { CoordinatesDto as CoordinatesDto } from './coordinates.dto'; + +export class AddressDto extends CoordinatesDto { + @IsOptional() + name?: string; + + @IsOptional() + @IsString() + houseNumber?: string; + + @IsOptional() + @IsString() + street?: string; + + @IsOptional() + @IsString() + locality?: string; + + @IsOptional() + @IsString() + postalCode?: string; + + @IsString() + country: string; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts new file mode 100644 index 0000000..cb636ae --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts @@ -0,0 +1,9 @@ +import { IsLatitude, IsLongitude } from 'class-validator'; + +export class CoordinatesDto { + @IsLongitude() + lon: number; + + @IsLatitude() + lat: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts new file mode 100644 index 0000000..607d485 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts @@ -0,0 +1,119 @@ +import { + AlgorithmType, + Frequency, +} from '@modules/matcher/core/domain/match.types'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsDecimal, + IsEnum, + IsISO8601, + IsInt, + IsOptional, + Max, + Min, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { HasDay } from './validators/decorators/has-day.decorator'; +import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decorator'; +import { ScheduleItemDto } from './schedule-item.dto'; +import { WaypointDto } from './waypoint.dto'; +import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator'; + +export class MatchRequestDto { + @IsOptional() + @IsBoolean() + driver?: boolean; + + @IsOptional() + @IsBoolean() + passenger?: boolean; + + @IsEnum(Frequency) + @HasDay('schedule', { + message: 'At least a day is required for a recurrent ad', + }) + frequency: Frequency; + + @IsISO8601({ + strict: true, + strictSeparator: true, + }) + fromDate: string; + + @IsISO8601({ + strict: true, + strictSeparator: true, + }) + @IsAfterOrEqual('fromDate', { + message: 'toDate must be after or equal to fromDate', + }) + toDate: string; + + @Type(() => ScheduleItemDto) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + schedule: ScheduleItemDto[]; + + @IsOptional() + @IsInt() + seatsProposed?: number; + + @IsOptional() + @IsInt() + seatsRequested?: number; + + @IsOptional() + @IsBoolean() + strict?: boolean; + + @Type(() => WaypointDto) + @IsArray() + @ArrayMinSize(2) + @HasValidPositionIndexes() + @ValidateNested({ each: true }) + waypoints: WaypointDto[]; + + @IsOptional() + @IsEnum(AlgorithmType) + algorithm?: AlgorithmType; + + @IsOptional() + @IsInt() + remoteness?: number; + + @IsOptional() + @IsBoolean() + useProportion?: boolean; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + proportion?: number; + + @IsOptional() + @IsBoolean() + useAzimuth?: boolean; + + @IsOptional() + @IsInt() + @Min(0) + @Max(359) + azimuthMargin?: number; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDistanceRatio?: number; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDurationRatio?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts new file mode 100644 index 0000000..112adc2 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts @@ -0,0 +1,16 @@ +import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator'; + +export class ScheduleItemDto { + @IsOptional() + @IsInt() + @Min(0) + @Max(6) + day?: number; + + @IsMilitaryTime() + time: string; + + @IsOptional() + @IsInt() + margin?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts new file mode 100644 index 0000000..ed3cf0f --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts @@ -0,0 +1,34 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { + registerDecorator, + ValidationOptions, + ValidationArguments, +} from 'class-validator'; + +export function HasDay( + property: string, + validationOptions?: ValidationOptions, +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'hasDay', + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[relatedPropertyName]; + return ( + value == Frequency.PUNCTUAL || + (Array.isArray(relatedValue) && + relatedValue.some((scheduleItem) => + scheduleItem.hasOwnProperty('day'), + )) + ); + }, + }, + }); + }; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts new file mode 100644 index 0000000..87e3a36 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts @@ -0,0 +1,22 @@ +import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator'; +import { hasValidPositionIndexes } from '../has-valid-position-indexes.validator'; +import { WaypointDto } from '../../waypoint.dto'; + +export const HasValidPositionIndexes = ( + validationOptions?: ValidationOptions, +): PropertyDecorator => + ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (waypoints: WaypointDto[]): boolean => + hasValidPositionIndexes(waypoints), + defaultMessage: buildMessage( + () => `invalid waypoints positions`, + validationOptions, + ), + }, + }, + validationOptions, + ); diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts new file mode 100644 index 0000000..fb6e734 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts @@ -0,0 +1,43 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, + isISO8601, +} from 'class-validator'; + +export function IsAfterOrEqual( + property: string, + validationOptions?: ValidationOptions, +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isAfterOrEqual', + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[relatedPropertyName]; + if ( + !( + typeof value === 'string' && + typeof relatedValue === 'string' && + isISO8601(value, { + strict: true, + strictSeparator: true, + }) && + isISO8601(relatedValue, { + strict: true, + strictSeparator: true, + }) + ) + ) + return false; + return new Date(value) >= new Date(relatedValue); + }, + }, + }); + }; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts new file mode 100644 index 0000000..04504a8 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts @@ -0,0 +1,17 @@ +import { WaypointDto } from '../waypoint.dto'; + +export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => { + if (!waypoints) return false; + if (waypoints.length == 0) return false; + if (waypoints.every((waypoint) => waypoint.position === undefined)) + return false; + if (waypoints.every((waypoint) => typeof waypoint.position === 'number')) { + const positions = Array.from(waypoints, (waypoint) => waypoint.position); + positions.sort(); + for (let i = 1; i < positions.length; i++) + if (positions[i] != positions[i - 1] + 1) return false; + + return true; + } + return false; +}; diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts new file mode 100644 index 0000000..ded5386 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts @@ -0,0 +1,8 @@ +import { IsInt, IsOptional } from 'class-validator'; +import { AddressDto } from './address.dto'; + +export class WaypointDto extends AddressDto { + @IsOptional() + @IsInt() + position?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts new file mode 100644 index 0000000..7fdee64 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts @@ -0,0 +1,37 @@ +import { Controller, UsePipes } from '@nestjs/common'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; +import { QueryBus } from '@nestjs/cqrs'; +import { MatchRequestDto } from './dtos/match.request.dto'; +import { MatchQuery } from '@modules/matcher/core/application/queries/match/match.query'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class MatchGrpcController { + constructor(private readonly queryBus: QueryBus) {} + + @GrpcMethod('MatcherService', 'Match') + async match(data: MatchRequestDto): Promise { + try { + const matches = await this.queryBus.execute(new MatchQuery(data)); + return { + data: matches, + page: 1, + perPage: 5, + total: 1, + }; + } catch (e) { + throw new RpcException({ + code: RpcExceptionCode.UNKNOWN, + message: e.message, + }); + } + } +} diff --git a/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts new file mode 100644 index 0000000..50c29e8 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts @@ -0,0 +1,60 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { ScheduleItemDto } from '@modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto'; +import { HasDay } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; +import { Validator } from 'class-validator'; + +describe('Has day decorator', () => { + class MyClass { + @HasDay('schedule', { + message: 'At least a day is required for a recurrent ad', + }) + frequency: Frequency; + + schedule: ScheduleItemDto[]; + } + + it('should return a property decorator has a function', () => { + const hasDay = HasDay('someProperty'); + expect(typeof hasDay).toBe('function'); + }); + + it('should validate a punctual frequency associated with a valid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.PUNCTUAL; + myClassInstance.schedule = [ + { + time: '07:15', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should validate a recurrent frequency associated with a valid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.RECURRENT; + myClassInstance.schedule = [ + { + time: '07:15', + day: 1, + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should not validate a recurrent frequency associated with an invalid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.RECURRENT; + myClassInstance.schedule = [ + { + time: '07:15', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts new file mode 100644 index 0000000..54fb8d1 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts @@ -0,0 +1,62 @@ +import { HasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { Validator } from 'class-validator'; + +describe('valid position indexes decorator', () => { + class MyClass { + @HasValidPositionIndexes() + waypoints: WaypointDto[]; + } + it('should return a property decorator has a function', () => { + const hasValidPositionIndexes = HasValidPositionIndexes(); + expect(typeof hasValidPositionIndexes).toBe('function'); + }); + it('should validate an array of waypoints with valid positions', async () => { + const myClassInstance = new MyClass(); + myClassInstance.waypoints = [ + { + position: 0, + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }, + { + position: 1, + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + it('should not validate an array of waypoints with invalid positions', async () => { + const myClassInstance = new MyClass(); + myClassInstance.waypoints = [ + { + position: 1, + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }, + { + position: 1, + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts new file mode 100644 index 0000000..a0c0661 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts @@ -0,0 +1,75 @@ +import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; + +describe('addresses position validator', () => { + const mockAddress1: WaypointDto = { + lon: 48.689445, + lat: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + }; + const mockAddress2: WaypointDto = { + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }; + const mockAddress3: WaypointDto = { + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }; + + it('should not validate if no position is defined', () => { + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should not validate if only one position is defined', () => { + mockAddress1.position = 0; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should not validate if positions are partially defined', () => { + mockAddress1.position = 0; + mockAddress2.position = null; + mockAddress3.position = undefined; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + + it('should not validate if multiple positions have same value', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 1; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should validate if all positions are ordered', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 2; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + mockAddress1.position = 1; + mockAddress2.position = 2; + mockAddress3.position = 3; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + }); + it('should not validate if no waypoints are defined', () => { + expect(hasValidPositionIndexes(undefined)).toBeFalsy(); + expect(hasValidPositionIndexes([])).toBeFalsy(); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts new file mode 100644 index 0000000..b388a2c --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts @@ -0,0 +1,45 @@ +import { IsAfterOrEqual } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; +import { Validator } from 'class-validator'; + +describe('Is after or equal decorator', () => { + class MyClass { + firstDate: string; + + @IsAfterOrEqual('firstDate', { + message: 'secondDate must be after or equal to firstDate', + }) + secondDate: string; + } + + it('should return a property decorator has a function', () => { + const isAfterOrEqual = IsAfterOrEqual('someProperty'); + expect(typeof isAfterOrEqual).toBe('function'); + }); + + it('should validate a secondDate posterior to firstDate', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-20'; + myClassInstance.secondDate = '2023-07-30'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should not validate a secondDate prior to firstDate', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-20'; + myClassInstance.secondDate = '2023-07-19'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); + + it('should not validate if dates are invalid', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-40'; + myClassInstance.secondDate = '2023-07-19'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts new file mode 100644 index 0000000..7c1ef68 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts @@ -0,0 +1,82 @@ +import { Frequency } from '@modules/matcher/core/domain/match.types'; +import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller'; +import { QueryBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const originWaypoint: WaypointDto = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: WaypointDto = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const punctualMatchRequestDto: MatchRequestDto = { + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-15', + toDate: '2023-08-15', + schedule: [ + { + time: '07:00', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], +}; + +const mockQueryBus = { + execute: jest.fn().mockImplementation(() => [ + { + adId: 1, + }, + { + adId: 2, + }, + ]), +}; + +describe('Match Grpc Controller', () => { + let matchGrpcController: MatchGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchGrpcController, + { + provide: QueryBus, + useValue: mockQueryBus, + }, + ], + }).compile(); + + matchGrpcController = module.get(MatchGrpcController); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(matchGrpcController).toBeDefined(); + }); + + it('should return matches', async () => { + const matchPaginatedResponseDto = await matchGrpcController.match( + punctualMatchRequestDto, + ); + expect(matchPaginatedResponseDto.data).toHaveLength(2); + }); +});