import { DefaultParamsProviderPort } from '@modules/geography/core/application/ports/default-params-provider.port'; import { GeodesicPort } from '@modules/geography/core/application/ports/geodesic.port'; import { GeorouterUnavailableException, RouteNotFoundException, } from '@modules/geography/core/domain/route.errors'; import { Route, Step } from '@modules/geography/core/domain/route.types'; import { GEODESIC, PARAMS_PROVIDER, } from '@modules/geography/geography.di-tokens'; import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosError } from 'axios'; import { of, throwError } from 'rxjs'; const mockHttpService = { get: jest .fn() .mockImplementationOnce(() => { return throwError( () => new AxiosError('Axios error', AxiosError.ERR_BAD_REQUEST), ); }) .mockImplementationOnce(() => { return throwError(() => 'Router unavailable'); }) .mockImplementationOnce(() => { return of({ status: 200, data: { paths: [ { distance: 50000, time: 1800000, snapped_waypoints: { coordinates: [ [0, 0], [10, 10], ], }, }, ], }, }); }) .mockImplementationOnce(() => { return of({ status: 200, data: { paths: [ { distance: 50000, time: 1800000, points: { coordinates: [ [0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9], [10, 10], ], }, snapped_waypoints: { coordinates: [ [0, 0], [10, 10], ], }, }, ], }, }); }) .mockImplementationOnce(() => { return of({ status: 200, data: { paths: [ { distance: 50000, time: 1800000, points: { coordinates: [ [0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9], [10, 10], ], }, details: { time: [ [0, 1, 180000], [1, 2, 180000], [2, 3, 180000], [3, 4, 180000], [4, 5, 180000], [5, 6, 180000], [6, 7, 180000], [7, 9, 360000], [9, 10, 180000], ], }, snapped_waypoints: { coordinates: [ [0, 0], [10, 10], ], }, }, ], }, }); }) .mockImplementationOnce(() => { return of({ status: 200, data: { paths: [ { distance: 50000, time: 1800000, points: { coordinates: [ [0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [7, 7], [8, 8], [9, 9], [10, 10], ], }, snapped_waypoints: { coordinates: [ [0, 0], [5, 5], [10, 10], ], }, details: { time: [ [0, 1, 180000], [1, 2, 180000], [2, 3, 180000], [3, 4, 180000], [4, 7, 540000], [7, 9, 360000], [9, 10, 180000], ], }, }, ], }, }); }) .mockImplementationOnce(() => { return of({ status: 200, data: { paths: [ { distance: 50000, time: 1800000, points: { coordinates: [ [0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9], [10, 10], ], }, snapped_waypoints: { coordinates: [ [0, 0], [5, 5], [10, 10], ], }, details: { time: [ [0, 1, 180000], [1, 2, 180000], [2, 3, 180000], [3, 4, 180000], [4, 7, 540000], [7, 9, 360000], [9, 10, 180000], ], }, instructions: [ { distance: 25000, sign: 0, interval: [0, 5], text: 'Some instructions', time: 900000, }, { distance: 0, sign: 5, interval: [5, 5], text: 'Waypoint 1', time: 0, }, { distance: 25000, sign: 2, interval: [5, 10], text: 'Some instructions', time: 900000, }, { distance: 0.0, sign: 4, interval: [10, 10], text: 'Arrive at destination', time: 0, }, ], }, ], }, }); }), }; const mockGeodesic: GeodesicPort = { inverse: jest.fn().mockImplementation(() => ({ azimuth: 45, distance: 50000, })), }; const mockDefaultParamsProvider: DefaultParamsProviderPort = { getParams: jest.fn().mockImplementation(() => ({ GEOROUTER_URL: 'http://localhost:8989', })), }; describe('Graphhopper Georouter', () => { let graphhopperGeorouter: GraphhopperGeorouter; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [], providers: [ GraphhopperGeorouter, { provide: HttpService, useValue: mockHttpService, }, { provide: PARAMS_PROVIDER, useValue: mockDefaultParamsProvider, }, { provide: GEODESIC, useValue: mockGeodesic, }, ], }).compile(); graphhopperGeorouter = module.get(GraphhopperGeorouter); }); it('should be defined', () => { expect(graphhopperGeorouter).toBeDefined(); }); it('should fail if route is not found', async () => { await expect( graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 1, lat: 1, }, ], { detailedDistance: false, detailedDuration: false, points: false, }, ), ).rejects.toBeInstanceOf(RouteNotFoundException); }); it('should fail if georouter is unavailable', async () => { await expect( graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 1, lat: 1, }, ], { detailedDistance: false, detailedDuration: false, points: false, }, ), ).rejects.toBeInstanceOf(GeorouterUnavailableException); }); it('should create a basic route', async () => { const route: Route = await graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 10, lat: 10, }, ], { detailedDistance: false, detailedDuration: false, points: false, }, ); expect(route.distance).toBe(50000); }); it('should create a route with points', async () => { const route: Route = await graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 10, lat: 10, }, ], { detailedDistance: false, detailedDuration: false, points: true, }, ); expect(route.distance).toBe(50000); expect(route.duration).toBe(1800); expect(route.fwdAzimuth).toBe(45); expect(route.backAzimuth).toBe(225); expect(route.points).toHaveLength(11); }); it('should create a route with points and time', async () => { const route: Route = await graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 10, lat: 10, }, ], { detailedDistance: false, detailedDuration: true, points: true, }, ); expect(route.steps).toHaveLength(2); expect((route.steps as Step[])[1].duration).toBe(1800); expect((route.steps as Step[])[1].distance).toBeUndefined(); }); it('should create one route with points and missed waypoints extrapolations', async () => { const route: Route = await graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 5, lat: 5, }, { position: 2, lon: 10, lat: 10, }, ], { detailedDistance: false, detailedDuration: true, points: true, }, ); expect(route.steps).toHaveLength(3); expect(route.distance).toBe(50000); expect(route.duration).toBe(1800); expect(route.fwdAzimuth).toBe(45); expect(route.backAzimuth).toBe(225); expect(route.points.length).toBe(9); }); it('should create a route with points, time and distance', async () => { const route: Route = await graphhopperGeorouter.route( [ { position: 0, lon: 0, lat: 0, }, { position: 1, lon: 10, lat: 10, }, ], { detailedDistance: true, detailedDuration: true, points: true, }, ); expect(route.steps).toHaveLength(3); expect((route.steps as Step[])[1].duration).toBe(990); expect((route.steps as Step[])[1].distance).toBe(25000); }); });