diff --git a/config/.env-example b/config/.env-example index 03ecc4d2..4dde4c5d 100644 --- a/config/.env-example +++ b/config/.env-example @@ -21,6 +21,7 @@ siteUrl=http://dummyuri functionAppUrl==http://dummyuri ignoreUseAutomatedService=true placeApiUrl=http://dummyuri +#AGOL agolClientId=TEST_AGOL_CLIENT_ID agolClientSecret=TEST_AGOL_CLIENT_SECRET agolServiceUrl=http://dummyAgolUrl @@ -28,4 +29,7 @@ agolCustomerTeamEndPoint=/Flood_Map_for_Planning_Query_Service_NON_PRODUCTION/Fe agolLocalAuthorityEndPoint=/Flood_Map_for_Planning_Query_Service_NON_PRODUCTION/FeatureServer/1 agolIsEnglandEndPoint=/Flood_Map_for_Planning_Query_Service_NON_PRODUCTION/FeatureServer/2 agolFloodZonesRiversAndSeaEndPoint=/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/FeatureServer/0 - +#EA Maps +eamapsServiceUrl=http://dummyEAMapslUrl +eamapsProduct1User=PRODUCT1_USER +eamapsProduct1Password=PRODUCT1_PASSWORD diff --git a/config/index.js b/config/index.js index 90683de4..34834b9b 100644 --- a/config/index.js +++ b/config/index.js @@ -37,6 +37,13 @@ const config = { localAuthorityEndPoint: process.env.agolLocalAuthorityEndPoint, isEnglandEndPoint: process.env.agolIsEnglandEndPoint, floodZonesRiversAndSeaEndPoint: process.env.agolFloodZonesRiversAndSeaEndPoint + }, + eamaps: { + serviceUrl: process.env.eamapsServiceUrl, + product1User: process.env.eamapsProduct1User, + product1Password: process.env.eamapsProduct1Password, + product1EndPoint: '/rest/services/FMfP/FMFPGetProduct1/GPServer/fmfp_get_product1/execute', + tokenEndPoint: '/tokens/generateToken' } } diff --git a/config/schema.js b/config/schema.js index fd843872..2b221c7b 100644 --- a/config/schema.js +++ b/config/schema.js @@ -42,6 +42,13 @@ const schema = Joi.object({ localAuthorityEndPoint: Joi.string().required(), isEnglandEndPoint: Joi.string().required(), floodZonesRiversAndSeaEndPoint: Joi.string().required() + }, + eamaps: { + serviceUrl: Joi.string().uri().required(), + product1User: Joi.string().required(), + product1Password: Joi.string().required(), + product1EndPoint: Joi.string().required(), + tokenEndPoint: Joi.string().required() } }) diff --git a/package-lock.json b/package-lock.json index 0256fad2..572177a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "devDependencies": { "@hapi/code": "^8.0.7", "@hapi/lab": "^25.3.2", + "axios-mock-adapter": "^2.1.0", "babel-core": "^6.26.3", "babel-preset-es2015": "^6.24.1", "babelify": "^8.0.0", @@ -3670,6 +3671,42 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", diff --git a/package.json b/package.json index dacf1921..0b39f6b5 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@hapi/code": "^8.0.7", "@hapi/lab": "^25.3.2", + "axios-mock-adapter": "^2.1.0", "babel-core": "^6.26.3", "babel-preset-es2015": "^6.24.1", "babelify": "^8.0.0", diff --git a/server/plugins/router.js b/server/plugins/router.js index 5b1cc349..8de36ce0 100644 --- a/server/plugins/router.js +++ b/server/plugins/router.js @@ -12,7 +12,7 @@ const routes = [].concat( require('../routes/os-get-capabilities'), require('../routes/os-maps-proxy'), require('../routes/feedback'), - require('../routes/pdf'), + require('../routes/product-1'), require('../routes/os-terms'), require('../routes/error'), require('../routes/cookies'), diff --git a/server/routes/pdf.js b/server/routes/pdf.js deleted file mode 100644 index 6819816d..00000000 --- a/server/routes/pdf.js +++ /dev/null @@ -1,350 +0,0 @@ -const Joi = require('joi') -const Boom = require('@hapi/boom') -const Wreck = require('@hapi/wreck') -const moment = require('moment-timezone') -const { config } = require('../../config') -const FloodZone = require('../models/flood-zone') -const { osMapsUrl, osMapsKey } = config.ordnanceSurvey - -module.exports = { - method: 'POST', - path: '/pdf', - options: { - description: 'Generate PDF', - handler: async (request, h) => { - let zone = request.payload.zone - const scale = request.payload.scale - const reference = request.payload.reference || '' - const holdingComments = request.payload.holdingComments === 'true' - const siteUrl = config.siteUrl - const geoserverUrl = config.geoserver - const printUrl = geoserverUrl + '/geoserver/pdf/print.pdf' - const polygon = request.payload.polygon - ? JSON.parse(request.payload.polygon) - : undefined - const center = request.payload.center - ? JSON.parse(request.payload.center) - : undefined - let vector - - // Always get Flood zone as flood zone is provided in the request if not provided. - zone = await request.server.methods.getFloodZonesByPolygon(polygon) - const floodZone = new FloodZone(zone) - zone = floodZone.zone - - // Prepare point or polygon - if (polygon) { - vector = { - type: 'vector', - styles: { - '': { - strokeColor: '#b21122', - strokeWidth: 3, - fillColor: '#b21122', - fillOpacity: 0.1 - } - }, - geoJson: { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [polygon] - }, - properties: {} - } - } - } else { - vector = { - type: 'vector', - styles: { - '': { - externalGraphic: siteUrl + '/assets/images/pin.png', - graphicXOffset: -10.5, - graphicYOffset: -30.5, - graphicWidth: 21, - graphicHeight: 30.5 - } - }, - geoJson: { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: center - }, - properties: {} - } - } - } - - const pdfSummaryTemplate = holdingComments - ? `summary-template-${zone}-risk-changed.pdf` - : `summary-template-${zone}.pdf` - // Prepare the PDF generate options - const options = { - payload: { - layout: 'Map', - srs: 'EPSG:27700', - units: 'meters', - geodetic: true, - outputFormat: 'pdf', - reference, - easting: parseInt(center[0]), - scale, - northing: parseInt(center[1]), - timestamp: moment().tz('Europe/London').format('D MMM YYYY H:mm'), - pdfSummaryTemplate, - pdfMapTemplate: polygon - ? 'map-template-polygon.pdf' - : 'map-template.pdf', - layers: [ - { - type: 'WMTS', - baseURL: osMapsUrl, - version: '1.0.0', - requestEncoding: 'KVP', - customParams: { - url: 'https://flood-warning-information.service.gov.uk/' - }, - format: 'image/png', - layer: 'Outdoor_27700', - opacity: 1, - style: 'default', - matrixSet: `EPSG:27700&key=${osMapsKey}`, - matrixIds: [ - { - identifier: 'EPSG:27700:0', - matrixSize: [5, 7], - resolution: 896, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:1', - matrixSize: [10, 13], - resolution: 448, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:2', - matrixSize: [20, 25], - resolution: 224, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:3', - matrixSize: [40, 49], - resolution: 112, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:4', - matrixSize: [80, 98], - resolution: 56, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:5', - matrixSize: [159, 195], - resolution: 28, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:6', - matrixSize: [318, 390], - resolution: 14, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:7', - matrixSize: [636, 779], - resolution: 7, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:8', - matrixSize: [1271, 1558], - resolution: 3.5, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:9', - matrixSize: [2542, 3116], - resolution: 1.75, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:10', - matrixSize: [5083, 6232], - resolution: 0.875, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:11', - matrixSize: [10165, 12463], - resolution: 0.4375, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:12', - matrixSize: [20329, 24925], - resolution: 0.21875, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - }, - { - identifier: 'EPSG:27700:13', - matrixSize: [40657, 49849], - resolution: 0.109375, - tileSize: [256, 256], - topLeftCorner: [-238375.0, 1376256.0] - } - ] - }, - { - type: 'WMTS', - baseURL: geoserverUrl + '/geoserver/gwc/service/wmts', - layer: 'fmp:fmp', - version: '1.0.0', - requestEncoding: 'KVP', - format: 'image/png', - opacity: 0.7, - matrixSet: 'EPSG:27700', - matrixIds: [ - { - identifier: '00', - matrixSize: [5, 5], - resolution: 896, - tileSize: [250, 250], - topLeftCorner: [0, 1120000] - }, - { - identifier: '01', - matrixSize: [9, 9], - resolution: 448, - tileSize: [250, 250], - topLeftCorner: [0, 1008000] - }, - { - identifier: '02', - matrixSize: [18, 18], - resolution: 224, - tileSize: [250, 250], - topLeftCorner: [0, 1008000] - }, - { - identifier: '03', - matrixSize: [36, 36], - resolution: 112, - tileSize: [250, 250], - topLeftCorner: [0, 1008000] - }, - { - identifier: '04', - matrixSize: [72, 72], - resolution: 56, - tileSize: [250, 250], - topLeftCorner: [0, 1008000] - }, - { - identifier: '05', - matrixSize: [143, 143], - resolution: 28, - tileSize: [250, 250], - topLeftCorner: [0, 1001000] - }, - { - identifier: '06', - matrixSize: [286, 286], - resolution: 14, - tileSize: [250, 250], - topLeftCorner: [0, 1001000] - }, - { - identifier: '07', - matrixSize: [572, 572], - resolution: 7, - tileSize: [250, 250], - topLeftCorner: [0, 1001000] - }, - { - identifier: '08', - matrixSize: [1143, 1143], - resolution: 3.5, - tileSize: [250, 250], - topLeftCorner: [0, 1000125] - }, - { - identifier: '09', - matrixSize: [2286, 2286], - resolution: 1.75, - tileSize: [250, 250], - topLeftCorner: [0, 1000125] - }, - { - identifier: '10', - matrixSize: [4572, 4572], - resolution: 0.875, - tileSize: [250, 250], - topLeftCorner: [0, 1000125] - } - ] - }, - vector - ], - pages: [ - { - center, - scale, - dpi: 300, - geodetic: false, - strictEpsg4326: false - } - ] - } - } - - try { - const result = await Wreck.post(printUrl, options) - const date = new Date().toISOString() - return h - .response(result.payload) - .encoding('binary') - .type('application/pdf') - .header( - 'content-disposition', - `attachment; filename=flood-map-planning-${date}.pdf;` - ) - .header('X-XSS-Protection', '1; mode=block') - } catch (err) { - return Boom.badImplementation( - (err && err.message) || 'An error occured during PDF generation', - err - ) - } - }, - validate: { - payload: Joi.object().keys({ - reference: Joi.string().allow('').max(25).trim(), - scale: Joi.number().allow(2500, 10000, 25000, 50000).required(), - polygon: Joi.string().required().allow(''), - center: Joi.string().required(), - zone: Joi.string() - .valid('FZ1', 'FZ2', 'FZ2a', 'FZ3', 'FZ3a') - .default('FZ3'), - holdingComments: Joi.string().allow(''), - id: Joi.string().allow('') - }) - } - } -} diff --git a/server/routes/product-1.js b/server/routes/product-1.js new file mode 100644 index 00000000..9b160381 --- /dev/null +++ b/server/routes/product-1.js @@ -0,0 +1,48 @@ +const Boom = require('@hapi/boom') +const Joi = require('joi') +const { getProduct1 } = require('../services/eaMaps/getProduct1') + +const MAX_REFERENCE_WIDTH = 25 +const SCALE_2500 = 2500 +const SCALE_10000 = 10000 +const SCALE_25000 = 25000 +const SCALE_50000 = 50000 + +module.exports = { + method: 'POST', + path: '/product-1', + options: { + description: 'Generate Product 1 PDF', + handler: async (request, h) => { + try { + const { + polygon, + scale = SCALE_2500, + reference = '' + } = request.payload + const holdingComments = request.payload.holdingComments === 'true' + + const product1 = await getProduct1(polygon, reference, scale, holdingComments) + const date = new Date().toISOString() + return h + .response(product1) + .encoding('binary') + .type('application/pdf') + .header('content-disposition', `attachment; filename=flood-map-planning-${date}.pdf;`) + .header('X-XSS-Protection', '1; mode=block') + } catch (err) { + const message = err.message + console.log('error caught in product-1 route', err.message) + return Boom.badImplementation(message, err) + } + }, + validate: { + payload: Joi.object().keys({ + reference: Joi.string().allow('').max(MAX_REFERENCE_WIDTH).trim(), + scale: Joi.number().allow(SCALE_2500, SCALE_10000, SCALE_25000, SCALE_50000).required(), + polygon: Joi.string().required().allow(''), + holdingComments: Joi.string().allow('') + }) + } + } +} diff --git a/server/services/eaMaps/getProduct1.js b/server/services/eaMaps/getProduct1.js new file mode 100644 index 00000000..6d29b316 --- /dev/null +++ b/server/services/eaMaps/getProduct1.js @@ -0,0 +1,78 @@ +const { config } = require('../../../config') +const axios = require('axios') +const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept-Encoding': 'gzip, deflate, br' +} +const { makePolygonGeometry } = require('../agol') +const { getToken } = require('./getToken') + +const parseEaMapsProduct1Response = (response) => { + const { data } = response + const { results } = data + + if (!results || !Array.isArray(results)) { + const message = 'unexpected results from eaMaps generate pdf' + console.log(message, results) + throw new Error(message) + } + + return results.reduce((returnValues, resultEntry) => { + const { paramName, value } = resultEntry + if (paramName === 'pdfFile') { + returnValues.url = (value?.url) || undefined + } + if (paramName === 'error') { + returnValues.error = value + } + return returnValues + }, { url: undefined, error: undefined }) +} + +const getProduct1 = async (polygon, referenceNumber, scale, _holdingComments) => { + try { + const token = await getToken() + const pdfUrl = config.eamaps.serviceUrl + config.eamaps.product1EndPoint + const geometry = JSON.stringify(makePolygonGeometry(polygon)) + // _holdingComments is awaiting an implementation for FMP2 + + const formData = { + geometry, + referenceNumber, + scale, + token, + product: '1', + f: 'json' + } + // 1st post the data, which triggers the EAMaps process to produce a temporary pdf + // and returns the url of that pdf + const response = await axios.post(pdfUrl, formData, { + headers + }) + + const { url, error } = parseEaMapsProduct1Response(response) + + if (error) { + console.log('An error was returned from the eaMaps Product 1 service', error) + throw (error) + } + if (!url) { + const message = 'The eaMaps Product 1 service failed to return a url' + console.log(message) + throw new Error(message) + } + + // Now fetch and return the actual pdf stream using the returned url + const pdfStreamResponse = await axios.get(url, { + responseType: 'stream' + }) + + const { data: pdfData } = pdfStreamResponse + return pdfData + } catch (error) { + console.log('Error caught in getProduct1', error) + throw (error) + } +} + +module.exports = { getProduct1 } diff --git a/server/services/eaMaps/getToken.js b/server/services/eaMaps/getToken.js new file mode 100644 index 00000000..020b5516 --- /dev/null +++ b/server/services/eaMaps/getToken.js @@ -0,0 +1,57 @@ +const { config } = require('../../../config') +const axios = require('axios') + +const headers = { 'Content-Type': 'application/x-www-form-urlencoded' } + +const cachedToken = { + token: undefined, + expires: undefined +} + +// invalidateToken is exported for unit tests +const invalidateToken = () => { + cachedToken.token = undefined + cachedToken.expires = undefined +} + +// getToken should be wrapped in a try catch as refreshToken will throw +const getToken = async () => { + if (cachedToken.token && cachedToken.expires > new Date()) { + return cachedToken.token + } + invalidateToken() + await refreshToken() + return cachedToken.token +} + +const refreshToken = async () => { + const tokenUrl = config.eamaps.serviceUrl + config.eamaps.tokenEndPoint + const formData = { + username: config.eamaps.product1User, + password: config.eamaps.product1Password, + ip: null, + client: null, + f: 'json', + expiration: 60 + } + + try { + const response = await axios.post(tokenUrl, formData, { headers }) + const { data } = response + + const { token, expires, error } = data + if (error) { + const errorMessage = 'An error was returned attempting to get an EA Maps esri token' + console.log(errorMessage, JSON.stringify(error)) + throw (new Error(errorMessage)) + } + // Now save the token and expiry time + cachedToken.token = token + cachedToken.expires = expires + } catch (error) { + console.log('There was an error requesting an EA Maps esri token') + throw (error) + } +} + +module.exports = { getToken, invalidateToken } diff --git a/server/services/pdf-service.js b/server/services/pdf-service.js deleted file mode 100644 index c333c254..00000000 --- a/server/services/pdf-service.js +++ /dev/null @@ -1,12 +0,0 @@ -const util = require('../util') -const { config } = require('../../config') -const url = config.service + '/printservice/' - -module.exports = { - get: (easting, northing) => { - if (!easting || !northing) { - throw new Error('No point provided') - } - return util.getJson(url + easting + '/' + northing) - } -} diff --git a/server/views/flood-zone-results.html b/server/views/flood-zone-results.html index aa1853c2..3c872523 100644 --- a/server/views/flood-zone-results.html +++ b/server/views/flood-zone-results.html @@ -200,11 +200,8 @@

-
- - + -
diff --git a/test/config.js b/test/config.js index ea80e6e5..9538555f 100644 --- a/test/config.js +++ b/test/config.js @@ -10,6 +10,7 @@ lab.experiment('Ensure config is correct', () => { require('../config') }).not.to.throw() }) + lab.test('test config values', () => { const { config } = require('../config') const expectedConfig = { @@ -41,6 +42,13 @@ lab.experiment('Ensure config is correct', () => { localAuthorityEndPoint: '/Flood_Map_for_Planning_Query_Service_NON_PRODUCTION/FeatureServer/1', isEnglandEndPoint: '/Flood_Map_for_Planning_Query_Service_NON_PRODUCTION/FeatureServer/2', floodZonesRiversAndSeaEndPoint: '/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/FeatureServer/0' + }, + eamaps: { + serviceUrl: 'http://dummyEAMapslUrl', + product1User: 'PRODUCT1_USER', + product1Password: 'PRODUCT1_PASSWORD', + product1EndPoint: '/rest/services/FMfP/FMFPGetProduct1/GPServer/fmfp_get_product1/execute', + tokenEndPoint: '/tokens/generateToken' } } Code.expect(config).to.equal(expectedConfig) diff --git a/test/routes/pdf.js b/test/routes/pdf.js deleted file mode 100644 index 02805db6..00000000 --- a/test/routes/pdf.js +++ /dev/null @@ -1,101 +0,0 @@ -require('dotenv').config({ path: 'config/.env-example' }) -const Lab = require('@hapi/lab') -const Code = require('@hapi/code') -const lab = (exports.lab = Lab.script()) -const createServer = require('../../server') -const Wreck = require('@hapi/wreck') -const { config } = require('../../config') - -lab.experiment('PDF', () => { - let server - let restoreGetByPolygon - let restoreWreckPost - - lab.before(async () => { - server = await createServer() - await server.initialize() - restoreGetByPolygon = server.methods.getFloodZonesByPolygon - server.methods.getFloodZonesByPolygon = async () => ({ in_england: true }) - restoreWreckPost = Wreck.post - }) - - lab.after(async () => { - server.methods.getFloodZonesByPolygon = restoreGetByPolygon - Wreck.post = restoreWreckPost - await server.stop() - }) - - lab.test('a /pdf request without a payload should error', async () => { - const options = { - method: 'POST', - url: '/pdf' - } - const response = await server.inject(options) - Code.expect(response.statusCode).to.equal(302) - const { headers } = response - Code.expect(headers.location).to.equal('/') - }) - - const payloads = [ - ['empty-reference', { reference: '', scale: 2500, polygon: '', center: '[1, 1]' }], - ['test-reference', { reference: 'testRef', scale: 2500, polygon: '', center: '[1, 1]' }], - [ - 'with polygon', - { reference: 'testRef', scale: 2500, polygon: '[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]', center: '[1, 1]' } - ], - [ - 'with polygon without zone', - { reference: 'testRef', scale: 2500, polygon: '[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]', center: '[1, 1]' } - ], - ['without polygon without zone', { reference: 'testRef', scale: 2500, polygon: '', center: '[1, 1]' }] - ] - payloads.forEach(([testDescription, payload]) => { - lab.test( - `a /pdf request with a payload: ${testDescription} should call the geoserver print.pdf route`, - async () => { - const options = { - method: 'POST', - url: '/pdf', - payload - } - let printUrl - let postedPayload - - Wreck.post = async (url, options) => { - printUrl = url - postedPayload = options.payload - return {} - } - - const response = await server.inject(options) - Code.expect(response.statusCode).to.equal(204) - Code.expect(printUrl).to.equal(config.geoserver + '/geoserver/pdf/print.pdf') - Code.expect(postedPayload.reference).to.equal(payload.reference || '') - const { layers } = postedPayload - Code.expect(layers.length).to.equal(3) - const vector = layers[2] - const expectedGraphicOffset = payload.polygon ? undefined : -10.5 - Code.expect(vector.styles[''].graphicXOffset).to.equal(expectedGraphicOffset) - } - ) - }) - - /*eslint-disable */ - const postErrorFunctions = [ - async (url, options) => {throw new Error('testing error')}, - async (url, options) => {throw undefined}, - async (url, options) => {throw 'a string'}, - ] - /* eslint-enable */ - postErrorFunctions.forEach((postErrorFunction) => { - lab.test('a /pdf request should return an internal error if Wreck.post fails', async () => { - const payload = payloads[0][1] - const options = { method: 'POST', url: '/pdf', payload } - - Wreck.post = postErrorFunction - - const response = await server.inject(options) - Code.expect(response.statusCode).to.equal(500) - }) - }) -}) diff --git a/test/routes/product-1.js b/test/routes/product-1.js new file mode 100644 index 00000000..4f8a5daa --- /dev/null +++ b/test/routes/product-1.js @@ -0,0 +1,147 @@ +require('dotenv').config({ path: 'config/.env-example' }) +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const lab = (exports.lab = Lab.script()) +const createServer = require('../../server') +const axios = require('axios') +const { config } = require('../../config') +const { invalidateToken } = require('../../server/services/eaMaps/getToken') + +lab.experiment('product-1.js', () => { + let server + const AxiosMockAdapter = require('axios-mock-adapter') + const mockAxios = new AxiosMockAdapter(axios) + + const getProduct1Url = config.eamaps.serviceUrl + config.eamaps.product1EndPoint + const getTokenUrl = config.eamaps.serviceUrl + config.eamaps.tokenEndPoint + const mockPdfUrl = 'PDF_URL' + + const validPdfResponse = { + results: [ + { paramName: 'pdfFile', dataType: 'GPDataFile', value: { url: mockPdfUrl } }, + { paramName: 'error', dataType: 'GPString', value: '' } + ] + } + + const invalidPdfResponse = { + results: [ + { paramName: 'pdfFile', dataType: 'GPDataFile', value: null }, + { paramName: 'pdfFile', dataType: 'GPDataFile', value: {} }, // increase coverage + { paramName: 'error', dataType: 'GPString', value: 'ERR' }, + { paramName: 'anything', dataType: 'GPString', value: 'for test coverage only' } + ] + } + + lab.before(async () => { + server = await createServer() + await server.initialize() + }) + + lab.beforeEach(async () => { + invalidateToken() + mockAxios.reset() + mockAxios.onPost(getTokenUrl) + .reply(200, { token: 'XXX', expires: 999999999999999 }) + + mockAxios.onPost(getProduct1Url) + .reply(200, validPdfResponse) + + mockAxios.onGet(mockPdfUrl) + .reply(200, 'PDF_STREAM') + }) + + lab.after(async () => { + server = await createServer() + await server.stop() + mockAxios.restore() + }) + const product1Payload = { + polygon: '[[1,2], [3,4]]', + scale: 2500, + reference: '', + holdingComments: 'true' + } + + const injectedServerValues = { + method: 'POST', + url: '/product-1', + payload: product1Payload + } + + lab.test('product-1 route should return a PDF Stream', async () => { + const response = await server.inject(injectedServerValues) + Code.expect(response.statusCode).to.equal(200) + Code.expect(response.result).to.equal('PDF_STREAM') + }) + + lab.test('product-1 route should catch an error when returned by getToken', async () => { + mockAxios.onPost(getTokenUrl) + .reply(200, { token: 'XXX', expires: 99999999999, error: { message: 'Mocked Error' } }) + + const response = await server.inject(injectedServerValues) + Code.expect(response.statusCode).to.equal(500) + // NB The current response when an error occurs is a redirect to a generic error page + // FCRM-5373 has been raised to determine a better UX + }) + + lab.test('product-1 route should catch an error when returned by eamaps getProduct1', async () => { + mockAxios.onPost(getProduct1Url).reply(200, invalidPdfResponse) + + const response = await server.inject(injectedServerValues) + Code.expect(response.statusCode).to.equal(500) + // NB The current response when an error occurs is a redirect to a generic error page + // FCRM-5373 has been raised to determine a better UX + }) + + // server/services/eaMaps/getToken.js missing coverage on line(s): 15, 16, 40, 52-54 + const unexpectedResponses = [ + [{}, 'no results'], + [{ results: 'not an array' }, "results that aren't an array"], + [{ results: [] }, 'results that are an empty array'] + ] + unexpectedResponses.forEach(([getProduct1Response, description]) => { + lab.test(`product-1 route should catch an error when eamaps getProduct1 returns ${description}`, async () => { + mockAxios.onPost(getProduct1Url).reply(200, getProduct1Response) + + const response = await server.inject(injectedServerValues) + Code.expect(response.statusCode).to.equal(500) + // NB The current response when an error occurs is a redirect to a generic error page + // FCRM-5373 has been raised to determine a better UX + }) + }) + + // mockAxios.onPost(getTokenUrl) + // .reply(200, { token: 'XXX', expires: 99999999999 }) + const getMockHistoryCalls = (url, history) => history.filter((item) => item.url === url) + + lab.test("getToken should reuse the token if it hasn't expired", async () => { + const getTokenCallCount = () => getMockHistoryCalls(getTokenUrl, mockAxios.history.post).length + + Code.expect(getTokenCallCount()).to.equal(0) + await server.inject(injectedServerValues) + Code.expect(getTokenCallCount()).to.equal(1) + await server.inject(injectedServerValues) + Code.expect(getTokenCallCount()).to.equal(1) // Should Still Be One + }) + + lab.test('getToken should request a new token if it has expired', async () => { + mockAxios.onPost(getTokenUrl).reply(200, { token: 'XXX', expires: 0 }) + + const getTokenCallCount = () => getMockHistoryCalls(getTokenUrl, mockAxios.history.post).length + + Code.expect(getTokenCallCount()).to.equal(0) + await server.inject(injectedServerValues) + Code.expect(getTokenCallCount()).to.equal(1) + await server.inject(injectedServerValues) + Code.expect(getTokenCallCount()).to.equal(2) // Should Be Two + }) + + lab.test('product-1 route should catch an error when getToken fails', async () => { + mockAxios.onPost(getTokenUrl).reply(404, { token: 'XXX', expires: 0 }) + + const response = await server.inject(injectedServerValues) + Code.expect(response.statusCode).to.equal(500) + // NB The current response when an error occurs is a redirect to a generic error page + // FCRM-5373 has been raised to determine a better UX + }) +}) diff --git a/test/schema.js b/test/schema.js new file mode 100644 index 00000000..72dcd5ed --- /dev/null +++ b/test/schema.js @@ -0,0 +1,13 @@ +const Lab = require('@hapi/lab') +const lab = (exports.lab = Lab.script()) +const Code = require('@hapi/code') +const { validateSchema } = require('../config/schema') + +lab.experiment('validateSchema', () => { + // Note the rest of schema is covered by the tests in config.js + lab.test('validate schema should throw an error when an invalid schema is passed', async () => { + Code.expect(() => { + validateSchema({}) + }).to.throw() + }) +})