move tests folder to the root

This commit is contained in:
Sylvain Briat
2024-02-16 08:36:59 +01:00
parent 540c63d297
commit 909ef04e69
29 changed files with 4 additions and 4 deletions

View File

@@ -0,0 +1,314 @@
import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY,
OUTPUT_DATETIME_TRANSFORMER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
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';
import { Test } from '@nestjs/testing';
describe('Ad Repository', () => {
let prismaService: PrismaService;
let adRepository: AdRepository;
const executeInsertCommand = async (table: string, object: any) => {
const command = `INSERT INTO ${table} ("${Object.keys(object).join(
'","',
)}") VALUES (${Object.values(object).join(',')})`;
await prismaService.$executeRawUnsafe(command);
};
const getSeed = (index: number, uuid: string): string => {
return `'${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}'`;
};
const baseUuid = {
uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
};
const baseScheduleUuid = {
uuid: 'bad5e786-3b15-4e51-a8fc-926fa9327ff1',
};
const baseOriginWaypointUuid = {
uuid: 'bad5e786-3b15-4e51-a8fc-926fa9327ff1',
};
const baseDestinationWaypointUuid = {
uuid: '4d200eb6-7389-487f-a1ca-dbc0e40381c9',
};
const baseUserUuid = {
userUuid: "'113e0000-0000-4000-a000-000000000000'",
};
const driverAd = {
driver: 'true',
passenger: 'false',
seatsProposed: 3,
seatsRequested: 1,
strict: 'false',
};
const punctualAd = {
frequency: `'PUNCTUAL'`,
fromDate: `'2023-01-01'`,
toDate: `'2023-01-01'`,
};
const schedulePunctualAd = {
day: 0,
time: `'07:00'`,
margin: 900,
};
const originWaypoint = {
position: 0,
lat: 43.7102,
lon: 7.262,
locality: "'Nice'",
postalCode: "'06000'",
country: "'France'",
};
const destinationWaypoint = {
position: 1,
lat: 43.2965,
lon: 5.3698,
locality: "'Marseille'",
postalCode: "'13000'",
country: "'France'",
};
const createPunctualDriverAds = async (nbToCreate = 10) => {
const adToCreate = {
...baseUuid,
...baseUserUuid,
...driverAd,
...punctualAd,
};
for (let i = 0; i < nbToCreate; i++) {
adToCreate.uuid = getSeed(i, baseUuid.uuid);
await executeInsertCommand('ad', adToCreate);
await executeInsertCommand('schedule_item', {
uuid: getSeed(i, baseScheduleUuid.uuid),
adUuid: adToCreate.uuid,
...schedulePunctualAd,
});
await executeInsertCommand('waypoint', {
uuid: getSeed(i, baseOriginWaypointUuid.uuid),
adUuid: adToCreate.uuid,
...originWaypoint,
});
await executeInsertCommand('waypoint', {
uuid: getSeed(i, baseDestinationWaypointUuid.uuid),
adUuid: adToCreate.uuid,
...destinationWaypoint,
});
}
};
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
const mockLogger = {
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
EventEmitterModule.forRoot(),
ConfigModule.forRoot({ isGlobal: true }),
],
providers: [
PrismaService,
AdMapper,
{
provide: AD_REPOSITORY,
useClass: AdRepository,
},
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
{
provide: TIMEZONE_FINDER,
useClass: TimezoneFinder,
},
{
provide: TIME_CONVERTER,
useClass: TimeConverter,
},
{
provide: OUTPUT_DATETIME_TRANSFORMER,
useClass: OutputDateTimeTransformer,
},
],
})
// disable logging
.setLogger(mockLogger)
.compile();
prismaService = module.get<PrismaService>(PrismaService);
adRepository = module.get<AdRepository>(AD_REPOSITORY);
});
afterAll(async () => {
await prismaService.$disconnect();
});
beforeEach(async () => {
await prismaService.ad.deleteMany();
});
describe('findOneById', () => {
it('should return an ad', async () => {
await createPunctualDriverAds(1);
const result = await adRepository.findOneById(baseUuid.uuid, {
waypoints: true,
schedule: true,
});
expect(result.id).toBe(baseUuid.uuid);
});
});
describe('create', () => {
it('should create a punctual ad', async () => {
const beforeCount = await prismaService.ad.count();
const createAdProps: CreateAdProps = {
userId: 'b4b56444-f8d3-4110-917c-e37bba77f383',
driver: true,
passenger: false,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-02-01',
toDate: '2023-02-01',
schedule: [
{
day: 3,
time: '12:05',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
address: {
locality: 'Nice',
postalCode: '06000',
country: 'France',
coordinates: {
lat: 43.7102,
lon: 7.262,
},
},
},
{
position: 1,
address: {
locality: 'Marseille',
postalCode: '13000',
country: 'France',
coordinates: {
lat: 43.2965,
lon: 5.3698,
},
},
},
],
};
const adToCreate: AdEntity = AdEntity.create(createAdProps);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();
expect(afterCount - beforeCount).toBe(1);
});
it('should create a recurrent ad', async () => {
const beforeCount = await prismaService.ad.count();
const createAdProps: CreateAdProps = {
userId: 'b4b56444-f8d3-4110-917c-e37bba77f383',
driver: true,
passenger: false,
frequency: Frequency.RECURRENT,
fromDate: '2023-02-01',
toDate: '2024-01-31',
schedule: [
{
day: 1,
time: '08:00',
margin: 900,
},
{
day: 2,
time: '08:00',
margin: 900,
},
{
day: 3,
time: '09:00',
margin: 900,
},
{
day: 4,
time: '08:00',
margin: 900,
},
{
day: 5,
time: '08:00',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
address: {
locality: 'Nice',
postalCode: '06000',
country: 'France',
coordinates: {
lat: 43.7102,
lon: 7.262,
},
},
},
{
position: 1,
address: {
locality: 'Marseille',
postalCode: '13000',
country: 'France',
coordinates: {
lat: 43.2965,
lon: 5.3698,
},
},
},
],
};
const adToCreate: AdEntity = AdEntity.create(createAdProps);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();
expect(afterCount - beforeCount).toBe(1);
});
});
});

View File

@@ -0,0 +1,166 @@
import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency, Status } from '@modules/ad/core/domain/ad.types';
import {
AdReadModel,
AdWriteModel,
} from '@modules/ad/infrastructure/ad.repository';
import { AdResponseDto } from '@modules/ad/interface/dtos/ad.response.dto';
import { Test } from '@nestjs/testing';
const now = new Date('2023-06-21 06:00:00');
const adEntity: AdEntity = new AdEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
props: {
userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false,
passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-21',
toDate: '2023-06-21',
schedule: [
{
day: 3,
time: '07:15',
margin: 900,
},
],
waypoints: [
{
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.1765102,
},
},
},
{
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
},
],
strict: false,
seatsProposed: 3,
seatsRequested: 1,
},
createdAt: now,
updatedAt: now,
});
const adReadModel: AdReadModel = {
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false,
passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'),
schedule: [
{
uuid: '3978f3d6-560f-4a8f-83ba-9bf5aa9a2d27',
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
createdAt: now,
updatedAt: now,
},
],
waypoints: [
{
uuid: '6f53f55e-2bdb-4c23-b6a9-6d7b498e47b9',
position: 0,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
lat: 48.689445,
lon: 6.1765102,
createdAt: now,
updatedAt: now,
},
{
uuid: 'e18c6a84-0ab7-4e44-af1d-829d0b0d0573',
position: 1,
locality: 'Paris',
postalCode: '75000',
country: 'France',
lat: 48.8566,
lon: 2.3522,
createdAt: now,
updatedAt: now,
},
],
strict: false,
seatsProposed: 3,
seatsRequested: 1,
createdAt: now,
updatedAt: now,
};
const mockOutputDatetimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('Ad Mapper', () => {
let adMapper: AdMapper;
beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [
AdMapper,
{
provide: OUTPUT_DATETIME_TRANSFORMER,
useValue: mockOutputDatetimeTransformer,
},
],
}).compile();
adMapper = module.get<AdMapper>(AdMapper);
});
it('should be defined', () => {
expect(adMapper).toBeDefined();
});
it('should map domain entity to persistence data', async () => {
const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
expect(mapped.waypoints?.create[0].uuid.length).toBe(36);
expect(mapped.waypoints?.create[1].uuid.length).toBe(36);
expect(mapped.schedule?.create.length).toBe(1);
});
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.689445,
);
expect(mapped.getProps().waypoints[1].address.coordinates.lon).toBe(2.3522);
expect(mapped.getProps().schedule.length).toBe(1);
expect(mapped.getProps().schedule[0].time).toBe('07:05');
});
it('should map domain entity to response', async () => {
const mapped: AdResponseDto = adMapper.toResponse(adEntity);
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
});
});

View File

@@ -0,0 +1,228 @@
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
Frequency,
Status,
} from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-21',
toDate: '2023-06-21',
schedule: [
{
day: 3,
time: '08:30',
margin: 600,
},
],
frequency: Frequency.PUNCTUAL,
};
const recurrentCreateAdProps = {
fromDate: '2023-06-21',
toDate: '2024-06-20',
schedule: [
{
day: 1,
time: '08:30',
margin: 600,
},
{
day: 2,
time: '08:30',
margin: 600,
},
{
day: 3,
time: '08:00',
margin: 600,
},
{
day: 4,
time: '08:30',
margin: 600,
},
{
day: 5,
time: '08:30',
margin: 600,
},
],
frequency: Frequency.RECURRENT,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const recurrentPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
driver: false,
passenger: true,
};
const punctualDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: true,
passenger: false,
};
const recurrentDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
driver: true,
passenger: false,
};
const punctualDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: true,
passenger: true,
};
const recurrentDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
driver: true,
passenger: true,
};
describe('Ad entity create', () => {
describe('With complete props', () => {
it('should create a new punctual passenger ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
expect(punctualPassengerAd.id.length).toBe(36);
expect(punctualPassengerAd.getProps().status).toBe(Status.PENDING);
expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualPassengerAd.getProps().driver).toBeFalsy();
expect(punctualPassengerAd.getProps().passenger).toBeTruthy();
});
it('should create a new punctual driver ad entity', async () => {
const punctualDriverAd: AdEntity = AdEntity.create(
punctualDriverCreateAdProps,
);
expect(punctualDriverAd.id.length).toBe(36);
expect(punctualDriverAd.getProps().schedule.length).toBe(1);
expect(punctualDriverAd.getProps().schedule[0].day).toBe(3);
expect(punctualDriverAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualDriverAd.getProps().driver).toBeTruthy();
expect(punctualDriverAd.getProps().passenger).toBeFalsy();
});
it('should create a new punctual driver and passenger ad entity', async () => {
const punctualDriverPassengerAd: AdEntity = AdEntity.create(
punctualDriverPassengerCreateAdProps,
);
expect(punctualDriverPassengerAd.id.length).toBe(36);
expect(punctualDriverPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualDriverPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualDriverPassengerAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualDriverPassengerAd.getProps().driver).toBeTruthy();
expect(punctualDriverPassengerAd.getProps().passenger).toBeTruthy();
});
it('should create a new recurrent passenger ad entity', async () => {
const recurrentPassengerAd: AdEntity = AdEntity.create(
recurrentPassengerCreateAdProps,
);
expect(recurrentPassengerAd.id.length).toBe(36);
expect(recurrentPassengerAd.getProps().schedule.length).toBe(5);
expect(recurrentPassengerAd.getProps().schedule[0].day).toBe(1);
expect(recurrentPassengerAd.getProps().schedule[2].time).toBe('08:00');
expect(recurrentPassengerAd.getProps().driver).toBeFalsy();
expect(recurrentPassengerAd.getProps().passenger).toBeTruthy();
});
it('should create a new recurrent driver ad entity', async () => {
const recurrentDriverAd: AdEntity = AdEntity.create(
recurrentDriverCreateAdProps,
);
expect(recurrentDriverAd.id.length).toBe(36);
expect(recurrentDriverAd.getProps().schedule.length).toBe(5);
expect(recurrentDriverAd.getProps().schedule[1].day).toBe(2);
expect(recurrentDriverAd.getProps().schedule[0].time).toBe('08:30');
expect(recurrentDriverAd.getProps().driver).toBeTruthy();
expect(recurrentDriverAd.getProps().passenger).toBeFalsy();
});
it('should create a new recurrent driver and passenger ad entity', async () => {
const recurrentDriverPassengerAd: AdEntity = AdEntity.create(
recurrentDriverPassengerCreateAdProps,
);
expect(recurrentDriverPassengerAd.id.length).toBe(36);
expect(recurrentDriverPassengerAd.getProps().schedule.length).toBe(5);
expect(recurrentDriverPassengerAd.getProps().schedule[3].day).toBe(4);
expect(recurrentDriverPassengerAd.getProps().schedule[4].time).toBe(
'08:30',
);
expect(recurrentDriverPassengerAd.getProps().driver).toBeTruthy();
expect(recurrentDriverPassengerAd.getProps().passenger).toBeTruthy();
});
});
});
describe('Ad entity validate status', () => {
it('should validate status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.valid();
expect(punctualPassengerAd.getProps().status).toBe(Status.VALID);
});
});
describe('Ad entity invalidate status', () => {
it('should invalidate status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.invalid();
expect(punctualPassengerAd.getProps().status).toBe(Status.INVALID);
});
});
describe('Ad entity suspend status', () => {
it('should suspend status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.suspend();
expect(punctualPassengerAd.getProps().status).toBe(Status.SUSPENDED);
});
});

View File

@@ -0,0 +1,24 @@
import { Address } from '@modules/ad/core/domain/value-objects/address.value-object';
describe('Address value object', () => {
it('should create an address value object', () => {
const addressVO = new Address({
houseNumber: '5',
street: 'rue de la monnaie',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
});
expect(addressVO.houseNumber).toBe('5');
expect(addressVO.street).toBe('rue de la monnaie');
expect(addressVO.locality).toBe('Nancy');
expect(addressVO.postalCode).toBe('54000');
expect(addressVO.country).toBe('France');
expect(addressVO.coordinates.lat).toBe(48.689445);
expect(addressVO.name).toBeUndefined();
});
});

View File

@@ -0,0 +1,12 @@
import { Coordinates } from '@modules/ad/core/domain/value-objects/coordinates.value-object';
describe('Coordinates value object', () => {
it('should create a coordinates value object', () => {
const coordinatesVO = new Coordinates({
lat: 48.689445,
lon: 6.17651,
});
expect(coordinatesVO.lat).toBe(48.689445);
expect(coordinatesVO.lon).toBe(6.17651);
});
});

View File

@@ -0,0 +1,126 @@
import { Test, TestingModule } from '@nestjs/testing';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
} from '@modules/ad/ad.di-tokens';
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 { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { ConflictException } from '@mobicoop/ddd-library';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
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 punctualCreateAdRequest: CreateAdRequestDto = {
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
fromDate: '2023-12-21',
toDate: '2023-12-21',
schedule: [
{
time: '08:15',
margin: 900,
day: 4,
},
],
driver: true,
passenger: true,
seatsRequested: 1,
seatsProposed: 3,
strict: false,
frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint],
};
const mockAdRepository = {
insert: jest
.fn()
.mockImplementationOnce(() => ({}))
.mockImplementationOnce(() => {
throw new Error();
})
.mockImplementationOnce(() => {
throw new ConflictException('already exists');
}),
};
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('create-ad.service', () => {
let createAdService: CreateAdService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
{
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer,
},
CreateAdService,
],
}).compile();
createAdService = module.get<CreateAdService>(CreateAdService);
});
it('should be defined', () => {
expect(createAdService).toBeDefined();
});
describe('execution', () => {
const createAdCommand = new CreateAdCommand(punctualCreateAdRequest);
it('should create a new punctual ad', async () => {
AdEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
const result: AggregateID =
await createAdService.execute(createAdCommand);
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
});
it('should throw an error if something bad happens', async () => {
AdEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
await expect(
createAdService.execute(createAdCommand),
).rejects.toBeInstanceOf(Error);
});
it('should throw an exception if Ad already exists', async () => {
AdEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
await expect(
createAdService.execute(createAdCommand),
).rejects.toBeInstanceOf(AdAlreadyExistsException);
});
});
});

View File

@@ -0,0 +1,98 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
};
describe('find-ad-by-id.query-handler', () => {
let findAdByIdQueryHandler: FindAdByIdQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
FindAdByIdQueryHandler,
],
}).compile();
findAdByIdQueryHandler = module.get<FindAdByIdQueryHandler>(
FindAdByIdQueryHandler,
);
});
it('should be defined', () => {
expect(findAdByIdQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return an ad', async () => {
const findAdbyIdQuery = new FindAdByIdQuery(
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
);
const ad: AdEntity =
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
expect(ad.getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@@ -0,0 +1,105 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ads: AdEntity[] = [
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
];
const mockAdRepository = {
findAllByIds: jest.fn().mockImplementation(() => ads),
};
describe('Find Ads By Ids Query Handler', () => {
let findAdsByIdsQueryHandler: FindAdsByIdsQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
FindAdsByIdsQueryHandler,
],
}).compile();
findAdsByIdsQueryHandler = module.get<FindAdsByIdsQueryHandler>(
FindAdsByIdsQueryHandler,
);
});
it('should be defined', () => {
expect(findAdsByIdsQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return an ad', async () => {
const findAdsByIdsQuery = new FindAdsByIdsQuery([
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
'dd264806-13b4-4226-9b18-87adf0ad5dd2',
'dd264806-13b4-4226-9b18-87adf0ad5dd3',
]);
const ads: AdEntity[] =
await findAdsByIdsQueryHandler.execute(findAdsByIdsQuery);
expect(ads).toHaveLength(3);
expect(ads[1].getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
update: jest.fn().mockImplementation(() => ad.id),
};
describe('Invalidate Ad Service', () => {
let invalidateAdService: InvalidateAdService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
InvalidateAdService,
],
}).compile();
invalidateAdService = module.get<InvalidateAdService>(InvalidateAdService);
});
it('should be defined', () => {
expect(invalidateAdService).toBeDefined();
});
describe('execution', () => {
it('should invalidate an ad', async () => {
jest.spyOn(ad, 'invalid');
const invalidateAdCommand = new InvalidateAdCommand(ad.id);
const result: AggregateID =
await invalidateAdService.execute(invalidateAdCommand);
expect(result).toBe(ad.id);
expect(ad.invalid).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,88 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-event';
import { Test, TestingModule } from '@nestjs/testing';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Publish message when ad is created domain event handler', () => {
let publishMessageWhenAdIsCreatedDomainEventHandler: PublishMessageWhenAdIsCreatedDomainEventHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
PublishMessageWhenAdIsCreatedDomainEventHandler,
],
}).compile();
publishMessageWhenAdIsCreatedDomainEventHandler =
module.get<PublishMessageWhenAdIsCreatedDomainEventHandler>(
PublishMessageWhenAdIsCreatedDomainEventHandler,
);
});
it('should publish a message', () => {
jest.spyOn(mockMessagePublisher, 'publish');
const adCreatedDomainEvent: AdCreatedDomainEvent = {
id: 'some-domain-event-id',
aggregateId: 'some-aggregate-id',
userId: 'some-user-id',
driver: false,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-28',
toDate: '2023-06-28',
schedule: [
{
day: 3,
time: '07:15',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
lat: 48.689445,
lon: 6.1765102,
},
{
position: 1,
locality: 'Paris',
postalCode: '75000',
country: 'France',
lat: 48.8566,
lon: 2.3522,
},
],
metadata: {
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
correlationId: 'some-correlation-id',
},
};
publishMessageWhenAdIsCreatedDomainEventHandler.handle(
adCreatedDomainEvent,
);
expect(publishMessageWhenAdIsCreatedDomainEventHandler).toBeDefined();
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
AD_CREATED_ROUTING_KEY,
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","userId":"some-user-id","driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-06-28","toDate":"2023-06-28","schedule":[{"day":3,"time":"07:15","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":false,"waypoints":[{"position":0,"houseNumber":"5","street":"Avenue Foch","locality":"Nancy","postalCode":"54000","country":"France","lat":48.689445,"lon":6.1765102},{"position":1,"locality":"Paris","postalCode":"75000","country":"France","lat":48.8566,"lon":2.3522}],"metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
);
});
});

View File

@@ -0,0 +1,14 @@
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
describe('Schedule item value object', () => {
it('should create a schedule item value object', () => {
const scheduleItemVO = new ScheduleItem({
day: 0,
time: '07:00',
margin: 900,
});
expect(scheduleItemVO.day).toBe(0);
expect(scheduleItemVO.time).toBe('07:00');
expect(scheduleItemVO.margin).toBe(900);
});
});

View File

@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
update: jest.fn().mockImplementation(() => ad.id),
};
describe('Validate Ad Service', () => {
let validateAdService: ValidateAdService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
ValidateAdService,
],
}).compile();
validateAdService = module.get<ValidateAdService>(ValidateAdService);
});
it('should be defined', () => {
expect(validateAdService).toBeDefined();
});
describe('execution', () => {
it('should validate an ad', async () => {
jest.spyOn(ad, 'valid');
const validateAdCommand = new ValidateAdCommand(ad.id);
const result: AggregateID =
await validateAdService.execute(validateAdCommand);
expect(result).toBe(ad.id);
expect(ad.valid).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,22 @@
import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
describe('Waypoint value object', () => {
it('should create a waypoint value object', () => {
const waypointVO = new Waypoint({
position: 0,
address: {
houseNumber: '5',
street: 'rue de la monnaie',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
});
expect(waypointVO.position).toBe(0);
expect(waypointVO.address.country).toBe('France');
});
});

View File

@@ -0,0 +1,52 @@
import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
const mockOutputDatetimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('Ad repository', () => {
let prismaService: PrismaService;
let adMapper: AdMapper;
let eventEmitter: EventEmitter2;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [EventEmitterModule.forRoot()],
providers: [
PrismaService,
AdMapper,
{
provide: OUTPUT_DATETIME_TRANSFORMER,
useValue: mockOutputDatetimeTransformer,
},
],
}).compile();
prismaService = module.get<PrismaService>(PrismaService);
adMapper = module.get<AdMapper>(AdMapper);
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
});
it('should be defined', () => {
expect(
new AdRepository(
prismaService,
adMapper,
eventEmitter,
mockMessagePublisher,
),
).toBeDefined();
});
});

View File

@@ -0,0 +1,248 @@
import { TIMEZONE_FINDER, TIME_CONVERTER } from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
const mockTimeConverter: TimeConverterPort = {
localStringTimeToUtcStringTime: jest
.fn()
.mockImplementationOnce(() => '00:15'),
utcStringTimeToLocalStringTime: jest.fn(),
localStringDateTimeToUtcDate: jest
.fn()
.mockImplementationOnce(() => new Date('2023-07-30T06:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-20T08:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')),
utcStringDateTimeToLocalIsoString: jest.fn(),
utcUnixEpochDayFromTime: jest
.fn()
.mockImplementationOnce(() => 4)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 5),
localUnixEpochDayFromTime: jest.fn(),
};
describe('Input Datetime Transformer', () => {
let inputDatetimeTransformer: InputDateTimeTransformer;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
InputDateTimeTransformer,
],
}).compile();
inputDatetimeTransformer = module.get<InputDateTimeTransformer>(
InputDateTimeTransformer,
);
});
it('should be defined', () => {
expect(inputDatetimeTransformer).toBeDefined();
});
describe('fromDate', () => {
it('should return fromDate as is if frequency is recurrent', () => {
const transformedFromDate: string = inputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedFromDate).toBe('2023-07-30');
});
it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => {
const transformedFromDate: string = inputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedFromDate).toBe('2023-07-30');
});
});
describe('toDate', () => {
it('should return toDate as is if frequency is recurrent', () => {
const transformedToDate: string = inputDatetimeTransformer.toDate(
'2024-07-29',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedToDate).toBe('2024-07-29');
});
it('should return transformed fromDate if frequency is punctual', () => {
const transformedToDate: string = inputDatetimeTransformer.toDate(
'2024-07-30',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedToDate).toBe('2023-07-20');
});
});
describe('day', () => {
it('should not change day if frequency is recurrent and converted UTC time is on the same day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted UTC time is on the previous day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted UTC time is on the previous day and given day is sunday', () => {
const day: number = inputDatetimeTransformer.day(
0,
{
date: '2023-07-23',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(6);
});
it('should change day if frequency is recurrent and converted UTC time is on the next day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '23:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(2);
});
it('should change day if frequency is recurrent and converted UTC time is on the next day and given day is saturday(6)', () => {
const day: number = inputDatetimeTransformer.day(
6,
{
date: '2023-07-29',
time: '23:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should return utc fromDate day if frequency is punctual', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-20',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(day).toBe(3);
});
});
describe('time', () => {
it('should transform given time to utc time if frequency is recurrent', () => {
const time: string = inputDatetimeTransformer.time(
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(time).toBe('00:15');
});
it('should return given time to utc time if frequency is punctual', () => {
const time: string = inputDatetimeTransformer.time(
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(time).toBe('23:15');
});
});
});

View File

@@ -0,0 +1,248 @@
import { TIMEZONE_FINDER, TIME_CONVERTER } from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
const mockTimeConverter: TimeConverterPort = {
localStringTimeToUtcStringTime: jest.fn(),
utcStringTimeToLocalStringTime: jest
.fn()
.mockImplementationOnce(() => '00:15'),
localStringDateTimeToUtcDate: jest.fn(),
utcStringDateTimeToLocalIsoString: jest
.fn()
.mockImplementationOnce(() => '2023-07-30T08:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-20T10:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-19T23:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-20T00:15:00.000+02:00'),
utcUnixEpochDayFromTime: jest.fn(),
localUnixEpochDayFromTime: jest
.fn()
.mockImplementationOnce(() => 4)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 3),
};
describe('Output Datetime Transformer', () => {
let outputDatetimeTransformer: OutputDateTimeTransformer;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
OutputDateTimeTransformer,
],
}).compile();
outputDatetimeTransformer = module.get<OutputDateTimeTransformer>(
OutputDateTimeTransformer,
);
});
it('should be defined', () => {
expect(outputDatetimeTransformer).toBeDefined();
});
describe('fromDate', () => {
it('should return fromDate as is if frequency is recurrent', () => {
const transformedFromDate: string = outputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedFromDate).toBe('2023-07-30');
});
it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => {
const transformedFromDate: string = outputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedFromDate).toBe('2023-07-30');
});
});
describe('toDate', () => {
it('should return toDate as is if frequency is recurrent', () => {
const transformedToDate: string = outputDatetimeTransformer.toDate(
'2024-07-29',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedToDate).toBe('2024-07-29');
});
it('should return transformed fromDate if frequency is punctual', () => {
const transformedToDate: string = outputDatetimeTransformer.toDate(
'2024-07-30',
{
date: '2023-07-20',
time: '08:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedToDate).toBe('2023-07-20');
});
});
describe('day', () => {
it('should not change day if frequency is recurrent and converted local time is on the same day', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted local time is on the next day', () => {
const day: number = outputDatetimeTransformer.day(
0,
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted local time is on the next day and given day is saturday', () => {
const day: number = outputDatetimeTransformer.day(
6,
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted local time is on the previous day', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-25',
time: '00:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted local time is on the previous day and given day is sunday(0)', () => {
const day: number = outputDatetimeTransformer.day(
0,
{
date: '2023-07-30',
time: '00:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(6);
});
it('should return local fromDate day if frequency is punctual', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-20',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(day).toBe(3);
});
});
describe('time', () => {
it('should transform utc time to local time if frequency is recurrent', () => {
const time: string = outputDatetimeTransformer.time(
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(time).toBe('00:15');
});
it('should return local time if frequency is punctual', () => {
const time: string = outputDatetimeTransformer.time(
{
date: '2023-07-19',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(time).toBe('00:15');
});
});
});

View File

@@ -0,0 +1,311 @@
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
describe('Time Converter', () => {
it('should be defined', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(timeConverter).toBeDefined();
});
describe('localStringTimeToUtcStringTime', () => {
it('should convert a paris time to utc time', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisTime = '08:00';
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBe('07:00');
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const fooBarTime = '08:00';
expect(() => {
timeConverter.localStringTimeToUtcStringTime(fooBarTime, 'Foo/Bar');
}).toThrow();
});
});
describe('utcStringTimeToLocalStringTime', () => {
it('should convert a utc time to a paris time', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcTime = '07:00';
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
'Europe/Paris',
);
expect(parisTime).toBe('08:00');
});
it('should throw an error if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcTime = '27:00';
expect(() => {
timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Europe/Paris');
}).toThrow();
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcTime = '07:00';
expect(() => {
timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Foo/Bar');
}).toThrow();
});
});
describe('localStringDateTimeToUtcDate', () => {
it('should convert a summer paris date and time to a utc date with dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
true,
);
expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z');
});
it('should convert a winter paris date and time to a utc date with dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-02-02';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
true,
);
expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z');
});
it('should convert a summer paris date and time to a utc date without dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z');
});
it('should convert a tonga date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const tongaDate = '2023-02-02';
const tongaTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
tongaDate,
tongaTime,
'Pacific/Tongatapu',
);
expect(utcDate.toISOString()).toBe('2023-02-01T23:00:00.000Z');
});
it('should convert a papeete date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const papeeteDate = '2023-02-02';
const papeeteTime = '15:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
papeeteDate,
papeeteTime,
'Pacific/Tahiti',
);
expect(utcDate.toISOString()).toBe('2023-02-03T01:00:00.000Z');
});
it('should throw an error if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-32';
const parisTime = '08:00';
expect(() => {
timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
}).toThrow();
});
it('should throw an error if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '28:00';
expect(() => {
timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
}).toThrow();
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
expect(() => {
timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Foo/Bar',
);
}).toThrow();
});
});
describe('utcStringDateTimeToLocalIsoString', () => {
it('should convert a utc string date and time to a summer paris date isostring with dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
true,
);
expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00');
});
it('should convert a utc string date and time to a winter paris date isostring with dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-02';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
true,
);
expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00');
});
it('should convert a utc string date and time to a summer paris date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00');
});
it('should convert a utc date to a tonga date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-01';
const utcTime = '23:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Pacific/Tongatapu',
);
expect(localIsoString).toBe('2023-02-02T12:00:00.000+13:00');
});
it('should convert a utc date to a papeete date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-03';
const utcTime = '01:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Pacific/Tahiti',
);
expect(localIsoString).toBe('2023-02-02T15:00:00.000-10:00');
});
it('should throw an error if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-32';
const utcTime = '07:00';
expect(() => {
timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
}).toThrow();
});
it('should throw an error if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '27:00';
expect(() => {
timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
}).toThrow();
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '07:00';
expect(() => {
timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Foo/Bar',
);
}).toThrow();
});
});
describe('utcUnixEpochDayFromTime', () => {
it('should get the utc day of paris at 12:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('12:00', 'Europe/Paris'),
).toBe(4);
});
it('should get the utc day of paris at 00:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('00:00', 'Europe/Paris'),
).toBe(3);
});
it('should get the utc day of papeete at 16:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('16:00', 'Pacific/Tahiti'),
).toBe(5);
});
it('should throw an error if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(() => {
timeConverter.utcUnixEpochDayFromTime('28:00', 'Europe/Paris');
}).toThrow();
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(() => {
timeConverter.utcUnixEpochDayFromTime('12:00', 'Foo/Bar');
}).toThrow();
});
});
describe('localUnixEpochDayFromTime', () => {
it('should get the day of paris at 12:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('12:00', 'Europe/Paris'),
).toBe(4);
});
it('should get the day of paris at 23:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('23:00', 'Europe/Paris'),
).toBe(5);
});
it('should get the day of papeete at 05:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('05:00', 'Pacific/Tahiti'),
).toBe(3);
});
it('should throw an error if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(() => {
timeConverter.localUnixEpochDayFromTime('28:00', 'Europe/Paris');
}).toThrow();
});
it('should throw an error if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(() => {
timeConverter.localUnixEpochDayFromTime('12:00', 'Foo/Bar');
}).toThrow();
});
});
});

View File

@@ -0,0 +1,14 @@
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
describe('Timezone Finder', () => {
it('should be defined', () => {
const timezoneFinder: TimezoneFinder = new TimezoneFinder();
expect(timezoneFinder).toBeDefined();
});
it('should get timezone for Nancy(France) as Europe/Paris', () => {
const timezoneFinder: TimezoneFinder = new TimezoneFinder();
const timezones = timezoneFinder.timezones(6.179373, 48.687913);
expect(timezones.length).toBe(1);
expect(timezones[0]).toBe('Europe/Paris');
});
});

View File

@@ -0,0 +1,122 @@
import { IdResponse } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
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 { CommandBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';
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 punctualCreateAdRequest: CreateAdRequestDto = {
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
fromDate: '2023-12-21',
toDate: '2023-12-21',
schedule: [
{
time: '08:15',
day: 4,
margin: 600,
},
],
driver: false,
passenger: true,
seatsRequested: 1,
seatsProposed: 3,
strict: false,
frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint],
};
const mockCommandBus = {
execute: jest
.fn()
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
.mockImplementationOnce(() => {
throw new AdAlreadyExistsException();
})
.mockImplementationOnce(() => {
throw new Error();
}),
};
describe('Create Ad Grpc Controller', () => {
let createAdGrpcController: CreateAdGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
CreateAdGrpcController,
],
}).compile();
createAdGrpcController = module.get<CreateAdGrpcController>(
CreateAdGrpcController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(createAdGrpcController).toBeDefined();
});
it('should create a new ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
const result: IdResponse = await createAdGrpcController.create(
punctualCreateAdRequest,
);
expect(result).toBeInstanceOf(IdResponse);
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should throw a dedicated RpcException if ad already exists', async () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await createAdGrpcController.create(punctualCreateAdRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
}
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should throw a generic RpcException', async () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await createAdGrpcController.create(punctualCreateAdRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
}
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,140 @@
import { NotFoundException } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AdMapper } from '@modules/ad/ad.mapper';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { FindAdByIdGrpcController } from '@modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
const mockQueryBus = {
execute: jest
.fn()
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
.mockImplementationOnce(() => {
throw new NotFoundException();
})
.mockImplementationOnce(() => {
throw new Error();
}),
};
const mockAdMapper = {
toResponse: jest.fn().mockImplementationOnce(() => ({
userId: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-27',
toDate: '2023-06-27',
schedule: {
tue: '07:15',
},
marginDurations: {
mon: 900,
tue: 900,
wed: 900,
thu: 900,
fri: 900,
sat: 900,
sun: 900,
},
seatsProposed: 3,
seatsRequested: 1,
waypoints: [
{
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
},
{
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
},
],
})),
};
describe('Find Ad By Id Grpc Controller', () => {
let findAdbyIdGrpcController: FindAdByIdGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: AdMapper,
useValue: mockAdMapper,
},
FindAdByIdGrpcController,
],
}).compile();
findAdbyIdGrpcController = module.get<FindAdByIdGrpcController>(
FindAdByIdGrpcController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(findAdbyIdGrpcController).toBeDefined();
});
it('should return an ad', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockAdMapper, 'toResponse');
const response = await findAdbyIdGrpcController.findOnebyId({
id: '6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
});
expect(response.userId).toBe('8cc90d1a-4a59-4289-a7d8-078f9db7857f');
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(1);
});
it('should throw a dedicated RpcException if ad is not found', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockAdMapper, 'toResponse');
expect.assertions(4);
try {
await findAdbyIdGrpcController.findOnebyId({
id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8',
});
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
}
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0);
});
it('should throw a generic RpcException', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockAdMapper, 'toResponse');
expect.assertions(4);
try {
await findAdbyIdGrpcController.findOnebyId({
id: '53c8e7ec-ef68-42bc-ba4c-5ef3effa60a6',
});
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
}
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0);
});
});

View File

@@ -0,0 +1,132 @@
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AdMapper } from '@modules/ad/ad.mapper';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { FindAdsByIdsGrpcController } from '@modules/ad/interface/grpc-controllers/find-ads-by-ids.grpc.controller';
import { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
const mockQueryBus = {
execute: jest
.fn()
.mockImplementationOnce(() => [
'200d61a8-d878-4378-a609-c19ea71633d2',
'200d61a8-d878-4378-a609-c19ea71633d3',
'200d61a8-d878-4378-a609-c19ea71633d4',
])
.mockImplementationOnce(() => {
throw new Error();
}),
};
const mockAdMapper = {
toResponse: jest.fn().mockImplementationOnce(() => ({
userId: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-27',
toDate: '2023-06-27',
schedule: {
tue: '07:15',
},
marginDurations: {
mon: 900,
tue: 900,
wed: 900,
thu: 900,
fri: 900,
sat: 900,
sun: 900,
},
seatsProposed: 3,
seatsRequested: 1,
waypoints: [
{
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
},
{
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
},
],
})),
};
describe('Find Ads By Ids Grpc Controller', () => {
let findAdsByIdsGrpcController: FindAdsByIdsGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: AdMapper,
useValue: mockAdMapper,
},
FindAdsByIdsGrpcController,
],
}).compile();
findAdsByIdsGrpcController = module.get<FindAdsByIdsGrpcController>(
FindAdsByIdsGrpcController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(findAdsByIdsGrpcController).toBeDefined();
});
it('should return ads', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockAdMapper, 'toResponse');
const response = await findAdsByIdsGrpcController.findAllByIds({
ids: [
'200d61a8-d878-4378-a609-c19ea71633d2',
'200d61a8-d878-4378-a609-c19ea71633d3',
'200d61a8-d878-4378-a609-c19ea71633d4',
],
});
expect(response.ads).toHaveLength(3);
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(3);
});
it('should throw a generic RpcException', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockAdMapper, 'toResponse');
expect.assertions(4);
try {
await findAdsByIdsGrpcController.findAllByIds({
ids: [
'200d61a8-d878-4378-a609-c19ea71633d2',
'200d61a8-d878-4378-a609-c19ea71633d3',
'200d61a8-d878-4378-a609-c19ea71633d4',
],
});
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
}
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0);
});
});

View File

@@ -0,0 +1,47 @@
import { HasRole } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-role.decorator';
import { Validator } from 'class-validator';
describe('has role decorator', () => {
class MyClass {
@HasRole('passenger')
driver: boolean;
passenger: boolean;
}
it('should return a property decorator has a function', () => {
const hasRole = HasRole('property');
expect(typeof hasRole).toBe('function');
});
it('should validate an instance with driver only set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.passenger = false;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with passenger only set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = false;
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with driver and passenger set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate an instance without driver and passenger set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = false;
myClassInstance.passenger = false;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@@ -0,0 +1,59 @@
import { HasSeats } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-seats.decorator';
import { Validator } from 'class-validator';
describe('has seats decorator', () => {
class MyClass {
@HasSeats('seatsProposed')
driver: boolean;
@HasSeats('seatsRequested')
passenger: boolean;
seatsProposed?: number;
seatsRequested?: number;
}
it('should return a property decorator has a function', () => {
const hasSeats = HasSeats('property');
expect(typeof hasSeats).toBe('function');
});
it('should validate an instance with seats proposed as driver', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 3;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with seats requested as passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.passenger = true;
myClassInstance.seatsRequested = 1;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with seats proposed as driver and passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 3;
myClassInstance.passenger = true;
myClassInstance.seatsRequested = 1;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate an instance without seats proposed as driver', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 0;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
it('should not validate an instance without seats requested as passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@@ -0,0 +1,62 @@
import { HasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator';
import { WaypointDto } from '@modules/ad/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,
lat: 48.8566,
lon: 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,
lat: 48.8566,
lon: 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);
});
});

View File

@@ -0,0 +1,54 @@
import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
describe('addresses position validator', () => {
const waypoint1: WaypointDto = {
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
};
const waypoint2: WaypointDto = {
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const waypoint3: WaypointDto = {
position: 2,
lon: 49.2628,
lat: 4.0347,
locality: 'Reims',
postalCode: '51454',
country: 'France',
};
it('should not validate if multiple positions have same value', () => {
expect(
hasValidPositionIndexes([waypoint1, waypoint1, waypoint2]),
).toBeFalsy();
});
it('should not validate if positions are not consecutives', () => {
expect(hasValidPositionIndexes([waypoint1, waypoint3])).toBeFalsy();
});
it('should not validate if waypoints are empty', () => {
expect(hasValidPositionIndexes([])).toBeFalsy();
});
it('should validate if all positions are ordered', () => {
expect(
hasValidPositionIndexes([waypoint1, waypoint2, waypoint3]),
).toBeTruthy();
waypoint1.position = 1;
waypoint2.position = 2;
waypoint3.position = 3;
expect(
hasValidPositionIndexes([waypoint1, waypoint2, waypoint3]),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,45 @@
import { IsAfterOrEqual } from '@modules/ad/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);
});
});

View File

@@ -0,0 +1,46 @@
import { MatcherAdCreatedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-created.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const matcherAdCreatedMessage =
'{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driverDuration":"3512","driverDistance":"65845","fwdAzimuth":"90","backAzimuth":"270"}';
const mockCommandBus = {
execute: jest.fn(),
};
describe('Matcher Ad Created Message Handler', () => {
let matcherAdCreatedMessageHandler: MatcherAdCreatedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
MatcherAdCreatedMessageHandler,
],
}).compile();
matcherAdCreatedMessageHandler = module.get<MatcherAdCreatedMessageHandler>(
MatcherAdCreatedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(matcherAdCreatedMessageHandler).toBeDefined();
});
it('should validate an ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
await matcherAdCreatedMessageHandler.matcherAdCreated(
matcherAdCreatedMessage,
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,47 @@
import { MatcherAdCreationFailedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const matcherAdCreationFailedMessage =
'{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}';
const mockCommandBus = {
execute: jest.fn(),
};
describe('Matcher Ad Creation Failed Message Handler', () => {
let matcherAdCreationFailedMessageHandler: MatcherAdCreationFailedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
MatcherAdCreationFailedMessageHandler,
],
}).compile();
matcherAdCreationFailedMessageHandler =
module.get<MatcherAdCreationFailedMessageHandler>(
MatcherAdCreationFailedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(matcherAdCreationFailedMessageHandler).toBeDefined();
});
it('should invalidate an ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
await matcherAdCreationFailedMessageHandler.matcherAdCreationFailed(
matcherAdCreationFailedMessage,
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});