diff --git a/devScripts/geotextElasticsearchData.json b/devScripts/geotextElasticsearchData.json index 0581ea2c..21652cbd 100644 --- a/devScripts/geotextElasticsearchData.json +++ b/devScripts/geotextElasticsearchData.json @@ -58,8 +58,8 @@ "geometry_hash": "f14be42df6ec5cf2290d764a89e3009b", "region": ["USA"], "sub_region": ["New York"], - "name": "JFK", - "text": ["JFK Airport"], + "name": "JFK International Airport", + "text": ["JFK International Airport", "John F Kennedy International Airport"], "translated_text": ["Aeropuerto JFK"], "text_language": "en" } diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 2683ca6f..02cf445e 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -109,4 +109,5 @@ export interface GenericGeocodingResponse extends Fe })[]; } -export type GenericGeocodingFeatureResponse = Feature & Pick, 'geocoding'>; +export type GenericGeocodingFeatureResponse = GenericGeocodingResponse['features'][number] & + Pick, 'geocoding'>; diff --git a/src/common/middlewares/feedbackApi.middleware.ts b/src/common/middlewares/feedbackApi.middleware.ts index 828430e7..4ed9e2cd 100644 --- a/src/common/middlewares/feedbackApi.middleware.ts +++ b/src/common/middlewares/feedbackApi.middleware.ts @@ -42,7 +42,7 @@ export class FeedbackApiMiddlewareManager { .then(() => { logger.info({ msg: `response ${reqId?.toString() ?? ''} saved to redis` }); }) - .catch((error: Error) => logger.error({ message: 'Error setting key:', error })); + .catch((error: Error) => logger.error({ msg: 'Error setting key:', error })); return originalJson.call(this, body); }; diff --git a/src/common/redis/index.ts b/src/common/redis/index.ts index dba9d379..5047dd68 100644 --- a/src/common/redis/index.ts +++ b/src/common/redis/index.ts @@ -38,6 +38,6 @@ export const redisClientFactory: FactoryFunction = (con .on('ready', (...args) => logger.debug({ msg: 'redis client is ready', ...args })); return redisClient; } catch (error) { - logger.error({ message: 'Connection to Redis was unsuccessful', error }); + logger.error({ msg: 'Connection to Redis was unsuccessful', error }); } }; diff --git a/src/common/s3/s3Repository.ts b/src/common/s3/s3Repository.ts index 6502badb..0fc3b717 100644 --- a/src/common/s3/s3Repository.ts +++ b/src/common/s3/s3Repository.ts @@ -49,7 +49,7 @@ const createS3Repository = (s3Client: S3Client, config: IConfig, logger: Logger) return filePath; } catch (error) { - logger.error({ message: `Error while downloading ${key}'s data from S3.`, error }); + logger.error({ msg: `Error while downloading ${key}'s data from S3.`, error }); throw error; } }, diff --git a/src/common/utils.ts b/src/common/utils.ts index 20abf606..bf6aee99 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -153,6 +153,8 @@ export type RemoveUnderscore = { [K in keyof T as K extends `_${infer Rest}` ? Rest : K]: T[K]; }; +export type RequireKeys = Required> & Omit; + export const validateWGS84Coordinate = (coordinate: { lon: number; lat: number }): boolean => { // eslint-disable-next-line @typescript-eslint/no-magic-numbers const [min, max] = [0, 180]; @@ -216,7 +218,7 @@ export const healthCheckFactory: FactoryFunction = (container: DependencyC return; }) .catch((error: Error) => { - logger.error({ message: `Healthcheck failed for ${key}.`, error }); + logger.error({ msg: `Healthcheck failed for ${key}.`, error }); }); } diff --git a/src/containerConfig.ts b/src/containerConfig.ts index 09f5e00b..8c3109e9 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -27,7 +27,7 @@ import { RedisClient, redisClientFactory } from './common/redis'; import { s3ClientFactory } from './common/s3'; import { S3_REPOSITORY_SYMBOL, s3RepositoryFactory } from './common/s3/s3Repository'; import { healthCheckFactory } from './common/utils'; -import { MGRS_ROUTER_SYMBOL, mgrsRouterFactory } from './mgrs/routers/mgrsRouter'; +import { MGRS_ROUTER_SYMBOL, mgrsRouterFactory } from './mgrs/routes/mgrsRouter'; export interface RegisterOptions { override?: InjectionObject[]; @@ -79,7 +79,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise const response = await Promise.all([elasticClients.control.ping(), elasticClients.geotext.ping()]); response.forEach((res) => { if (!res) { - logger.error({ message: 'Failed to connect to Elasticsearch', res }); + logger.error({ msg: 'Failed to connect to Elasticsearch', res }); } }); cleanupRegistry.register({ @@ -90,7 +90,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise id: SERVICES.ELASTIC_CLIENTS, }); } catch (error) { - logger.error({ message: 'Failed to connect to Elasticsearch', error }); + logger.error({ msg: 'Failed to connect to Elasticsearch', error }); } }, }, @@ -103,7 +103,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise await s3Client.send(new ListBucketsCommand({})); logger.info('Connected to S3'); } catch (error) { - logger.error({ message: 'Failed to connect to S3', error }); + logger.error({ msg: 'Failed to connect to S3', error }); } cleanupRegistry.register({ func: async () => { @@ -160,7 +160,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise cleanupRegistry.register({ func: redis.disconnect.bind(redis), id: SERVICES.REDIS }); await redis.connect(); } catch (error) { - logger.error({ message: 'Connection to redis failed', error }); + logger.error({ msg: 'Connection to redis failed', error }); } }, }, diff --git a/src/control/item/controllers/itemController.ts b/src/control/item/controllers/itemController.ts index 298b95bb..38e52d63 100644 --- a/src/control/item/controllers/itemController.ts +++ b/src/control/item/controllers/itemController.ts @@ -41,7 +41,7 @@ export class ItemController { }); return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.warn('itemController.getItems Error:', error); + this.logger.error({ msg: 'itemController.getItems error', error }); next(error); } }; diff --git a/src/control/route/controllers/routeController.ts b/src/control/route/controllers/routeController.ts index 36be0db7..45adca5d 100644 --- a/src/control/route/controllers/routeController.ts +++ b/src/control/route/controllers/routeController.ts @@ -40,7 +40,7 @@ export class RouteController { }); return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.warn('routeController.getRoutes Error:', error); + this.logger.error({ msg: 'routeController.getRoutes error', error }); next(error); } }; diff --git a/src/control/route/models/route.ts b/src/control/route/models/route.ts index 607fcd12..91c585f1 100644 --- a/src/control/route/models/route.ts +++ b/src/control/route/models/route.ts @@ -5,6 +5,7 @@ export interface Route extends Feature { properties: GeoJsonProperties & { TYPE: 'ROUTE'; OBJECT_COMMAND_NAME: string; + TIED_TO?: string; ENTITY_HEB: string; LAYER_NAME: string; }; diff --git a/src/control/tile/DAL/queries.ts b/src/control/tile/DAL/queries.ts index 35dadf7a..6e065202 100644 --- a/src/control/tile/DAL/queries.ts +++ b/src/control/tile/DAL/queries.ts @@ -2,7 +2,7 @@ import { estypes } from '@elastic/elasticsearch'; import { BBox } from 'geojson'; import { CommonRequestParameters } from '../../../common/interfaces'; import { ELASTIC_KEYWORDS } from '../../constants'; -import { ConvertSnakeToCamelCase, geoContextQuery, parseGeo } from '../../../common/utils'; +import { ConvertSnakeToCamelCase, geoContextQuery, parseGeo, RequireKeys } from '../../../common/utils'; export interface TileQueryParams extends ConvertSnakeToCamelCase { tile?: string; @@ -46,7 +46,7 @@ export const queryForSubTiles = ({ geoContextMode, subTile, disableFuzziness, -}: Omit, 'mgrs'>): estypes.SearchRequest => ({ +}: RequireKeys, 'tile' | 'subTile'>): estypes.SearchRequest => ({ query: { bool: { must: [ diff --git a/src/control/tile/controllers/tileController.ts b/src/control/tile/controllers/tileController.ts index db3df58e..d4f12873 100644 --- a/src/control/tile/controllers/tileController.ts +++ b/src/control/tile/controllers/tileController.ts @@ -50,7 +50,7 @@ export class TileController { }); return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.warn('tileController.getTiles Error:', error); + this.logger.error({ msg: 'tileController.getTiles', error }); next(error); } }; diff --git a/src/control/tile/models/tileManager.ts b/src/control/tile/models/tileManager.ts index 0825d120..26ed9734 100644 --- a/src/control/tile/models/tileManager.ts +++ b/src/control/tile/models/tileManager.ts @@ -35,11 +35,6 @@ export class TileManager { let bbox: BBox = [0, 0, 0, 0]; try { bbox = mgrs.inverse(tileQueryParams.mgrs); - bbox.forEach((coord) => { - if (isNaN(coord)) { - throw new Error('Invalid MGRS'); - } - }); } catch (error) { throw new BadRequestError(`Invalid MGRS: ${tileQueryParams.mgrs}`); } diff --git a/src/latLon/DAL/latLonDAL.ts b/src/latLon/DAL/latLonDAL.ts index 6505134c..5b17fc08 100644 --- a/src/latLon/DAL/latLonDAL.ts +++ b/src/latLon/DAL/latLonDAL.ts @@ -37,7 +37,7 @@ export class LatLonDAL { this.dataLoadError = false; this.init().catch((error: Error) => { - this.logger.error({ message: 'Failed to initialize lat-lon data', error }); + this.logger.error({ msg: 'Failed to initialize lat-lon data', error }); this.dataLoadError = true; }); } @@ -74,7 +74,7 @@ export class LatLonDAL { this.logger.debug('latLonData initialized'); } catch (error) { - this.logger.error({ message: `Failed to initialize latLon data.`, error }); + this.logger.error({ msg: `Failed to initialize latLon data.`, error }); this.dataLoadError = true; } finally { this.onGoingUpdate = false; @@ -111,7 +111,7 @@ export class LatLonDAL { try { await fs.promises.unlink(latLonDataPath); } catch (error) { - this.logger.error({ message: `Failed to delete latLonData file ${latLonDataPath}.`, error }); + this.logger.error({ msg: `Failed to delete latLonData file ${latLonDataPath}.`, error }); } this.logger.info('loadLatLonData: update completed'); } @@ -147,7 +147,7 @@ export const cronLoadTileLatLonDataFactory: FactoryFunction if (!latLonDAL.getOnGoingUpdate()) { logger.info('cronLoadTileLatLonData: starting update'); latLonDAL.init().catch((error: Error) => { - logger.error({ message: 'cronLoadTileLatLonData: update failed', error }); + logger.error({ msg: 'cronLoadTileLatLonData: update failed', error }); }); } else { logger.info('cronLoadTileLatLonData: update is already in progress'); diff --git a/src/latLon/controllers/latLonController.ts b/src/latLon/controllers/latLonController.ts index 555efe04..97bb8825 100644 --- a/src/latLon/controllers/latLonController.ts +++ b/src/latLon/controllers/latLonController.ts @@ -39,7 +39,7 @@ export class LatLonController { return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.warn('latLonController.getCoordinates Error:', error); + this.logger.error({ msG: 'latLonController.getCoordinates error', error }); next(error); } }; diff --git a/src/latLon/models/latLonManager.ts b/src/latLon/models/latLonManager.ts index b1706cb0..645b2545 100644 --- a/src/latLon/models/latLonManager.ts +++ b/src/latLon/models/latLonManager.ts @@ -26,14 +26,14 @@ export class LatLonManager { public async latLonToTile({ lat, lon, targetGrid }: WGS84Coordinate & { targetGrid: string }): Promise { if (!validateWGS84Coordinate({ lat, lon })) { - this.logger.warn("LatLonManager.latLonToTile: Invalid lat lon, check 'lat' and 'lon' keys exists and their values are legal"); + this.logger.error({ msg: "LatLonManager.latLonToTile: Invalid lat lon, check 'lat' and 'lon' keys exists and their values are legal" }); throw new BadRequestError("Invalid lat lon, check 'lat' and 'lon' keys exists and their values are legal"); } const utm = convertWgs84ToUTM({ longitude: lon, latitude: lat }); if (typeof utm === 'string') { - this.logger.warn('LatLonManager.latLonToTile: utm is string'); + this.logger.error({ msg: 'LatLonManager.latLonToTile: utm is string' }); throw new BadRequestError('utm is string'); } @@ -46,7 +46,7 @@ export class LatLonManager { const tileCoordinateData = await this.latLonDAL.latLonToTile({ x: coordinatesUTM.x, y: coordinatesUTM.y, zone: coordinatesUTM.zone }); if (!tileCoordinateData) { - this.logger.warn('LatLonManager.latLonToTile: The coordinate is outside the grid extent'); + this.logger.error({ msg: 'LatLonManager.latLonToTile: The coordinate is outside the grid extent' }); throw new BadRequestError('The coordinate is outside the grid extent'); } @@ -89,7 +89,18 @@ export class LatLonManager { coordinates: [lon, lat], }, properties: { - name: tileCoordinateData.tile_name, + matches: [ + { + layer: 'convertionTable', + source: 'mapcolonies', + // eslint-disable-next-line @typescript-eslint/naming-convention + source_id: [], + }, + ], + names: { + default: [tileCoordinateData.tile_name], + display: tileCoordinateData.tile_name, + }, tileName: tileCoordinateData.tile_name, subTileNumber: new Array(SUB_TILE_LENGTH).fill('').map(function (value, i) { return xNumber[i] + yNumber[i]; @@ -103,9 +114,7 @@ export class LatLonManager { lon, accuracy = MGRS_ACCURACY, targetGrid, - }: { - lat: number; - lon: number; + }: WGS84Coordinate & { accuracy?: number; targetGrid: string; }): GenericGeocodingFeatureResponse { @@ -139,9 +148,21 @@ export class LatLonManager { coordinates: [lon, lat], }, properties: { - name: mgrsStr, + matches: [ + { + layer: 'MGRS', + source: 'npm/MGRS', + // eslint-disable-next-line @typescript-eslint/naming-convention + source_id: [], + }, + ], + names: { + default: [mgrsStr], + display: mgrsStr, + }, accuracy: accuracyString[accuracy], mgrs: mgrsStr, + score: 1, }, }; } diff --git a/src/location/DAL/locationRepository.ts b/src/location/DAL/locationRepository.ts index bf201ad3..d436be44 100644 --- a/src/location/DAL/locationRepository.ts +++ b/src/location/DAL/locationRepository.ts @@ -2,7 +2,7 @@ import { Logger } from '@map-colonies/js-logger'; import { estypes } from '@elastic/elasticsearch'; import { FactoryFunction } from 'tsyringe'; import { ElasticClient } from '../../common/elastic'; -import { cleanQuery, fetchNLPService } from '../utils'; +import { fetchNLPService } from '../utils'; import { TextSearchParams, TokenResponse } from '../interfaces'; import { PlaceTypeSearchHit, HierarchySearchHit, TextSearchHit } from '../models/elasticsearchHits'; import { BadRequestError } from '../../common/errors'; @@ -12,7 +12,10 @@ import { queryElastic } from '../../common/elastic/utils'; import { SERVICES } from '../../common/constants'; import { hierarchyQuery, placetypeQuery, geotextQuery } from './queries'; -/* eslint-enable @typescript-eslint/naming-convention */ +const FIND_QUOTES = /["']/g; + +const FIND_SPECIAL = /[`!@#$%^&*()_\-+=|\\/,.<>:[\]{}\n\t\r\s;؛]+/g; +const cleanQuery = (query: string): string[] => query.replace(FIND_QUOTES, '').split(FIND_SPECIAL); // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const createGeotextRepository = (client: ElasticClient, logger: Logger) => { @@ -25,7 +28,7 @@ const createGeotextRepository = (client: ElasticClient, logger: Logger) => { if (!tokens || !tokens.length || !prediction || !prediction.length) { const message = 'No tokens or prediction'; - logger.error({ message }); + logger.error({ msg: message }); throw new BadRequestError(message); } diff --git a/src/location/controllers/locationController.ts b/src/location/controllers/locationController.ts index 85908055..41739e72 100644 --- a/src/location/controllers/locationController.ts +++ b/src/location/controllers/locationController.ts @@ -47,7 +47,7 @@ export class GeotextSearchController { const response = await this.manager.search({ query, region, source, disableFuzziness, geoContext, geoContextMode, limit }); return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.error({ message: 'Error in getGeotextSearch', error }); + this.logger.error({ msg: 'Error in getGeotextSearch', error }); next(error); } }; diff --git a/src/location/utils.ts b/src/location/utils.ts index 7f9e0a71..03b69e80 100644 --- a/src/location/utils.ts +++ b/src/location/utils.ts @@ -9,14 +9,11 @@ import { TextSearchParams } from './interfaces'; import { TextSearchHit } from './models/elasticsearchHits'; import { generateDisplayName } from './parsing'; -const FIND_QUOTES = /["']/g; - -const FIND_SPECIAL = /[`!@#$%^&*()_\-+=|\\/,.<>:[\]{}\n\t\r\s;؛]+/g; - const axiosInstance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }), }); +/* istanbul ignore next */ export const fetchNLPService = async (endpoint: string, requestData: object): Promise<{ data: T[]; latency: number }> => { let res: Response | null = null, data: T[] | undefined | null = null; @@ -38,8 +35,6 @@ export const fetchNLPService = async (endpoint: string, requestData: object): return { data, latency }; }; -export const cleanQuery = (query: string): string[] => query.replace(FIND_QUOTES, '').split(FIND_SPECIAL); - /* eslint-disable @typescript-eslint/naming-convention */ export const convertResult = ( params: TextSearchParams, @@ -61,7 +56,7 @@ export const convertResult = ( placeType: number; hierarchies: number; }; - } = { nameKeys: [], mainLanguageRegex: '', externalResourcesLatency: { query: 0, nlpAnalyser: 0, placeType: 0, hierarchies: 0 } } + } ): GenericGeocodingResponse => ({ type: 'FeatureCollection', geocoding: { diff --git a/src/mgrs/controllers/mgrsController.ts b/src/mgrs/controllers/mgrsController.ts index c0b459e9..4ce70797 100644 --- a/src/mgrs/controllers/mgrsController.ts +++ b/src/mgrs/controllers/mgrsController.ts @@ -42,7 +42,7 @@ export class MgrsController { const response = this.manager.getTile({ tile }); return res.status(httpStatus.OK).json(response); } catch (error: unknown) { - this.logger.error({ message: 'MgrsController.getTile', error }); + this.logger.error({ msg: 'MgrsController.getTile', error }); next(error); } }; diff --git a/src/mgrs/models/mgrsManager.ts b/src/mgrs/models/mgrsManager.ts index 5c967805..7ef030e6 100644 --- a/src/mgrs/models/mgrsManager.ts +++ b/src/mgrs/models/mgrsManager.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { IConfig } from 'config'; import { Logger } from '@map-colonies/js-logger'; -import { BBox, Feature, Geometry } from 'geojson'; +import { BBox, Geometry } from 'geojson'; import { inject, injectable } from 'tsyringe'; import * as mgrs from 'mgrs'; import { SERVICES } from '../../common/constants'; -import { GenericGeocodingResponse, IApplication } from '../../common/interfaces'; +import { GenericGeocodingFeatureResponse, IApplication } from '../../common/interfaces'; import { GetTileQueryParams } from '../controllers/mgrsController'; import { BadRequestError } from '../../common/errors'; import { parseGeo } from '../../common/utils'; @@ -18,16 +18,13 @@ export class MgrsManager { @inject(SERVICES.APPLICATION) private readonly application: IApplication ) {} - public getTile({ tile }: GetTileQueryParams): Feature & Pick, 'geocoding'> { + public getTile({ tile }: GetTileQueryParams): GenericGeocodingFeatureResponse { let bbox: BBox | undefined; try { bbox = mgrs.inverse(tile); } catch (error) { - if ((error as Error).message.includes('MGRSPoint bad conversion')) { - throw new BadRequestError('Invalid MGRS tile'); - } - this.logger.error({ message: 'Failed to convert MGRS tile to bbox.', error }); - throw error; + this.logger.error({ msg: 'Failed to convert MGRS tile to bbox.', error }); + throw new BadRequestError(`Invalid MGRS tile. ${(error as Error).message}`); } const geometry = parseGeo({ bbox }) as Geometry; @@ -48,6 +45,17 @@ export class MgrsManager { bbox, geometry, properties: { + matches: [ + { + layer: 'MGRS', + source: 'npm/mgrs', + source_id: [], + }, + ], + names: { + default: [tile], + display: tile, + }, score: 1, }, }; diff --git a/src/mgrs/routers/mgrsRouter.ts b/src/mgrs/routes/mgrsRouter.ts similarity index 100% rename from src/mgrs/routers/mgrsRouter.ts rename to src/mgrs/routes/mgrsRouter.ts diff --git a/src/serverBuilder.ts b/src/serverBuilder.ts index 6a533d37..3bf7bb5d 100644 --- a/src/serverBuilder.ts +++ b/src/serverBuilder.ts @@ -17,7 +17,7 @@ import { LAT_LON_ROUTER_SYMBOL } from './latLon/routes/latLonRouter'; import { GEOTEXT_SEARCH_ROUTER_SYMBOL } from './location/routes/locationRouter'; import { cronLoadTileLatLonDataSymbol } from './latLon/DAL/latLonDAL'; import { FeedbackApiMiddlewareManager } from './common/middlewares/feedbackApi.middleware'; -import { MGRS_ROUTER_SYMBOL } from './mgrs/routers/mgrsRouter'; +import { MGRS_ROUTER_SYMBOL } from './mgrs/routes/mgrsRouter'; @injectable() export class ServerBuilder { diff --git a/tests/configurations/unit/jest.config.js b/tests/configurations/unit/jest.config.js index bfd03fc4..2cea4e27 100644 --- a/tests/configurations/unit/jest.config.js +++ b/tests/configurations/unit/jest.config.js @@ -12,6 +12,7 @@ module.exports = { '!/vendor/**', '!*/common/**', '!**/controllers/**', + '!**/DAL/**', '!**/routes/**', '!**/redis/**', '!/src/*', diff --git a/tests/integration/control/item/item.spec.ts b/tests/integration/control/item/item.spec.ts index 0fad0366..7e362261 100644 --- a/tests/integration/control/item/item.spec.ts +++ b/tests/integration/control/item/item.spec.ts @@ -13,8 +13,8 @@ import { CommonRequestParameters, GenericGeocodingResponse, GeoContext, GeoConte import { cronLoadTileLatLonDataSymbol } from '../../../../src/latLon/DAL/latLonDAL'; import { S3_REPOSITORY_SYMBOL } from '../../../../src/common/s3/s3Repository'; import { expectedResponse } from '../utils'; +import { ITEM_1234, ITEM_1235, ITEM_1236 } from '../../../mockObjects/items'; import { ItemRequestSender } from './helpers/requestSender'; -import { ITEM_1234, ITEM_1235, ITEM_1236 } from './mockObjects'; describe('/search/control/items', function () { let requestSender: ItemRequestSender; diff --git a/tests/integration/control/route/route.spec.ts b/tests/integration/control/route/route.spec.ts index 5b117c7a..222d16cd 100644 --- a/tests/integration/control/route/route.spec.ts +++ b/tests/integration/control/route/route.spec.ts @@ -13,8 +13,13 @@ import { CommonRequestParameters, GenericGeocodingResponse, GeoContext, GeoConte import { S3_REPOSITORY_SYMBOL } from '../../../../src/common/s3/s3Repository'; import { cronLoadTileLatLonDataSymbol } from '../../../../src/latLon/DAL/latLonDAL'; import { expectedResponse } from '../utils'; +import { + ROUTE_VIA_CAMILLUCCIA_A, + ROUTE_VIA_CAMILLUCCIA_B, + CONTROL_POINT_OLIMPIADE_111, + CONTROL_POINT_OLIMPIADE_112, +} from '../../../mockObjects/routes'; import { RouteRequestSender } from './helpers/requestSender'; -import { ROUTE_VIA_CAMILLUCCIA_A, ROUTE_VIA_CAMILLUCCIA_B, CONTROL_POINT_OLIMPIADE_111, CONTROL_POINT_OLIMPIADE_112 } from './mockObjects'; describe('/search/control/route', function () { let requestSender: RouteRequestSender; diff --git a/tests/integration/control/tile/tile.spec.ts b/tests/integration/control/tile/tile.spec.ts index c44d6967..9ba39459 100644 --- a/tests/integration/control/tile/tile.spec.ts +++ b/tests/integration/control/tile/tile.spec.ts @@ -14,8 +14,8 @@ import { CommonRequestParameters, GenericGeocodingResponse, GeoContext, GeoConte import { S3_REPOSITORY_SYMBOL } from '../../../../src/common/s3/s3Repository'; import { cronLoadTileLatLonDataSymbol } from '../../../../src/latLon/DAL/latLonDAL'; import { expectedResponse } from '../utils'; +import { RIC_TILE, RIT_TILE, SUB_TILE_65, SUB_TILE_66 } from '../../../mockObjects/tiles'; import { TileRequestSender } from './helpers/requestSender'; -import { RIC_TILE, RIT_TILE, SUB_TILE_65, SUB_TILE_66 } from './mockObjects'; describe('/search/control/tiles', function () { let requestSender: TileRequestSender; diff --git a/tests/integration/latLon/latLon.spec.ts b/tests/integration/latLon/latLon.spec.ts index fe50dd93..add94956 100644 --- a/tests/integration/latLon/latLon.spec.ts +++ b/tests/integration/latLon/latLon.spec.ts @@ -9,6 +9,7 @@ import httpStatusCodes from 'http-status-codes'; import { getApp } from '../../../src/app'; import { SERVICES } from '../../../src/common/constants'; import { LatLonDAL } from '../../../src/latLon/DAL/latLonDAL'; +import { GenericGeocodingFeatureResponse } from '../../../src/common/interfaces'; import { LatLonRequestSender } from './helpers/requestSender'; describe('/lookup', function () { @@ -45,7 +46,7 @@ describe('/lookup', function () { expect(response.status).toBe(httpStatusCodes.OK); expect(response).toSatisfyApiSpec(); - expect(response.body).toEqual({ + expect(response.body).toEqual({ type: 'Feature', geocoding: { version: expect.any(String) as string, @@ -74,7 +75,17 @@ describe('/lookup', function () { ], }, properties: { - name: 'BRN', + matches: [ + { + layer: 'convertionTable', + source: 'mapcolonies', + source_id: [], + }, + ], + names: { + default: ['BRN'], + display: 'BRN', + }, tileName: 'BRN', subTileNumber: ['06', '97', '97'], }, @@ -90,7 +101,7 @@ describe('/lookup', function () { expect(response.status).toBe(httpStatusCodes.OK); expect(response).toSatisfyApiSpec(); - expect(response.body).toEqual({ + expect(response.body).toEqual({ type: 'Feature', geocoding: { version: expect.any(String) as string, @@ -111,9 +122,20 @@ describe('/lookup', function () { coordinates: [12.948781146422107, 52.57326537485767], }, properties: { - name: '33UUU6099626777', + matches: [ + { + layer: 'MGRS', + source: 'npm/MGRS', + source_id: [], + }, + ], + names: { + default: ['33UUU6099626777'], + display: '33UUU6099626777', + }, accuracy: '1m', mgrs: '33UUU6099626777', + score: 1, }, }); }); diff --git a/tests/integration/location/location.spec.ts b/tests/integration/location/location.spec.ts index f9c81011..68314c99 100644 --- a/tests/integration/location/location.spec.ts +++ b/tests/integration/location/location.spec.ts @@ -14,7 +14,6 @@ import { S3_REPOSITORY_SYMBOL } from '../../../src/common/s3/s3Repository'; import { cronLoadTileLatLonDataSymbol } from '../../../src/latLon/DAL/latLonDAL'; import { GetGeotextSearchParams } from '../../../src/location/interfaces'; import { GenericGeocodingResponse, GeoContext, GeoContextMode, IApplication } from '../../../src/common/interfaces'; -import { LocationRequestSender } from './helpers/requestSender'; import { OSM_LA_PORT, GOOGLE_LA_PORT, @@ -25,7 +24,8 @@ import { LA_HIERRARCHY, MockLocationQueryFeature, PARIS_WI_SCHOOL, -} from './mockObjects'; +} from '../../mockObjects/locations'; +import { LocationRequestSender } from './helpers/requestSender'; import { expectedResponse, hierarchiesWithAnyWieght } from './utils'; describe('/search/location', function () { diff --git a/tests/integration/location/utils.ts b/tests/integration/location/utils.ts index 9e9a6da0..5c0c009b 100644 --- a/tests/integration/location/utils.ts +++ b/tests/integration/location/utils.ts @@ -2,7 +2,7 @@ import { Feature } from 'geojson'; import { GetGeotextSearchParams } from '../../../src/location/interfaces'; import { GenericGeocodingResponse } from '../../../src/common/interfaces'; -import { MockLocationQueryFeature } from './mockObjects'; +import { MockLocationQueryFeature } from '../../mockObjects/locations'; const expectedObjectWithScore = (obj: MockLocationQueryFeature, expect: jest.Expect): GenericGeocodingResponse['features'][number] => ({ diff --git a/tests/integration/mgrs/mgrs.spec.ts b/tests/integration/mgrs/mgrs.spec.ts index 0b885b36..3e581475 100644 --- a/tests/integration/mgrs/mgrs.spec.ts +++ b/tests/integration/mgrs/mgrs.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import 'jest-openapi'; import { DependencyContainer } from 'tsyringe'; import { Application } from 'express'; import { CleanupRegistry } from '@map-colonies/cleanup-registry'; @@ -73,6 +74,17 @@ describe('/search/MGRS', function () { ], }, properties: { + matches: [ + { + layer: 'MGRS', + source: 'npm/mgrs', + source_id: [], + }, + ], + names: { + default: ['18SUJ2339007393'], + display: '18SUJ2339007393', + }, score: 1, }, }); @@ -84,7 +96,7 @@ describe('/search/MGRS', function () { const response = await requestSender.getTile({ tile: 'ABC{}' }); expect(response.body).toMatchObject({ - message: 'Invalid MGRS tile', + message: 'Invalid MGRS tile. MGRSPoint bad conversion from ABC{}', }); expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); }); @@ -97,16 +109,13 @@ describe('/search/MGRS', function () { message: "request/query must have required property 'tile'", }); }); - }); - describe('Sad Path', function () { - // All requests with status code 4XX-5XX it('should return 500 status code when MGRSPoint zone letter A not handled', async function () { const response = await requestSender.getTile({ tile: '{ABC}' }); - expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); expect(response.body).toMatchObject({ - message: 'MGRSPoint zone letter A not handled: {ABC}', + message: 'Invalid MGRS tile. MGRSPoint zone letter A not handled: {ABC}', }); }); }); diff --git a/tests/integration/control/item/mockObjects.ts b/tests/mockObjects/items.ts similarity index 97% rename from tests/integration/control/item/mockObjects.ts rename to tests/mockObjects/items.ts index 96a62178..2037d950 100644 --- a/tests/integration/control/item/mockObjects.ts +++ b/tests/mockObjects/items.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { Item } from '../../../../src/control/item/models/item'; +import { Item } from '../../src/control/item/models/item'; export const ITEM_1234: Item = { type: 'Feature', diff --git a/tests/integration/location/mockObjects.ts b/tests/mockObjects/locations.ts similarity index 97% rename from tests/integration/location/mockObjects.ts rename to tests/mockObjects/locations.ts index 8fede417..ec1f0f35 100644 --- a/tests/integration/location/mockObjects.ts +++ b/tests/mockObjects/locations.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { Feature } from 'geojson'; -import { GenericGeocodingResponse } from '../../../src/common/interfaces'; -import { HierarchySearchHit } from '../../../src/location/models/elasticsearchHits'; +import { GenericGeocodingResponse } from '../../src/common/interfaces'; +import { HierarchySearchHit } from '../../src/location/models/elasticsearchHits'; export type MockLocationQueryFeature = GenericGeocodingResponse['features'][number]; @@ -49,10 +49,10 @@ export const NY_JFK_AIRPORT: MockLocationQueryFeature = { properties: { matches: [{ source: 'OSM', layer: 'osm_airports', source_id: ['03ed6d97-fc81-4340-b68a-11993554eef1'] }], names: { - en: ['JFK Airport'], + en: ['JFK International Airport', 'John F Kennedy International Airport'], fr: ['Aeropuerto JFK'], - default: ['JFK'], - display: 'JFK Airport', + default: ['JFK International Airport'], + display: 'John F Kennedy International Airport', }, placetype: 'transportation', sub_placetype: 'airport', diff --git a/tests/integration/control/route/mockObjects.ts b/tests/mockObjects/routes.ts similarity index 96% rename from tests/integration/control/route/mockObjects.ts rename to tests/mockObjects/routes.ts index eb60440b..d0252209 100644 --- a/tests/integration/control/route/mockObjects.ts +++ b/tests/mockObjects/routes.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { Route } from '../../../../src/control/route/models/route'; + +import { Route } from '../../src/control/route/models/route'; export const ROUTE_VIA_CAMILLUCCIA_A: Route = { type: 'Feature', diff --git a/tests/integration/control/tile/mockObjects.ts b/tests/mockObjects/tiles.ts similarity index 96% rename from tests/integration/control/tile/mockObjects.ts rename to tests/mockObjects/tiles.ts index 2292a2cd..923df454 100644 --- a/tests/integration/control/tile/mockObjects.ts +++ b/tests/mockObjects/tiles.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { Tile } from '../../../../src/control/tile/models/tile'; + +import { Tile } from '../../src/control/tile/models/tile'; export const RIT_TILE: Tile = { type: 'Feature', diff --git a/tests/unit/control/item/item.spec.ts b/tests/unit/control/item/item.spec.ts new file mode 100644 index 00000000..0fa1576e --- /dev/null +++ b/tests/unit/control/item/item.spec.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import jsLogger from '@map-colonies/js-logger'; +import { estypes } from '@elastic/elasticsearch'; +import { ItemQueryParams } from '../../../../src/control/item/DAL/queries'; +import { ItemRepository } from '../../../../src/control/item/DAL/itemRepository'; +import { ItemManager } from '../../../../src/control/item/models/itemManager'; +import { GenericGeocodingResponse, IApplication } from '../../../../src/common/interfaces'; +import { Item } from '../../../../src/control/item/models/item'; +import { convertCamelToSnakeCase } from '../../../../src/control/utils'; +import { ITEM_1234 } from '../../../mockObjects/items'; + +let itemManager: ItemManager; + +describe('#ItemManager', () => { + const getItems = jest.fn(); + const controlObjectDisplayNamePrefixes = { ITEM: 'Item' }; + beforeEach(() => { + jest.resetAllMocks(); + + const repository = { + getItems, + } as unknown as ItemRepository; + + itemManager = new ItemManager( + jsLogger({ enabled: false }), + {} as never, + { + controlObjectDisplayNamePrefixes, + } as unknown as IApplication, + repository + ); + }); + + test.each<{ + feature: Item; + queryParams: ItemQueryParams; + expectedNames: { + default: string[]; + display: string; + }; + mockFunction: jest.Mock; + }>([ + { + feature: ITEM_1234, + queryParams: { + commandName: ITEM_1234.properties.OBJECT_COMMAND_NAME, + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [ITEM_1234.properties.OBJECT_COMMAND_NAME], + display: `${controlObjectDisplayNamePrefixes.ITEM} ${ITEM_1234.properties.OBJECT_COMMAND_NAME}`, + }, + mockFunction: getItems, + }, + ])('should response with the right Geocoding GeoJSON', async ({ feature, queryParams, expectedNames, mockFunction }) => { + const hit: estypes.SearchResponse = { + took: 3, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 3.5145261, + hits: [ + { + _index: 'control_gil_v5', + _id: expect.any(String) as string, + _score: expect.any(Number) as number, + _source: feature, + }, + ], + }, + }; + + mockFunction.mockResolvedValue(hit); + + const generated = await itemManager.getItems(queryParams); + + expect(generated).toEqual>({ + type: 'FeatureCollection', + geocoding: { + version: process.env.npm_package_version as string, + query: convertCamelToSnakeCase(queryParams as unknown as Record), + response: { + results_count: 1, + max_score: expect.any(Number) as number, + match_latency_ms: expect.any(Number) as number, + }, + }, + features: [ + { + type: 'Feature', + properties: { + ...feature.properties, + matches: [ + { + layer: feature.properties.LAYER_NAME, + source: hit.hits.hits[0]._index, + source_id: [], + }, + ], + names: expectedNames, + score: expect.any(Number) as number, + }, + geometry: feature.geometry, + }, + ], + }); + }); +}); diff --git a/tests/unit/control/location/location.spec.ts b/tests/unit/control/location/location.spec.ts new file mode 100644 index 00000000..320d496c --- /dev/null +++ b/tests/unit/control/location/location.spec.ts @@ -0,0 +1,159 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import config from 'config'; +import jsLogger from '@map-colonies/js-logger'; +import { Feature } from 'geojson'; +import { estypes } from '@elastic/elasticsearch'; +import { GeotextRepository } from '../../../../src/location/DAL/locationRepository'; +import { GeotextSearchManager } from '../../../../src/location/models/locationManager'; +import { GenericGeocodingResponse, IApplication } from '../../../../src/common/interfaces'; +import { ConvertSnakeToCamelCase } from '../../../../src/common/utils'; +import { GetGeotextSearchParams } from '../../../../src/location/interfaces'; +import { NY_JFK_AIRPORT } from '../../../mockObjects/locations'; +import { convertCamelToSnakeCase } from '../../../../src/control/utils'; +import { expectedResponse } from '../../../integration/location/utils'; + +let geotextSearchManager: GeotextSearchManager; +describe('#GeotextSearchManager', () => { + const extractName = jest.fn(); + const generatePlacetype = jest.fn(); + const extractHierarchy = jest.fn(); + const geotextSearch = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + + const repositry = { + extractName, + generatePlacetype, + extractHierarchy, + geotextSearch, + } as unknown as GeotextRepository; + + geotextSearchManager = new GeotextSearchManager(jsLogger({ enabled: false }), config.get('application'), config, repositry); + }); + + test.each([['JFK International Airport', 'John F Kennedy International Airport'], undefined])( + 'should return location response and highlight %s', + async (highlight) => { + const extractNameResolve = { name: '', latency: 7 }; + const generatePlacetypeResolve = { + placeTypes: ['transportation'], + subPlaceTypes: ['airport'], + matchLatencyMs: 6, + }; + const extractHierarchyResolve = { + hierarchies: [], + matchLatencyMs: 1, + }; + + extractName.mockResolvedValueOnce(extractNameResolve); + generatePlacetype.mockResolvedValueOnce(generatePlacetypeResolve); + extractHierarchy.mockResolvedValueOnce(extractHierarchyResolve); + geotextSearch.mockResolvedValueOnce({ + took: 2, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { value: 3, relation: 'eq' }, + max_score: 1.2880917, + hits: [ + { + _index: expect.any(String) as string, + _id: expect.any(String) as string, + _score: 1.2880917, + _source: { + source: NY_JFK_AIRPORT.properties.matches[0].source, + layer_name: NY_JFK_AIRPORT.properties.matches[0].layer, + source_id: NY_JFK_AIRPORT.properties.matches[0].source_id, + placetype: NY_JFK_AIRPORT.properties.placetype as string, + sub_placetype: NY_JFK_AIRPORT.properties.sub_placetype as string, + geo_json: NY_JFK_AIRPORT.geometry, + region: [(NY_JFK_AIRPORT.properties.regions as { region: string }[])[0].region], + sub_region: (NY_JFK_AIRPORT.properties.regions as { sub_region_names: string[] }[])[0].sub_region_names, + name: NY_JFK_AIRPORT.properties.names.default[0], + text: NY_JFK_AIRPORT.properties.names.en, + translated_text: NY_JFK_AIRPORT.properties.names.fr, + }, + highlight: highlight ? { text: highlight } : undefined, + }, + ], + }, + }); + + const query: ConvertSnakeToCamelCase = { + query: 'airport', + disableFuzziness: false, + limit: 1, + }; + + const response = await geotextSearchManager.search(query); + + expect(response).toEqual>( + expectedResponse( + { + ...(convertCamelToSnakeCase(query as unknown as Record) as unknown as GetGeotextSearchParams), + geo_context: undefined, + geo_context_mode: undefined, + region: undefined, + source: undefined, + }, + { + place_types: ['transportation'], + sub_place_types: ['airport'], + hierarchies: undefined, + name: '', + }, + [ + { + ...NY_JFK_AIRPORT, + properties: { + ...NY_JFK_AIRPORT.properties, + names: { + ...NY_JFK_AIRPORT.properties.names, + display: expect.stringContaining('JFK') as string, + }, + }, + }, + ], + expect + ) + ); + } + ); + + it('should return sources', () => { + const response = geotextSearchManager.sources(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(response).toEqual(Object.keys(config.get('application').sources!)); + }); + it('should return regions', () => { + const response = geotextSearchManager.regions(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(response).toEqual(Object.keys(config.get('application').regions!)); + }); + + it('should return empty sources array', () => { + geotextSearchManager = new GeotextSearchManager( + jsLogger({ enabled: false }), + {} as unknown as IApplication, + config, + {} as unknown as GeotextRepository + ); + + const response = geotextSearchManager.sources(); + expect(response).toEqual(Object.keys({})); + }); + + it('should return empty regions array', () => { + geotextSearchManager = new GeotextSearchManager( + jsLogger({ enabled: false }), + {} as unknown as IApplication, + config, + {} as unknown as GeotextRepository + ); + + const response = geotextSearchManager.regions(); + expect(response).toEqual(Object.keys({})); + }); +}); diff --git a/tests/unit/control/route/route.spec.ts b/tests/unit/control/route/route.spec.ts new file mode 100644 index 00000000..14505a9d --- /dev/null +++ b/tests/unit/control/route/route.spec.ts @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import jsLogger from '@map-colonies/js-logger'; +import { estypes } from '@elastic/elasticsearch'; +import { RouteQueryParams } from '../../../../src/control/route/DAL/queries'; +import { RouteRepository } from '../../../../src/control/route/DAL/routeRepository'; +import { RouteManager } from '../../../../src/control/route/models/routeManager'; +import { GenericGeocodingResponse, IApplication } from '../../../../src/common/interfaces'; +import { Route } from '../../../../src/control/route/models/route'; +import { convertCamelToSnakeCase } from '../../../../src/control/utils'; +import { CONTROL_POINT_OLIMPIADE_111, ROUTE_VIA_CAMILLUCCIA_A } from '../../../mockObjects/routes'; + +let routeManager: RouteManager; + +describe('#RouteManager', () => { + const getRoutes = jest.fn(); + const getControlPointInRoute = jest.fn(); + const controlObjectDisplayNamePrefixes = { ROUTE: 'Route', CONTROL_POINT: 'Control Point' }; + beforeEach(() => { + jest.resetAllMocks(); + + const repository = { + getRoutes, + getControlPointInRoute, + } as unknown as RouteRepository; + + routeManager = new RouteManager( + jsLogger({ enabled: false }), + {} as never, + { + controlObjectDisplayNamePrefixes, + } as unknown as IApplication, + repository + ); + }); + + test.each<{ + feature: Route; + queryParams: RouteQueryParams; + expectedNames: { + default: string[]; + display: string; + }; + mockFunction: jest.Mock; + }>([ + { + feature: ROUTE_VIA_CAMILLUCCIA_A, + queryParams: { + commandName: ROUTE_VIA_CAMILLUCCIA_A.properties.OBJECT_COMMAND_NAME, + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [ROUTE_VIA_CAMILLUCCIA_A.properties.OBJECT_COMMAND_NAME], + display: `${controlObjectDisplayNamePrefixes.ROUTE} ${ROUTE_VIA_CAMILLUCCIA_A.properties.OBJECT_COMMAND_NAME}`, + }, + mockFunction: getRoutes, + }, + { + feature: CONTROL_POINT_OLIMPIADE_111, + queryParams: { + commandName: CONTROL_POINT_OLIMPIADE_111.properties.TIED_TO as string, + controlPoint: CONTROL_POINT_OLIMPIADE_111.properties.OBJECT_COMMAND_NAME, + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [CONTROL_POINT_OLIMPIADE_111.properties.OBJECT_COMMAND_NAME], + display: `${controlObjectDisplayNamePrefixes.ROUTE} ${CONTROL_POINT_OLIMPIADE_111.properties.TIED_TO as string} ${ + controlObjectDisplayNamePrefixes.CONTROL_POINT + } ${CONTROL_POINT_OLIMPIADE_111.properties.OBJECT_COMMAND_NAME}`, + }, + mockFunction: getControlPointInRoute, + }, + ])('should response with the right Geocoding GeoJSON', async ({ feature, queryParams, expectedNames, mockFunction }) => { + const hit: estypes.SearchResponse = { + took: 3, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 3.5145261, + hits: [ + { + _index: 'control_gil_v5', + _id: expect.any(String) as string, + _score: expect.any(Number) as number, + _source: feature, + }, + ], + }, + }; + + mockFunction.mockResolvedValue(hit); + + const generated = await routeManager.getRoutes(queryParams); + + expect(generated).toEqual>({ + type: 'FeatureCollection', + geocoding: { + version: process.env.npm_package_version as string, + query: convertCamelToSnakeCase(queryParams as unknown as Record), + response: { + results_count: 1, + max_score: expect.any(Number) as number, + match_latency_ms: expect.any(Number) as number, + }, + }, + features: [ + { + type: 'Feature', + properties: { + ...feature.properties, + matches: [ + { + layer: feature.properties.LAYER_NAME, + source: hit.hits.hits[0]._index, + source_id: [], + }, + ], + names: expectedNames, + score: expect.any(Number) as number, + }, + geometry: feature.geometry, + }, + ], + }); + }); +}); diff --git a/tests/unit/control/tile/tile.spec.ts b/tests/unit/control/tile/tile.spec.ts new file mode 100644 index 00000000..672add37 --- /dev/null +++ b/tests/unit/control/tile/tile.spec.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import jsLogger from '@map-colonies/js-logger'; +import { estypes } from '@elastic/elasticsearch'; +import { TileQueryParams } from '../../../../src/control/tile/DAL/queries'; +import { TileRepository } from '../../../../src/control/tile/DAL/tileRepository'; +import { TileManager } from '../../../../src/control/tile/models/tileManager'; +import { GenericGeocodingResponse, IApplication } from '../../../../src/common/interfaces'; +import { Tile } from '../../../../src/control/tile/models/tile'; +import { RIC_TILE, SUB_TILE_66 } from '../../../mockObjects/tiles'; +import { convertCamelToSnakeCase } from '../../../../src/control/utils'; +import { BadRequestError } from '../../../../src/common/errors'; + +let tileManager: TileManager; + +describe('#TileManager', () => { + const getTiles = jest.fn(); + const getSubTiles = jest.fn(); + const getTilesByBbox = jest.fn(); + const controlObjectDisplayNamePrefixes = { TILE: 'Tile', SUB_TILE: 'Sub Tile' }; + beforeEach(() => { + jest.resetAllMocks(); + + const repository = { + getTiles, + getSubTiles, + getTilesByBbox, + } as unknown as TileRepository; + + tileManager = new TileManager( + jsLogger({ enabled: false }), + {} as never, + { + controlObjectDisplayNamePrefixes, + } as unknown as IApplication, + repository + ); + }); + + test.each<{ + feature: Tile; + queryParams: TileQueryParams; + expectedNames: { + default: string[]; + display: string; + }; + mockFunction: jest.Mock; + }>([ + { + feature: RIC_TILE, + queryParams: { + tile: RIC_TILE.properties.TILE_NAME, + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [RIC_TILE.properties.TILE_NAME as string], + display: `${controlObjectDisplayNamePrefixes.TILE} ${RIC_TILE.properties.TILE_NAME as string}`, + }, + mockFunction: getTiles, + }, + { + feature: SUB_TILE_66, + queryParams: { + tile: SUB_TILE_66.properties.TILE_NAME, + subTile: SUB_TILE_66.properties.SUB_TILE_ID, + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [SUB_TILE_66.properties.SUB_TILE_ID as string], + display: `${controlObjectDisplayNamePrefixes.TILE} ${SUB_TILE_66.properties.TILE_NAME as string} ${ + controlObjectDisplayNamePrefixes.SUB_TILE + } ${SUB_TILE_66.properties.SUB_TILE_ID as string}`, + }, + mockFunction: getSubTiles, + }, + { + feature: RIC_TILE, + queryParams: { + mgrs: '33TTG958462', + limit: 5, + disableFuzziness: false, + }, + expectedNames: { + default: [RIC_TILE.properties.TILE_NAME as string], + display: `${controlObjectDisplayNamePrefixes.TILE} ${RIC_TILE.properties.TILE_NAME as string}`, + }, + mockFunction: getTilesByBbox, + }, + ])('should response with the right Geocoding GeoJSON', async ({ feature, queryParams, expectedNames, mockFunction }) => { + const hit: estypes.SearchResponse = { + took: 3, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 3.5145261, + hits: [ + { + _index: 'control_gil_v5', + _id: expect.any(String) as string, + _score: expect.any(Number) as number, + _source: feature, + }, + ], + }, + }; + + mockFunction.mockResolvedValue(hit); + + const generated = await tileManager.getTiles(queryParams); + + expect(generated).toEqual>({ + type: 'FeatureCollection', + geocoding: { + version: process.env.npm_package_version as string, + query: convertCamelToSnakeCase(queryParams as unknown as Record), + response: { + results_count: 1, + max_score: expect.any(Number) as number, + match_latency_ms: expect.any(Number) as number, + }, + }, + features: [ + { + type: 'Feature', + properties: { + ...feature.properties, + matches: [ + { + layer: feature.properties.LAYER_NAME as string, + source: hit.hits.hits[0]._index, + source_id: [], + }, + ], + names: expectedNames, + score: expect.any(Number) as number, + }, + geometry: feature.geometry, + }, + ], + }); + }); + + describe('Bad Path', () => { + // All requests with status code of 400 + it('should throw BadRequestError if both tile and mgrs are provided', async () => { + const queryParams = { + tile: RIC_TILE.properties.TILE_NAME, + mgrs: '33TTG958462', + limit: 5, + disableFuzziness: false, + }; + + await expect(tileManager.getTiles(queryParams)).rejects.toThrow( + new BadRequestError("/control/tiles: only one of 'tile' or 'mgrs' query parameter must be defined") + ); + }); + + it('should throw BadRequestError if invalid MGRS tile is provided', async () => { + const queryParams = { + mgrs: 'ABC{}', + limit: 5, + disableFuzziness: false, + }; + + await expect(tileManager.getTiles(queryParams)).rejects.toThrow(new BadRequestError(`Invalid MGRS: ${queryParams.mgrs}`)); + }); + }); +}); diff --git a/tests/unit/control/utils.spec.ts b/tests/unit/control/utils.spec.ts new file mode 100644 index 00000000..494a61fc --- /dev/null +++ b/tests/unit/control/utils.spec.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import config from 'config'; +import { estypes } from '@elastic/elasticsearch'; +import { additionalControlSearchProperties, convertCamelToSnakeCase } from '../../../src/control/utils'; +import { elasticConfigPath } from '../../../src/common/constants'; +import { ElasticDbClientsConfig } from '../../../src/common/elastic/interfaces'; +import { CONTROL_FIELDS } from '../../../src/control/constants'; + +describe('#convertCamelToSnakeCase', () => { + it('should convert camel case to snake case', () => { + const camelCase = { + camelCaseKey: 'value', + anotherCamelCaseKey: 'anotherValue', + }; + + const snakeCase = { + camel_case_key: 'value', + another_camel_case_key: 'anotherValue', + }; + + expect(convertCamelToSnakeCase(camelCase)).toEqual(snakeCase); + }); +}); + +describe('#additionalControlSearchProperties', () => { + it('should return additional control search properties', () => { + const size = 5; + const searchProperties = additionalControlSearchProperties(config, size); + + expect(searchProperties).toEqual>({ + size, + index: config.get(elasticConfigPath).control.properties.index as string, + _source: CONTROL_FIELDS, + }); + }); +}); diff --git a/tests/unit/latLon/latLon.spec.ts b/tests/unit/latLon/latLon.spec.ts new file mode 100644 index 00000000..8bfb31bc --- /dev/null +++ b/tests/unit/latLon/latLon.spec.ts @@ -0,0 +1,171 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import jsLogger from '@map-colonies/js-logger'; +import { LatLonManager } from '../../../src/latLon/models/latLonManager'; +import { LatLonDAL } from '../../../src/latLon/DAL/latLonDAL'; +import { GenericGeocodingFeatureResponse, WGS84Coordinate } from '../../../src/common/interfaces'; +import { convertCamelToSnakeCase } from '../../../src/control/utils'; +import { BadRequestError } from '../../../src/common/errors'; +import * as CommonUtils from '../../../src/common/utils'; + +type QueryParams = WGS84Coordinate & { targetGrid: 'control' | 'MGRS' }; + +let latLonManager: LatLonManager; +describe('#LatLonManager', () => { + const latLonToTile = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + const repositry = { latLonToTile } as unknown as LatLonDAL; + latLonManager = new LatLonManager(jsLogger({ enabled: false }), repositry, {} as never); + }); + + describe('happy path', () => { + it('should return control tile', async () => { + latLonToTile.mockResolvedValueOnce({ + tile_name: 'BRN', + zone: '33', + min_x: '360000', + min_y: '5820000', + ext_min_x: 360000, + ext_min_y: 5820000, + ext_max_x: 370000, + ext_max_y: 5830000, + }); + const queryParams: QueryParams = { + lat: 52.57326537485767, + lon: 12.948781146422107, + targetGrid: 'control', + }; + + const response = await latLonManager.latLonToTile(queryParams); + const minLongitude = 12.93694771534361, + minLatitude = 52.51211561266182, + maxLongitude = 13.080296161196031, + maxLatitude = 52.60444267653175; + + expect(response).toEqual({ + type: 'Feature', + geocoding: { + version: process.env.npm_package_version as string, + query: convertCamelToSnakeCase(queryParams as unknown as Record), + response: { + max_score: 1, + results_count: 1, + match_latency_ms: expect.any(Number) as number, + }, + }, + bbox: [minLongitude, minLatitude, maxLongitude, maxLatitude], + geometry: { + type: 'Polygon', + coordinates: [ + [ + [minLongitude, minLatitude], + [minLongitude, maxLatitude], + [maxLongitude, maxLatitude], + [maxLongitude, minLatitude], + [minLongitude, minLatitude], + ], + ], + }, + properties: { + matches: [ + { + layer: 'convertionTable', + source: 'mapcolonies', + source_id: [], + }, + ], + names: { + default: ['BRN'], + display: 'BRN', + }, + tileName: 'BRN', + subTileNumber: ['06', '97', '97'], + }, + }); + }); + + it('should return MGRS tile', () => { + const queryParams: QueryParams = { + lat: 52.57326537485767, + lon: 12.948781146422107, + targetGrid: 'MGRS', + }; + + const response = latLonManager.latLonToMGRS(queryParams); + + expect(response).toEqual({ + type: 'Feature', + geocoding: { + version: process.env.npm_package_version as string, + query: convertCamelToSnakeCase(queryParams as unknown as Record), + response: { + max_score: 1, + results_count: 1, + match_latency_ms: expect.any(Number) as number, + }, + }, + bbox: [12.948777289238832, 52.57325754975297, 12.948791616108007, 52.57326678960368], + geometry: { + type: 'Point', + coordinates: [12.948781146422107, 52.57326537485767], + }, + properties: { + matches: [ + { + layer: 'MGRS', + source: 'npm/MGRS', + source_id: [], + }, + ], + names: { + default: ['33UUU6099626777'], + display: '33UUU6099626777', + }, + accuracy: '1m', + mgrs: '33UUU6099626777', + score: 1, + }, + }); + }); + }); + + describe('sad path', () => { + it('should throw BadRequestError when invalid coordiantes are being provided', async () => { + const queryParams: QueryParams = { + lat: 100, + lon: 200, + targetGrid: 'control', + }; + + await expect(latLonManager.latLonToTile(queryParams)).rejects.toThrow( + new BadRequestError("Invalid lat lon, check 'lat' and 'lon' keys exists and their values are legal") + ); + }); + + it('should throw BadRequestError when the coordinate is outside the grid extent', async () => { + latLonToTile.mockResolvedValueOnce(null); + const queryParams: QueryParams = { + lat: 52.57326537485767, + lon: 12.948781146422107, + targetGrid: 'control', + }; + + await expect(latLonManager.latLonToTile(queryParams)).rejects.toThrow(new BadRequestError('The coordinate is outside the grid extent')); + }); + + it('should throw BadRequestError when convertWgs84ToUTM resolves as a string', async () => { + const spy = jest.spyOn(CommonUtils, 'convertWgs84ToUTM'); + spy.mockReturnValueOnce('abc'); + + const queryParams: QueryParams = { + lat: 52.57326537485767, + lon: 12.948781146422107, + targetGrid: 'control', + }; + + await expect(latLonManager.latLonToTile(queryParams)).rejects.toThrow(new BadRequestError('utm is string')); + + spy.mockClear(); + }); + }); +}); diff --git a/tests/unit/mgrs/mges.spec.ts b/tests/unit/mgrs/mges.spec.ts new file mode 100644 index 00000000..8c5d3c81 --- /dev/null +++ b/tests/unit/mgrs/mges.spec.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import jsLogger from '@map-colonies/js-logger'; +import { MgrsManager } from '../../../src/mgrs/models/mgrsManager'; +import { GetTileQueryParams } from '../../../src/mgrs/controllers/mgrsController'; +import { GenericGeocodingFeatureResponse } from '../../../src/common/interfaces'; +import { BadRequestError } from '../../../src/common/errors'; + +let mgrsManager: MgrsManager; +describe('#MgrsManager', () => { + beforeEach(() => { + mgrsManager = new MgrsManager(jsLogger({ enabled: false }), {} as never, {} as never); + }); + + describe('happy path', () => { + it('should return mgrs tile in its GeoJSON Representation', () => { + const queryParams: GetTileQueryParams = { + tile: '18SUJ2339007393', + }; + + const response = mgrsManager.getTile(queryParams); + + expect(response).toEqual({ + type: 'Feature', + geocoding: { + version: process.env.npm_package_version as string, + query: { + tile: queryParams.tile, + }, + response: { + max_score: 1, + results_count: 1, + match_latency_ms: expect.any(Number) as number, + }, + }, + bbox: [-77.03654883669269, 38.89767541638445, -77.03653756947197, 38.897684623284015], + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-77.03654883669269, 38.89767541638445], + [-77.03654883669269, 38.897684623284015], + [-77.03653756947197, 38.897684623284015], + [-77.03653756947197, 38.89767541638445], + [-77.03654883669269, 38.89767541638445], + ], + ], + }, + properties: { + matches: [ + { + layer: 'MGRS', + source: 'npm/mgrs', + source_id: [], + }, + ], + names: { + default: ['18SUJ2339007393'], + display: '18SUJ2339007393', + }, + score: 1, + }, + }); + }); + }); + describe('sad path', () => { + it('should throw a BadRequestError for invalid MGRS tile', () => { + const queryParams: GetTileQueryParams = { + tile: 'ABC{}', + }; + + expect(() => mgrsManager.getTile(queryParams)).toThrow(new BadRequestError('Invalid MGRS tile. MGRSPoint bad conversion from ABC{}')); + }); + }); + + describe('bad path', () => { + it('should throw an error for invalid MGRS tile', () => { + const queryParams: GetTileQueryParams = { + tile: '{ABC}', + }; + + expect(() => mgrsManager.getTile(queryParams)).toThrow(new BadRequestError('Invalid MGRS tile. MGRSPoint zone letter A not handled: {ABC}')); + }); + }); +});