From b16693697cd1276b77f6d4d5b4bbb53c280415b7 Mon Sep 17 00:00:00 2001 From: Tedd <teddmason@gmail.com> Date: Tue, 21 Jan 2025 15:32:07 +0000 Subject: [PATCH 1/5] FCRM-5514 - return england-only route to service (#453) --- server/constants.js | 2 + server/routes/__tests__/england-only.spec.js | 52 ++++++++++++++++++++ server/routes/__tests__/location.spec.js | 26 +++++++++- server/routes/england-only.js | 4 +- server/routes/location.js | 40 +++++++++------ server/services/__tests__/is-england.spec.js | 16 +++--- server/services/is-england.js | 19 +++---- server/views/england-only.html | 9 ++-- 8 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 server/routes/__tests__/england-only.spec.js diff --git a/server/constants.js b/server/constants.js index 77d4866b..1fc61b46 100644 --- a/server/constants.js +++ b/server/constants.js @@ -3,6 +3,7 @@ const HOME = 'home' const TRIAGE = 'triage' const ABOUT_MAP = 'about-map' const LOCATION = 'location' +const ENGLAND_ONLY = 'england-only' const MAP = 'map' const RESULTS = 'results' const CONTACT = 'contact' @@ -18,6 +19,7 @@ const views = { TRIAGE, ABOUT_MAP, LOCATION, + ENGLAND_ONLY, MAP, RESULTS, CONTACT, diff --git a/server/routes/__tests__/england-only.spec.js b/server/routes/__tests__/england-only.spec.js new file mode 100644 index 00000000..2ad752b5 --- /dev/null +++ b/server/routes/__tests__/england-only.spec.js @@ -0,0 +1,52 @@ +const { submitGetRequest } = require('../../__test-helpers__/server') +const { assertCopy } = require('../../__test-helpers__/copy') +const constants = require('../../constants') + +const url = constants.routes.ENGLAND_ONLY + +describe('Triage Page', () => { + const getQueries = [{ + testName: 'Happy get: with postcode', + queryParams: { + easting: '123456', + northing: '123456', + locationDetails: 'Some address in Wales', + isPostCode: true, + placeOrPostcode: 'SY16 1AA' + }, + text: 'Your search for \'SY16 1AA\' has been placed in Some address in Wales' + }, { + testName: 'Happy get: with place', + queryParams: { + easting: '123456', + northing: '123456', + locationDetails: 'Some address in Wales', + isPostCode: false, + placeOrPostcode: 'Swansea' + }, + text: 'Your search for \'Swansea\' has been placed in Some address in Wales' + }, { + testName: 'Happy get: with NGR', + queryParams: { + easting: '123456', + northing: '123456', + nationalGridReference: 'SN 85981 88534' + }, + text: 'The location you submitted is not in England, or is only partly in England.' + }, { + testName: 'Happy get: with easting and northing', + queryParams: { + easting: '123456', + northing: '123456' + }, + text: 'The location you submitted is not in England, or is only partly in England.' + }] + getQueries.forEach(({ testName, queryParams, text }) => { + it(testName, async () => { + const response = await submitGetRequest({ url: `${url}?${new URLSearchParams(queryParams).toString()}` }, 'This service provides data for locations in England only') + document.body.innerHTML = response.payload + assertCopy('title', 'This service is for England only - Flood map for planning - GOV.UK') + assertCopy('#not-england-page > div > p:nth-child(2)', text) + }) + }) +}) diff --git a/server/routes/__tests__/location.spec.js b/server/routes/__tests__/location.spec.js index f4b61b42..f46a4a13 100644 --- a/server/routes/__tests__/location.spec.js +++ b/server/routes/__tests__/location.spec.js @@ -7,9 +7,17 @@ const { const ngrToBngService = require('../../services/ngr-to-bng') const addressService = require('../../services/address') const isValidNgrService = require('../../services/is-valid-ngr') -const url = '/location' +const constants = require('../../constants') +const url = constants.routes.LOCATION + +jest.mock('../../services/is-england') +const isEnglandService = require('../../services/is-england') describe('location route', () => { + beforeEach(() => { + isEnglandService.mockResolvedValue(true) + }) + it(`Should return success response and correct view for ${url}`, async () => { const response = await submitGetRequest({ url }, 'Find the location') document.body.innerHTML = response.payload @@ -550,4 +558,20 @@ describe('location route', () => { expect(response.payload).not.toContain('<a href="#easting">Enter an easting in the correct format</a>') expect(response.payload).toContain('<a href="#northing">Enter a northing in the correct format</a>') }) + + it('Should redirect to england-only if not isEngland', async () => { + const options = { + url, + payload: { + findby: 'eastingNorthing', + easting: '123456', + northing: '123456' + } + } + // Switch isEngland mock to be false + isEnglandService.mockResolvedValue(false) + + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual('/england-only?easting=123456&northing=123456') + }) }) diff --git a/server/routes/england-only.js b/server/routes/england-only.js index e66c87b9..034179be 100644 --- a/server/routes/england-only.js +++ b/server/routes/england-only.js @@ -1,6 +1,8 @@ +const constants = require('../constants') + module.exports = { method: 'GET', - path: '/england-only', + path: constants.routes.ENGLAND_ONLY, options: { description: 'That location is not in England', handler: async (request, h) => { diff --git a/server/routes/location.js b/server/routes/location.js index 7997603e..d3096069 100644 --- a/server/routes/location.js +++ b/server/routes/location.js @@ -1,30 +1,38 @@ +const constants = require('../constants') const addressService = require('../services/address') const isValidEastingNorthingService = require('../services/is-valid-easting-northing') const isValidNgrService = require('../services/is-valid-ngr') const ngrToBng = require('../services/ngr-to-bng') +const isEnglandService = require('../services/is-england') const handlers = { - get: async (_request, h) => h.view('location'), + get: async (_request, h) => h.view(constants.views.LOCATION), post: async (request, h) => { // Validate and process the payload - const { BNG, errorSummary } = await validatePayload(request.payload) + const { location, errorSummary } = await validatePayload(request.payload) // If any validation failed then return the location view with the captured validation error if (errorSummary.length > 0) { // Get the analytics object - return h.view('location', { + return h.view(constants.views.LOCATION, { ...request.payload, errorSummary, analyticsPageEvent: analyticsPageEvent(request.payload) }) } - return h.redirect(`/map?cz=${BNG.easting},${BNG.northing},15`) + // Check final location is in England + if (!await isEnglandService(location.easting, location.northing)) { + const queryString = new URLSearchParams(location).toString() + return h.redirect(`${constants.routes.ENGLAND_ONLY}?${queryString}`) + } + + return h.redirect(`${constants.routes.MAP}?cz=${location.easting},${location.northing},15`) } } const validatePayload = async payload => { - const BNG = {} + const location = {} const { findby, placeOrPostcode, @@ -35,11 +43,11 @@ const validatePayload = async payload => { const errorSummary = [] if (findby === 'placeOrPostcode') { - await validatePlace(BNG, placeOrPostcode, errorSummary) + await validatePlace(location, placeOrPostcode, errorSummary) } else if (findby === 'nationalGridReference') { - validateGridReference(BNG, nationalGridReference, errorSummary) + validateGridReference(location, nationalGridReference, errorSummary) } else if (findby === 'eastingNorthing') { - validateBNG(BNG, easting, northing, errorSummary) + validateBNG(location, easting, northing, errorSummary) } else { errorSummary.push({ text: 'Select a place or postcode, National Grid Reference (NGR) or an Easting and northing', @@ -48,12 +56,12 @@ const validatePayload = async payload => { } return { - BNG, + location, errorSummary } } -const validatePlace = async (BNG, placeOrPostcode, errorSummary) => { +const validatePlace = async (location, placeOrPostcode, errorSummary) => { placeOrPostcode = placeOrPostcode?.trim() || '' if (!validatePlaceOrPostcode(placeOrPostcode)) { errorSummary.push({ @@ -68,8 +76,11 @@ const validatePlace = async (BNG, placeOrPostcode, errorSummary) => { href: '#placeOrPostcode' }) } else { - BNG.easting = address[0].geometry_x - BNG.northing = address[0].geometry_y + location.easting = address[0].geometry_x + location.northing = address[0].geometry_y + location.locationDetails = address[0].locationDetails + location.isPostCode = address[0].isPostCode + location.placeOrPostcode = placeOrPostcode } } } @@ -80,6 +91,7 @@ const validateGridReference = (BNG, nationalGridReference, errorSummary) => { const convertedBNG = ngrToBng.convert(nationalGridReference) BNG.easting = convertedBNG.easting BNG.northing = convertedBNG.northing + BNG.nationalGridReference = nationalGridReference } else { errorSummary.push({ text: 'Enter a real National Grid Reference (NGR)', @@ -151,7 +163,7 @@ const analyticsPageEvent = payload => { module.exports = [ { method: 'GET', - path: '/location', + path: constants.routes.LOCATION, options: { description: 'Get location for a postcode, national grid reference, easting or northing' }, @@ -159,7 +171,7 @@ module.exports = [ }, { method: 'POST', - path: '/location', + path: constants.routes.LOCATION, options: { description: 'Get location for a postcode, national grid reference, easting or northing', handler: handlers.post diff --git a/server/services/__tests__/is-england.spec.js b/server/services/__tests__/is-england.spec.js index 50832559..dfef26e4 100644 --- a/server/services/__tests__/is-england.spec.js +++ b/server/services/__tests__/is-england.spec.js @@ -21,33 +21,33 @@ describe('is-england', () => { it('is-england without easting or northing should throw "No point provided"', async () => { const point = {} try { - await isEnglandService.get(point.easting, point.northing) + await isEnglandService(point.easting, point.northing) } catch (err) { - expect(isEnglandService.get).toThrow(Error, 'No point provided') + expect(isEnglandService).toThrow(Error, 'No point provided') } }) it('is-england without easting should throw "No point provided"', async () => { const point = { northing: 388244 } try { - await isEnglandService.get(point.easting, point.northing) + await isEnglandService(point.easting, point.northing) } catch (err) { - expect(isEnglandService.get).toThrow(Error, 'No point provided') + expect(isEnglandService).toThrow(Error, 'No point provided') } }) it('is-england without northing should throw "No point provided"', async () => { const point = { easting: 388244 } try { - await isEnglandService.get(point.easting, point.northing) + await isEnglandService(point.easting, point.northing) } catch (err) { - expect(isEnglandService.get).toThrow(Error, 'No point provided') + expect(isEnglandService).toThrow(Error, 'No point provided') } }) it('is-england with northing and easting should call esriRequest"', async () => { const point = { northing: 388244, easting: 388244 } - const response = await isEnglandService.get(point.easting, point.northing) - expect(response).toEqual({ is_england: true }) + const response = await isEnglandService(point.easting, point.northing) + expect(response).toEqual(true) }) }) diff --git a/server/services/is-england.js b/server/services/is-england.js index 50724515..79aaf780 100644 --- a/server/services/is-england.js +++ b/server/services/is-england.js @@ -1,15 +1,12 @@ const { config } = require('../../config') const { esriRequest, makePointGeometry } = require('./agol') -module.exports = { - get: (easting, northing) => { - if (!easting || !northing) { - throw new Error('No point provided') - } - return esriRequest(config.agol.isEnglandEndPoint, makePointGeometry(easting, northing), 'esriGeometryPoint') - .then((esriResult) => { - const isEngland = esriResult && Array.isArray(esriResult) && esriResult.length > 0 - return { is_england: isEngland } - }) - } // returns a Promise that resolves as { is_england: true | false } +module.exports = (easting, northing) => { + if (!easting || !northing) { + throw new Error('No point provided') + } + return esriRequest(config.agol.isEnglandEndPoint, makePointGeometry(easting, northing), 'esriGeometryPoint') + .then((esriResult) => { + return esriResult && Array.isArray(esriResult) && esriResult.length > 0 + }) } diff --git a/server/views/england-only.html b/server/views/england-only.html index ad1df691..6773c771 100644 --- a/server/views/england-only.html +++ b/server/views/england-only.html @@ -5,12 +5,9 @@ {% block content %} {{ super() }} - - - - <div id="not-england-page" class="grid-row"> - <div class="column-two-thirds"> - <h1 class="heading-xlarge">This service provides data for locations in England only</h1> + <div id="not-england-page" class="govuk-grid-row govuk-body"> + <div class="govuk-grid-column-two-thirds"> + <h1 class="govuk-heading-xl">This service provides data for locations in England only</h1> {% if locationDetails %} <p>Your search for '{{placeOrPostcode}}' has been placed in {{locationDetails}}</p> {% else %} From 182204fd7fe78096ffeaf0ac096c0f7757400ae8 Mon Sep 17 00:00:00 2001 From: Tedd <teddmason@gmail.com> Date: Tue, 21 Jan 2025 15:39:13 +0000 Subject: [PATCH 2/5] Feature/fcrm 5429 - Block duplicate p4 requests (#451) * WIP so far for a single p4 check * Switching p4 cookie check to multiple request check * sorting sonarcloud issues before unit test work * Adding tests for check-your-details route * review comments --- server/index.js | 1 + server/plugins/register-cookie.js | 9 +- .../__tests__/check-your-details.spec.js | 126 ++++++++++++++++++ server/routes/check-your-details.js | 57 ++++---- server/routes/flood-zone-results.js | 1 - server/services/__mocks__/address.js | 5 + server/services/agol/__mocks__/getContacts.js | 11 ++ 7 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 server/routes/__tests__/check-your-details.spec.js create mode 100644 server/services/__mocks__/address.js create mode 100644 server/services/agol/__mocks__/getContacts.js diff --git a/server/index.js b/server/index.js index 67fd16aa..902fe14b 100644 --- a/server/index.js +++ b/server/index.js @@ -43,6 +43,7 @@ async function createServer () { await server.register(require('./plugins/error-pages')) await server.register(require('blipp')) await server.register(require('./plugins/full-url')) + await server.register(require('./plugins/register-cookie')) server.ext('onPreResponse', async (request, h) => { request.response.header('cache-control', 'no-cache') diff --git a/server/plugins/register-cookie.js b/server/plugins/register-cookie.js index ea57caa1..5296591f 100644 --- a/server/plugins/register-cookie.js +++ b/server/plugins/register-cookie.js @@ -3,15 +3,14 @@ const { config } = require('../../config') exports.plugin = { name: 'register-cookie', register: (server, options) => { - // Cookie used to notify the client - // browser that the download is complete - server.state('pdf-download', { - ttl: 10000, + // Cookie used to store a successful p4 request to stop duplication + server.state('p4Request', { + ttl: null, // ttl is session isSecure: config.siteUrl.startsWith('https'), path: '/', isSameSite: false, isHttpOnly: true, - encoding: 'none', + encoding: 'base64json', clearInvalid: true, strictHeader: true }) diff --git a/server/routes/__tests__/check-your-details.spec.js b/server/routes/__tests__/check-your-details.spec.js new file mode 100644 index 00000000..0782c454 --- /dev/null +++ b/server/routes/__tests__/check-your-details.spec.js @@ -0,0 +1,126 @@ +const { getServer } = require('../../../.jest/setup') +const { assertCopy } = require('../../__test-helpers__/copy') +const { + submitGetRequest, + submitPostRequest +} = require('../../__test-helpers__/server') +const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock') +jest.mock('../../services/agol/getContacts') +jest.mock('../../services/address') +jest.mock('@hapi/wreck') +const wreck = require('@hapi/wreck') +const user = { + fullName: 'John Smith', + email: 'john.smith@email.com' +} + +const url = '/check-your-details' + +describe('Check your details page', () => { + beforeEach(() => { + wreck.post.mockResolvedValue({ + payload: { + applicationReferenceNumber: '12345', + nextTask: 'SEND_CONFIRMATION_EMAIL' + } + }) + }) + describe('GET', () => { + const floodZoneGets = [ + { polygon: mockPolygons.fz1_only, floodZone: '1' }, + { polygon: mockPolygons.fz2_only, floodZone: '2' }, + { polygon: mockPolygons.fz3_only, floodZone: '3' } + ] + floodZoneGets.forEach(({ polygon, floodZone }) => { + it(`Happy get request for a flood zone ${floodZone} information`, async () => { + const response = await submitGetRequest({ url: `${url}?polygon=${polygon}&fullName=${user.fullName}&recipientemail=${user.email}` }, 'Check your details before requesting your data') + document.body.innerHTML = response.payload + assertCopy('title', 'Check your details - Flood map for planning - GOV.UK') + assertCopy('.govuk-summary-list__row > dd.govuk-summary-list__value', user.fullName) + assertCopy('.govuk-summary-list__row:nth-child(2) > dd.govuk-summary-list__value', user.email) + assertCopy('.govuk-summary-list__row:nth-child(4) > dd.govuk-summary-list__value', floodZone) + }) + }) + }) + describe('POST', () => { + // Makes a first request with no cookie + const postTests = [ + { + description: 'Happy post: request with p4request made with no p4Request cookie', + payload: { + recipientemail: user.email, + fullName: user.fullName, + polygon: mockPolygons.fz1_only + }, + p4Cookie: {}, + expectedAppRef: '12345', + expectedZoneNumber: '1', + expectedWreckCalls: 1 + }, + { + description: 'Happy post: Attempts a 2nd duplicate request and wreck should not be called to make p4', + payload: { + recipientemail: user.email, + fullName: user.fullName, + polygon: mockPolygons.fz1_only + }, + p4Cookie: { [mockPolygons.fz1_only]: '12345' }, + expectedAppRef: '12345', + expectedZoneNumber: '1', + expectedWreckCalls: 0 + }, + { + description: 'Happy post: Attempts a 2nd non duplicate request and wreck should be called to make p4', + payload: { + recipientemail: user.email, + fullName: user.fullName, + polygon: mockPolygons.fz2_only + }, + p4Cookie: { [mockPolygons.fz1_only]: '12346' }, + expectedAppRef: '12345', + expectedZoneNumber: '2', + expectedWreckCalls: 1 + } + ] + + postTests.forEach(({ description, payload, p4Cookie, expectedAppRef, expectedZoneNumber, expectedWreckCalls }) => { + it(description, async () => { + const options = { url, payload } + + getServer().ext('onPreHandler', (request, h) => { + request.state = { p4Request: p4Cookie } + return h.continue + }) + + const queryParams = { + applicationReferenceNumber: expectedAppRef, + polygon: payload.polygon, + recipientemail: payload.recipientemail, + zoneNumber: expectedZoneNumber + } + + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(`/confirmation?${new URLSearchParams(queryParams).toString()}`) + expect(response.request.state.p4Request).toEqual(p4Cookie) + expect(wreck.post).toHaveBeenCalledTimes(expectedWreckCalls) + }) + }) + // Sad paths + // Wreck returns an error + it('Sad: Wreck returns an error for p4Request', async () => { + const options = { + url, + payload: { + recipientemail: 'john.smith@email.com', + fullName: 'John Smith', + polygon: mockPolygons.fz2_only + } + } + wreck.post.mockImplementation(() => { + throw new Error() + }) + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(`/order-not-submitted?polygon=${options.payload.polygon}`) + }) + }) +}) diff --git a/server/routes/check-your-details.js b/server/routes/check-your-details.js index 52bf5cd9..2b4a8748 100644 --- a/server/routes/check-your-details.js +++ b/server/routes/check-your-details.js @@ -12,7 +12,7 @@ const getFunctionAppResponse = async (data) => { ) payload.postcode = postcode - return wreck.post(publishToQueueURL, { payload: JSON.stringify(payload) }) + return wreck.post(publishToQueueURL, { json: true, payload: JSON.stringify(payload) }) } const floodZoneResultsToFloodZone = (floodZoneResults) => @@ -40,55 +40,60 @@ module.exports = [ options: { description: 'submits the page to Confirmation Screen', handler: async (request, h) => { - try { - const payload = request.payload || {} - const { recipientemail, fullName } = payload - const coordinates = getCentreOfPolygon(payload.polygon) + const payload = request.payload || {} + const { recipientemail, fullName, polygon } = payload + const coordinates = getCentreOfPolygon(polygon) + const floodZoneResults = await request.server.methods.getFloodZonesByPolygon(polygon) + const zoneNumber = floodZoneResultsToFloodZone(floodZoneResults) + let applicationReferenceNumber + // Check if p4Request is duplicate + if (!request.state?.p4Request?.[polygon]) { // Send details to function app - const plotSize = getAreaInHectares(payload.polygon) - const psoResults = await request.server.methods.getPsoContactsByPolygon(payload.polygon) - const floodZoneResults = await request.server.methods.getFloodZonesByPolygon(payload.polygon) - const zoneNumber = floodZoneResultsToFloodZone(floodZoneResults) + const plotSize = getAreaInHectares(polygon) + const psoResults = await request.server.methods.getPsoContactsByPolygon(polygon) const data = JSON.stringify({ name: fullName, recipientemail, x: coordinates.x, y: coordinates.y, - polygon: '[' + payload.polygon + ']', + polygon: `[${polygon}]`, zoneNumber, plotSize, areaName: psoResults.AreaName, psoEmailAddress: psoResults.EmailAddress, llfa: psoResults.LocalAuthorities || '' }) - let applicationReferenceNumber try { const result = await getFunctionAppResponse(data) - const response = result.payload.toString() - const { applicationReferenceNumber: appRef } = JSON.parse(response) - applicationReferenceNumber = appRef + applicationReferenceNumber = result.payload.applicationReferenceNumber + // Upsert p4Cookie to store app ref by polygon key + const p4Cookie = request.state.p4Request || {} + p4Cookie[polygon] = applicationReferenceNumber + h.state('p4Request', p4Cookie) } catch (error) { console.log( '\nFailed to POST these data to the functionsApp /order-product-four:\n', data ) console.log('Error\n', JSON.stringify(error)) - const redirectURL = `/order-not-submitted?polygon=${payload?.polygon}¢er=[${payload?.easting},${payload?.northing}]` + const redirectURL = `/order-not-submitted?polygon=${payload?.polygon}` return h.redirect(redirectURL) } + } else { + applicationReferenceNumber = request.state.p4Request[polygon] + } - // Forward details to confirmation page - const queryParams = { - applicationReferenceNumber, - polygon: payload.polygon, - recipientemail, - zoneNumber - } - // During serializing, the UTF-8 encoding format is used to encode any character that requires percent-encoding. - const query = new URLSearchParams(queryParams).toString() - return h.redirect(`/confirmation?${query}`) - } catch (error) {} + // Forward details to confirmation page + const queryParams = { + applicationReferenceNumber, + polygon: polygon, + recipientemail, + zoneNumber + } + // During serializing, the UTF-8 encoding format is used to encode any character that requires percent-encoding. + const query = new URLSearchParams(queryParams).toString() + return h.redirect(`/confirmation?${query}`) } } } diff --git a/server/routes/flood-zone-results.js b/server/routes/flood-zone-results.js index 5b12ace8..f6c2c3a3 100644 --- a/server/routes/flood-zone-results.js +++ b/server/routes/flood-zone-results.js @@ -89,7 +89,6 @@ module.exports = [ }) return h .view('flood-zone-results', floodZoneResultsData) - .unstate('pdf-download') } catch (err) { console.log('Error in flood zone results', err.message) return Boom.badImplementation(err.message, err) diff --git a/server/services/__mocks__/address.js b/server/services/__mocks__/address.js new file mode 100644 index 00000000..d536074a --- /dev/null +++ b/server/services/__mocks__/address.js @@ -0,0 +1,5 @@ +const getPostcodeFromEastingorNorthing = (easting, northing) => { + return 'M1 1AA' +} + +module.exports = { getPostcodeFromEastingorNorthing } diff --git a/server/services/agol/__mocks__/getContacts.js b/server/services/agol/__mocks__/getContacts.js new file mode 100644 index 00000000..aa46df5c --- /dev/null +++ b/server/services/agol/__mocks__/getContacts.js @@ -0,0 +1,11 @@ +const getContacts = () => { + return { + isEngland: true, + EmailAddress: 'inforequests.gmmc@environment-agency.gov.uk', + AreaName: 'Greater Manchester Merseyside and Cheshire', + LocalAuthorities: 'Cheshire West and Chester (B)', + useAutomatedService: true + } +} + +module.exports = { getContacts } From 56f7aed9779617d8eeee7864eba2a0cead9dc079 Mon Sep 17 00:00:00 2001 From: Mark Fee <mark.fee@defra.gov.uk> Date: Tue, 21 Jan 2025 17:45:19 +0000 Subject: [PATCH 3/5] FCRM-5476 Upgrade @defra-map to 0.4.0 (#452) * FCRM-5476 attempted an upgrade to 0.4.0 - getting invalid styles error * FCRM-5476 fixed issue causing map to fail if basemap had an incorrect value * FCRM-5476 refactor to avoid Sonar issue --- client/js/defra-map/index.js | 15 ++++++++++++++- package-lock.json | 28 ++++++++++++++-------------- package.json | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/client/js/defra-map/index.js b/client/js/defra-map/index.js index bb06b3f0..b678868f 100644 --- a/client/js/defra-map/index.js +++ b/client/js/defra-map/index.js @@ -69,7 +69,20 @@ const surfaceWaterStyleLayers = [ // 'Risk of Flooding from Surface Water Depth CCSW1 > 900mm/1', // 'Risk of Flooding from Surface Water Depth CCSW1 > 1200mm/1' // ] + +const fixLocalStorage = () => { + // Temp fix until 0.5.0 + // Map will not load if localStorage basemap is not one of default OR dark + // but 0.3.0 sets the value to 'default,light', which screws up + // the map component after an upgrade to 0.4.0 + const basemap = window.localStorage.getItem('basemap') + if (basemap !== 'default' && basemap !== 'dark') { + window.localStorage.removeItem('basemap') + } +} + getDefraMapConfig().then((defraMapConfig) => { + fixLocalStorage() // Temp fix until 0.5.0 const getVectorTileUrl = (layerName) => `${defraMapConfig.agolVectorTileUrl}/${layerName + defraMapConfig.layerNameSuffix}/VectorTileServer` const getFeatureLayerUrl = (layerName) => `${defraMapConfig.agolServiceUrl}/${layerName}/FeatureServer` const getModelFeatureLayerUrl = (layerName) => `${defraMapConfig.agolServiceUrl}/${layerName + defraMapConfig.layerNameSuffix}/FeatureServer` @@ -364,7 +377,7 @@ getDefraMapConfig().then((defraMapConfig) => { maxZoom: 20, centre: [340367, 322766], height: '100%', - hasGeoLocation: true, + hasGeoLocation: false, framework: 'esri', symbols: [symbols.waterStorageAreas, symbols.floodDefences, symbols.mainRivers], requestCallback: getRequest, diff --git a/package-lock.json b/package-lock.json index d1610f99..7e53fbda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.25.9", - "@defra/flood-map": "^0.3.0", + "@defra/flood-map": "^0.4.0", "@esri/arcgis-rest-feature-service": "^4.0.6", "@esri/arcgis-rest-request": "^4.2.3", "@hapi/boom": "^9.1.4", @@ -128,9 +128,9 @@ } }, "node_modules/@arcgis/core": { - "version": "4.31.3", - "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.31.3.tgz", - "integrity": "sha512-exz54+7asVBITYnKRC0RpJl/r3YG12ZC+togFuVJlfCzk9DO60qCp6XxU/7Zw0QojV20bQFkKjBvzPY0udEtiA==", + "version": "4.31.6", + "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.31.6.tgz", + "integrity": "sha512-+NSYtEJy/wRQcEX+f5OHBhg4MX26HeZFYghd7+VMTasalHUAD3y9PPiakLIfhahX/4EiSLuy25m1P93BBlybXg==", "dependencies": { "@esri/arcgis-html-sanitizer": "~4.1.0-next.4", "@esri/calcite-colors": "~6.1.0", @@ -1867,14 +1867,14 @@ "dev": true }, "node_modules/@defra/flood-map": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@defra/flood-map/-/flood-map-0.3.0.tgz", - "integrity": "sha512-qTMrz4LbXp7DcZsK46ETEdBZhZLuW7grZmOKh5Yfz2qaUqhLj8cZOhjBldyXaZjulSj5FEhWFkMeoSjMSeCY/Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@defra/flood-map/-/flood-map-0.4.0.tgz", + "integrity": "sha512-4Fx0zhpdA6CTEn5D3+e83Mg7n9yDmNffxR8u/JztpmeMezsGlyng4taXedQH3jL6iL/+CJ/b91cDgLj/DbCnsA==", "dependencies": { - "@arcgis/core": "^4.31.0", - "maplibre-gl": "^4.7.0", - "maplibre-gl-legacy": "npm:maplibre-gl@^1.15.0", - "preact": "^10.23.1" + "@arcgis/core": "^4.31.6", + "maplibre-gl": "^4.7.1", + "maplibre-gl-legacy": "npm:maplibre-gl@^1.15.3", + "preact": "^10.25.2" } }, "node_modules/@discoveryjs/json-ext": { @@ -16423,9 +16423,9 @@ "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "version": "10.25.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", + "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" diff --git a/package.json b/package.json index bf6aee56..7950c7ea 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.25.9", - "@defra/flood-map": "^0.3.0", + "@defra/flood-map": "^0.4.0", "@esri/arcgis-rest-feature-service": "^4.0.6", "@esri/arcgis-rest-request": "^4.2.3", "@hapi/boom": "^9.1.4", From dbf2b7df1e390b2589ccffdefcd2a4aa1bed7b3d Mon Sep 17 00:00:00 2001 From: Mark Fee <mark.fee@defra.gov.uk> Date: Wed, 22 Jan 2025 08:37:00 +0000 Subject: [PATCH 4/5] FCRM-5495 added appType internal/public and refactored tests (#454) * FCRM-5495 copy helper ammended to show failures better * FCRM-5495 /results now includes contacts and optin/optout status * FCRM-5495 moved getContacts.spec.js * FCRM-5495 removed server dependency from getContacts.spec.js * FCRM-5495 separated getContacts into getCustomerTeam and getLocalAuthority * FCRM-5495 added tests for getEsriToken * FCRM-5495 added tests for esriRequest * FCRM-5495 added tests for getCustomerTeam * FCRM-5495 added test coverage for server/services/agol/getContacts.js * FCRM-5495 covered make...geometry functions * FCRM-5495 added a config for fmpAppType * FCRM-5495 Results page honours the appType==internal check when showing the P4 button * FCRM-5495 added appType to the payload for requestProductFour --- .../@esri/arcgis-rest-feature-service.js | 15 ++++ __mocks__/@esri/arcgis-rest-request.js | 17 ++++ config/.env-example | 3 +- config/__tests__/config.spec.js | 3 +- config/index.js | 1 + config/schema.js | 1 + server/__test-helpers__/copy.js | 21 +++-- .../__tests__/check-your-details.spec.js | 26 +++++- server/routes/__tests__/results.spec.js | 31 ++++++- server/routes/check-your-details.js | 1 + server/routes/results.js | 6 +- server/services/__data__/mockPolygons.json | 3 + server/services/__tests__/agol.spec.js | 86 ------------------- server/services/__tests__/getContacts.spec.js | 32 ------- server/services/agol/__mocks__/getContacts.js | 42 +++++++-- .../agol/__mocks__/getCustomerTeam.js | 40 +++++++++ .../services/agol/__mocks__/getFloodZones.js | 1 + .../agol/__mocks__/getLocalAuthority.js | 30 +++++++ .../agol/__tests__/esriRequest.spec.js | 28 ++++++ .../services/agol/__tests__/geometry.spec.js | 25 ++++++ .../agol/__tests__/getContacts.spec.js | 35 ++++++++ .../agol/__tests__/getCustomerTeam.spec.js | 46 ++++++++++ .../agol/__tests__/getEsriToken.spec.js | 19 ++++ .../agol/__tests__/getLocalAuthority.spec.js | 33 +++++++ server/services/agol/esriRequest.js | 20 +++++ server/services/agol/getContacts.js | 48 +++-------- server/services/agol/getCustomerTeam.js | 34 ++++++++ server/services/agol/getLocalAuthority.js | 15 ++++ server/services/agol/index.js | 20 +---- server/views/results.html | 8 +- 30 files changed, 492 insertions(+), 198 deletions(-) create mode 100644 __mocks__/@esri/arcgis-rest-feature-service.js create mode 100644 __mocks__/@esri/arcgis-rest-request.js delete mode 100644 server/services/__tests__/agol.spec.js delete mode 100644 server/services/__tests__/getContacts.spec.js create mode 100644 server/services/agol/__mocks__/getCustomerTeam.js create mode 100644 server/services/agol/__mocks__/getLocalAuthority.js create mode 100644 server/services/agol/__tests__/esriRequest.spec.js create mode 100644 server/services/agol/__tests__/geometry.spec.js create mode 100644 server/services/agol/__tests__/getContacts.spec.js create mode 100644 server/services/agol/__tests__/getCustomerTeam.spec.js create mode 100644 server/services/agol/__tests__/getEsriToken.spec.js create mode 100644 server/services/agol/__tests__/getLocalAuthority.spec.js create mode 100644 server/services/agol/esriRequest.js create mode 100644 server/services/agol/getCustomerTeam.js create mode 100644 server/services/agol/getLocalAuthority.js diff --git a/__mocks__/@esri/arcgis-rest-feature-service.js b/__mocks__/@esri/arcgis-rest-feature-service.js new file mode 100644 index 00000000..6ec21882 --- /dev/null +++ b/__mocks__/@esri/arcgis-rest-feature-service.js @@ -0,0 +1,15 @@ +let expectedParameters +const queryFeatureSpy = { + expectParameters: (params) => { expectedParameters = params } +} + +const queryFeatures = async (requestObject) => { + if (expectedParameters) { + expect(requestObject).toEqual(expectedParameters) + } + return { + features: 'QUERY_FEATURES_RESPONSE' + } +} + +module.exports = { queryFeatures, queryFeatureSpy } diff --git a/__mocks__/@esri/arcgis-rest-request.js b/__mocks__/@esri/arcgis-rest-request.js new file mode 100644 index 00000000..20fad093 --- /dev/null +++ b/__mocks__/@esri/arcgis-rest-request.js @@ -0,0 +1,17 @@ + +const response = { + token: 'TEST_TOKEN', + refreshToken: () => { + response.token = 'REFRESHED_TOKEN' + return response.token + } +} +const _invalidateToken = () => { response.token = undefined } + +const _resetToken = () => { response.token = 'TEST_TOKEN' } + +const ApplicationCredentialsManager = { + fromCredentials: () => (response) +} + +module.exports = { ApplicationCredentialsManager, _invalidateToken, _resetToken } diff --git a/config/.env-example b/config/.env-example index 36db0a8e..6c6de2e2 100644 --- a/config/.env-example +++ b/config/.env-example @@ -1,6 +1,7 @@ ENV=dev HOST=0.0.0.0 PORT=8050 +fmpAppType=internal CONNECTIONSTRING=http://dummyuri ERRBIT_POST_ERRORS=false ERRBIT_KEY=replace_this @@ -19,7 +20,7 @@ ordnanceSurveyOsClientId=replace_this ordnanceSurveyOsClientSecret=replace_this ordnanceSurveyOsMapsKey=replace_this siteUrl=http://dummyuri -functionAppUrl==http://dummyuri +functionAppUrl=http://dummyuri ignoreUseAutomatedService=true placeApiUrl=http://dummyuri #AGOL diff --git a/config/__tests__/config.spec.js b/config/__tests__/config.spec.js index 90df3cb1..40add3ac 100644 --- a/config/__tests__/config.spec.js +++ b/config/__tests__/config.spec.js @@ -11,6 +11,7 @@ describe('Ensure config is correct', () => { const { config } = require('../index') const expectedConfig = { env: 'dev', + appType: 'internal', server: { port: '8050' }, geoserver: 'http://dummyuri', views: { isCached: false }, @@ -28,7 +29,7 @@ describe('Ensure config is correct', () => { osClientSecret: 'replace_this' }, siteUrl: 'http://dummyuri', - functionAppUrl: '=http://dummyuri', + functionAppUrl: 'http://dummyuri', ignoreUseAutomatedService: true, placeApi: { url: 'http://dummyuri' }, agol: { diff --git a/config/index.js b/config/index.js index a92f591e..01718a0d 100644 --- a/config/index.js +++ b/config/index.js @@ -4,6 +4,7 @@ require('./environment') const config = { env: process.env.ENV, + appType: process.env.fmpAppType, server: { port: process.env.PORT }, diff --git a/config/schema.js b/config/schema.js index 860686e5..4059fa0f 100644 --- a/config/schema.js +++ b/config/schema.js @@ -8,6 +8,7 @@ const serverSchema = Joi.object() const schema = Joi.object({ env: Joi.string().required(), + appType: Joi.string().required().allow('internal', 'public'), server: serverSchema, geoserver: Joi.string().uri().required(), logging: Joi.object(), diff --git a/server/__test-helpers__/copy.js b/server/__test-helpers__/copy.js index acde2708..d45a2132 100644 --- a/server/__test-helpers__/copy.js +++ b/server/__test-helpers__/copy.js @@ -1,12 +1,19 @@ - const assertCopy = (selector, expectedCopy) => { - const element = document.querySelector(selector) - if (expectedCopy) { + try { + const element = document.querySelector(selector) + if (expectedCopy) { // the REGEX trims all whitespace and only compares the actual text that will be rendered - expect(element.textContent.replace(/\s\s+/g, ' ').trim()) - .toContain(expectedCopy.replace(/\s\s+/g, ' ').trim()) - } else { - expect(element).toBeNull() + expect(element.textContent.replace(/\s\s+/g, ' ').trim()) + .toContain(expectedCopy.replace(/\s\s+/g, ' ').trim()) + } else { + expect(element).toBeNull() + } + } catch (error) { + // This Assert only runs when the expected assert fails. + // It is added as the exact location of the failure is hard to infer, when a fail occurs + // and this helps give a more meaningful failure message. + const failMessage = `expected selector ${selector} to contain ${expectedCopy || 'NO CONTENT'}` + expect(failMessage).toBeNull() } } diff --git a/server/routes/__tests__/check-your-details.spec.js b/server/routes/__tests__/check-your-details.spec.js index 0782c454..d5c81c7c 100644 --- a/server/routes/__tests__/check-your-details.spec.js +++ b/server/routes/__tests__/check-your-details.spec.js @@ -5,6 +5,7 @@ const { submitPostRequest } = require('../../__test-helpers__/server') const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock') +const { getCentreOfPolygon } = require('../../services/shape-utils') jest.mock('../../services/agol/getContacts') jest.mock('../../services/address') jest.mock('@hapi/wreck') @@ -25,6 +26,7 @@ describe('Check your details page', () => { } }) }) + describe('GET', () => { const floodZoneGets = [ { polygon: mockPolygons.fz1_only, floodZone: '1' }, @@ -42,6 +44,7 @@ describe('Check your details page', () => { }) }) }) + describe('POST', () => { // Makes a first request with no cookie const postTests = [ @@ -91,10 +94,11 @@ describe('Check your details page', () => { request.state = { p4Request: p4Cookie } return h.continue }) - + const { polygon } = payload + const { x, y } = getCentreOfPolygon(polygon) const queryParams = { applicationReferenceNumber: expectedAppRef, - polygon: payload.polygon, + polygon, recipientemail: payload.recipientemail, zoneNumber: expectedZoneNumber } @@ -103,6 +107,24 @@ describe('Check your details page', () => { expect(response.headers.location).toEqual(`/confirmation?${new URLSearchParams(queryParams).toString()}`) expect(response.request.state.p4Request).toEqual(p4Cookie) expect(wreck.post).toHaveBeenCalledTimes(expectedWreckCalls) + if (expectedWreckCalls) { + const expectedPayload = JSON.stringify({ + appType: 'internal', + name: user.fullName, + recipientemail: user.email, + x, + y, + polygon: `[${polygon}]`, + zoneNumber: expectedZoneNumber, + plotSize: '0', + areaName: 'Yorkshire', + psoEmailAddress: 'neyorkshire@environment-agency.gov.uk', + llfa: 'North Yorkshire', + postcode: 'M1 1AA' + }) + + expect(wreck.post).toHaveBeenCalledWith('http://dummyuri/order-product-four', { json: true, payload: expectedPayload }) + } }) }) // Sad paths diff --git a/server/routes/__tests__/results.spec.js b/server/routes/__tests__/results.spec.js index 4b664bde..66c9a8d4 100644 --- a/server/routes/__tests__/results.spec.js +++ b/server/routes/__tests__/results.spec.js @@ -1,6 +1,8 @@ const { submitGetRequest } = require('../../__test-helpers__/server') const { assertCopy } = require('../../__test-helpers__/copy') const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock') +const { config } = require('../../../config') +jest.mock('../../services/agol/getContacts') const url = '/results' @@ -16,12 +18,19 @@ const assertRiskAdminCopy = (expected) => { ) } -describe('Results Page', () => { +const assertOrderFloodRiskDataButton = (expected = true) => { + assertCopy('[data-testid="order-product4"]', expected && 'Order flood risk data') +} + +describe('Results Page On Public', () => { + beforeAll(() => { config.appType = 'public' }) + afterAll(() => { config.appType = 'internal' }) it('should have the correct copy for Zone 1"', async () => { const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.fz1_only}` }) document.body.innerHTML = response.payload assertFloodZoneCopy(1) assertRiskAdminCopy(false) + assertOrderFloodRiskDataButton() }) it('should have the correct copy for Zone 1 with riskAdmin"', async () => { @@ -29,5 +38,25 @@ describe('Results Page', () => { document.body.innerHTML = response.payload assertFloodZoneCopy(1) assertRiskAdminCopy(true) + assertOrderFloodRiskDataButton() + }) + + it('should not show the "Order flood risk data" for opted out areas', async () => { + const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.optedOut.fz3_only}` }) + document.body.innerHTML = response.payload + assertFloodZoneCopy(3) + assertRiskAdminCopy(false) + assertOrderFloodRiskDataButton(false) + }) +}) + +describe('Results Page On Internal', () => { + beforeAll(() => { config.appType = 'internal' }) + it('should show the "Order flood risk data" for opted out areas on internal', async () => { + const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.optedOut.fz3_only}` }) + document.body.innerHTML = response.payload + assertFloodZoneCopy(3) + assertRiskAdminCopy(false) + assertOrderFloodRiskDataButton(true) }) }) diff --git a/server/routes/check-your-details.js b/server/routes/check-your-details.js index 2b4a8748..d18384d4 100644 --- a/server/routes/check-your-details.js +++ b/server/routes/check-your-details.js @@ -53,6 +53,7 @@ module.exports = [ const plotSize = getAreaInHectares(polygon) const psoResults = await request.server.methods.getPsoContactsByPolygon(polygon) const data = JSON.stringify({ + appType: config.appType, name: fullName, recipientemail, x: coordinates.x, diff --git a/server/routes/results.js b/server/routes/results.js index bb132063..29d43f0c 100644 --- a/server/routes/results.js +++ b/server/routes/results.js @@ -1,3 +1,5 @@ +const { config } = require('../../config') + module.exports = [ { method: 'GET', @@ -6,8 +8,10 @@ module.exports = [ description: 'Results Page', handler: async (request, h) => { const { polygon } = request.query + const contactData = await request.server.methods.getPsoContactsByPolygon(polygon) + const showOrderProduct4Button = config.appType === 'internal' || contactData.useAutomatedService === true const floodData = await request.server.methods.getFloodZonesByPolygon(polygon) - return h.view('results', { polygon, floodData }) + return h.view('results', { polygon, floodData, contactData, showOrderProduct4Button }) } } } diff --git a/server/services/__data__/mockPolygons.json b/server/services/__data__/mockPolygons.json index 89acc8c0..4c0f153a 100644 --- a/server/services/__data__/mockPolygons.json +++ b/server/services/__data__/mockPolygons.json @@ -10,5 +10,8 @@ "fz3_only": "[[1323,1323],[1323,1324],[1324,1324],[1324,1323],[1323,1323]]", "fz2_and_3": "[[1333,1333],[1333,1334],[1334,1334],[1334,1333],[1333,1333]]", "throws": "199999" + }, + "optedOut": { + "fz3_only": "[[9323,1323],[9323,1324],[9324,1324],[9324,1323],[9323,1323]]" } } diff --git a/server/services/__tests__/agol.spec.js b/server/services/__tests__/agol.spec.js deleted file mode 100644 index ee1a4a53..00000000 --- a/server/services/__tests__/agol.spec.js +++ /dev/null @@ -1,86 +0,0 @@ -it('commented out as needs sorting at later date', () => { - expect(true) -}) - -// const sinon = require('sinon') -// const proxyquire = require('proxyquire') - -// describe('agol.js', () => { -// let agol -// let stubQueryFeatures -// let stubFromCredentials -// let stubRefreshToken -// let stubTokenGetter - -// beforeAll(async () => { -// stubQueryFeatures = sinon.stub().resolves({ features: {} }) -// stubRefreshToken = sinon.stub().returns('dummy_token_refreshed') - -// stubTokenGetter = sinon.stub() -// let callCount = 0 -// stubTokenGetter.onCall(callCount++).returns('dummy_token_initial') -// stubTokenGetter.onCall(callCount++).returns('dummy_token_initial') -// stubTokenGetter.onCall(callCount++).returns(undefined) -// stubTokenGetter.returns('dummy_token_persisted') - -// stubFromCredentials = sinon.stub() -// const appCredentialsManagerValue = { -// token: undefined, -// refreshToken: stubRefreshToken -// } - -// sinon.stub(appCredentialsManagerValue, 'token').get(stubTokenGetter) - -// stubFromCredentials.returns(appCredentialsManagerValue) - -// stubFromCredentials.onCall(1).returns({ -// token: 'dummy_token_2' -// }) - -// const { getEsriToken } = proxyquire('../agol/getEsriToken', { -// '@esri/arcgis-rest-request': { ApplicationCredentialsManager: { fromCredentials: stubFromCredentials } } -// }) - -// agol = proxyquire('../agol', { -// './getEsriToken': { getEsriToken }, -// '@esri/arcgis-rest-feature-service': { queryFeatures: stubQueryFeatures } -// }) -// }) - -// afterAll(async () => { -// sinon.restore() -// }) - -// it('esriRequest should call fromCredentials and refreshToken once when 2 esriRequest are made', async () => { -// await agol.esriRequest('/endPoint1') -// expect(stubFromCredentials.calledOnce).toBeTruthy() // Expect One Call and no further calls -// expect(stubQueryFeatures.callCount).toEqual(1) -// expect(stubRefreshToken.callCount).toEqual(0) // Should use the initial token getter -// expect(stubTokenGetter.callCount).toEqual(2) // Once to test the value and once to return the value - -// // Test that the expected params get passed to queryFeatures -// const expectedParameters = { -// url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endPoint1', -// geometry: undefined, -// geometryType: undefined, -// spatialRel: 'esriSpatialRelIntersects', -// returnGeometry: 'false', -// authentication: 'dummy_token_initial', -// outFields: '*' -// } -// expect(stubQueryFeatures.getCall(0).args[0]).toEqual(expectedParameters) - -// await agol.esriRequest('/endPoint2') -// expect(stubFromCredentials.calledOnce).toBeTruthy() // Should not have been called again -// expect(stubQueryFeatures.callCount).toEqual(2) // Should have been be called again -// expect(stubRefreshToken.callCount).toEqual(1) // Should not be called again - -// // Test that the modified expected params get passed to queryFeatures -// Object.assign(expectedParameters, { -// url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endPoint2', -// authentication: 'dummy_token_refreshed' -// }) - -// expect(stubQueryFeatures.getCall(1).args[0]).toEqual(expectedParameters) -// }) -// }) diff --git a/server/services/__tests__/getContacts.spec.js b/server/services/__tests__/getContacts.spec.js deleted file mode 100644 index 4e482043..00000000 --- a/server/services/__tests__/getContacts.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -const { mockEsriRequest, stopMockingEsriRequests } = require('./__mocks__/agol') -const createServer = require('../../../server') - -describe('getContacts', () => { - let server - beforeAll(async () => { - mockEsriRequest() - server = await createServer() - await server.initialize() - }) - - afterAll(async () => { - await server.stop() - stopMockingEsriRequests() - }) - - const errorsToTest = [ - ['false', false], - ['a non array', 'not-array'] - ] - errorsToTest.forEach(([titleText, returnValue]) => { - it(`if esriRequest returns ${titleText} expect an error`, async () => { - mockEsriRequest(returnValue) - const { getContacts } = require('../../services/agol/getContacts') - try { - await getContacts({ geometryType: 'esriGeometryPolygon', polygon: [[1, 2], [3, 4]] }) - } catch (error) { - expect(error.message).toEqual('Invalid response from AGOL customerTeam request') - } - }) - }) -}) diff --git a/server/services/agol/__mocks__/getContacts.js b/server/services/agol/__mocks__/getContacts.js index aa46df5c..29b9c8f9 100644 --- a/server/services/agol/__mocks__/getContacts.js +++ b/server/services/agol/__mocks__/getContacts.js @@ -1,10 +1,38 @@ -const getContacts = () => { - return { - isEngland: true, - EmailAddress: 'inforequests.gmmc@environment-agency.gov.uk', - AreaName: 'Greater Manchester Merseyside and Cheshire', - LocalAuthorities: 'Cheshire West and Chester (B)', - useAutomatedService: true +const mockPolygons = require('../../__data__/mockPolygons.json') + +const optedOutResponse = { + isEngland: true, + EmailAddress: 'wessexenquiries@environment-agency.gov.uk', + AreaName: 'Wessex', + LocalAuthorities: 'Bath and North East Somerset', + useAutomatedService: false +} + +const optedInResponse = { + isEngland: true, + EmailAddress: 'neyorkshire@environment-agency.gov.uk', + AreaName: 'Yorkshire', + LocalAuthorities: 'North Yorkshire', + useAutomatedService: true +} + +const getContacts = async (options = {}) => { + switch (options.polygon) { + case mockPolygons.optedOut.fz3_only: + return optedOutResponse + case mockPolygons.fz1_only: + case mockPolygons.inRiskAdmin.fz1_only: + case mockPolygons.inRiskAdmin.throws: + case mockPolygons.fz2_only: + case mockPolygons.inRiskAdmin.fz2_only: + case mockPolygons.fz3_only: + case mockPolygons.inRiskAdmin.fz3_only: + case mockPolygons.fz2_and_3: + case mockPolygons.inRiskAdmin.fz2_and_3: + return optedInResponse + default: { + throw new Error(`Error - No Polygon Mocked for getContacts- ${JSON.stringify(options.polygon)}`) + } } } diff --git a/server/services/agol/__mocks__/getCustomerTeam.js b/server/services/agol/__mocks__/getCustomerTeam.js new file mode 100644 index 00000000..7ad46964 --- /dev/null +++ b/server/services/agol/__mocks__/getCustomerTeam.js @@ -0,0 +1,40 @@ +const mockPolygons = require('../../__data__/mockPolygons.json') + +const optedOutResponse = { + isEngland: true, + EmailAddress: 'wessexenquiries@environment-agency.gov.uk', + AreaName: 'Wessex', + useAutomatedService: false +} + +const optedInResponse = { + isEngland: true, + EmailAddress: 'neyorkshire@environment-agency.gov.uk', + AreaName: 'Yorkshire', + useAutomatedService: true +} + +const getCustomerTeam = async (options = {}) => { + if (options.geometryType === 'esriGeometryPoint') { + return optedOutResponse + } + switch (options.polygon) { + case mockPolygons.optedOut.fz3_only: + return optedOutResponse + case mockPolygons.fz1_only: + case mockPolygons.inRiskAdmin.fz1_only: + case mockPolygons.inRiskAdmin.throws: + case mockPolygons.fz2_only: + case mockPolygons.inRiskAdmin.fz2_only: + case mockPolygons.fz3_only: + case mockPolygons.inRiskAdmin.fz3_only: + case mockPolygons.fz2_and_3: + case mockPolygons.inRiskAdmin.fz2_and_3: + return optedInResponse + default: { + throw new Error(`Error - No Polygon Mocked for getCustomerTeam- ${JSON.stringify(options.polygon)}`) + } + } +} + +module.exports = { getCustomerTeam } diff --git a/server/services/agol/__mocks__/getFloodZones.js b/server/services/agol/__mocks__/getFloodZones.js index 91291ed5..dcecb8da 100644 --- a/server/services/agol/__mocks__/getFloodZones.js +++ b/server/services/agol/__mocks__/getFloodZones.js @@ -11,6 +11,7 @@ const getFloodZones = async (options) => { return { floodZone: '2', floodzone_2: true, floodzone_3: false } case mockPolygons.fz3_only: case mockPolygons.inRiskAdmin.fz3_only: + case mockPolygons.optedOut.fz3_only: return { floodZone: '3', floodzone_2: false, floodzone_3: true } case mockPolygons.fz2_and_3: case mockPolygons.inRiskAdmin.fz2_and_3: diff --git a/server/services/agol/__mocks__/getLocalAuthority.js b/server/services/agol/__mocks__/getLocalAuthority.js new file mode 100644 index 00000000..d9aa3c69 --- /dev/null +++ b/server/services/agol/__mocks__/getLocalAuthority.js @@ -0,0 +1,30 @@ +const mockPolygons = require('../../__data__/mockPolygons.json') + +const bathResponse = { LocalAuthorities: 'Bath and North East Somerset' } +const yorkshireResponse = { LocalAuthorities: 'North Yorkshire' } +const wessexResponse = { LocalAuthorities: 'Winchester' } + +const getLocalAuthority = async (options = {}) => { + if (options.geometryType === 'esriGeometryPoint') { + return wessexResponse + } + switch (options.polygon) { + case mockPolygons.optedOut.fz3_only: + return bathResponse + case mockPolygons.fz1_only: + case mockPolygons.inRiskAdmin.fz1_only: + case mockPolygons.inRiskAdmin.throws: + case mockPolygons.fz2_only: + case mockPolygons.inRiskAdmin.fz2_only: + case mockPolygons.fz3_only: + case mockPolygons.inRiskAdmin.fz3_only: + case mockPolygons.fz2_and_3: + case mockPolygons.inRiskAdmin.fz2_and_3: + return yorkshireResponse + default: { + throw new Error(`Error - No Polygon Mocked for getLocalAuthority- ${JSON.stringify(options.polygon)}`) + } + } +} + +module.exports = { getLocalAuthority } diff --git a/server/services/agol/__tests__/esriRequest.spec.js b/server/services/agol/__tests__/esriRequest.spec.js new file mode 100644 index 00000000..80d14f90 --- /dev/null +++ b/server/services/agol/__tests__/esriRequest.spec.js @@ -0,0 +1,28 @@ +const { esriRequest } = require('../esriRequest') +const { queryFeatureSpy } = require('@esri/arcgis-rest-feature-service') + +const geometry = { + rings: [[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]], + spatialReference: { wkid: 27700 } +} + +describe('esriRequest', () => { + it('should respond with the mocked object', async () => { + const response = await esriRequest('/endpoint', geometry, 'esriGeometryPolygon') + expect(response).toEqual('QUERY_FEATURES_RESPONSE') + }) + + it('should call queryFeatures with the expected object', async () => { + queryFeatureSpy.expectParameters({ + url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endpoint', + geometry, + geometryType: 'esriGeometryPolygon', + spatialRel: 'esriSpatialRelIntersects', + returnGeometry: 'false', + authentication: 'TEST_TOKEN', + outFields: '*' + }) + const response = await esriRequest('/endpoint', geometry, 'esriGeometryPolygon') + expect(response).toEqual('QUERY_FEATURES_RESPONSE') + }) +}) diff --git a/server/services/agol/__tests__/geometry.spec.js b/server/services/agol/__tests__/geometry.spec.js new file mode 100644 index 00000000..e80da8fd --- /dev/null +++ b/server/services/agol/__tests__/geometry.spec.js @@ -0,0 +1,25 @@ +const { makePointGeometry, makePolygonGeometry } = require('../') + +describe('makePointGeometry', () => { + it('should form a valid esriPointGeometry object', async () => { + expect(makePointGeometry(17, 19)).toEqual({ x: 17, y: 19, spatialReference: { wkid: 27700 } }) + }) +}) + +describe('makePolygonGeometry', () => { + it('should form a valid esriPolygonGeometry object from an array', async () => { + expect(makePolygonGeometry([[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]])) + .toEqual({ + rings: [[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]], + spatialReference: { wkid: 27700 } + }) + }) + + it('should form a valid esriPolygonGeometry object from an array in string form', async () => { + expect(makePolygonGeometry('[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]')) + .toEqual({ + rings: [[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]], + spatialReference: { wkid: 27700 } + }) + }) +}) diff --git a/server/services/agol/__tests__/getContacts.spec.js b/server/services/agol/__tests__/getContacts.spec.js new file mode 100644 index 00000000..7ac1158d --- /dev/null +++ b/server/services/agol/__tests__/getContacts.spec.js @@ -0,0 +1,35 @@ +const { getContacts } = require('../getContacts') +const mockPolygons = require('../../__data__/mockPolygons.json') +jest.mock('../getCustomerTeam') +jest.mock('../getLocalAuthority') + +describe('getContacts', () => { + it('should return the combined contents of getCustomerTeam and getLocalAuthority for a polygon', async () => { + const response = await getContacts({ + geometryType: 'esriGeometryPolygon', + polygon: mockPolygons.fz1_only + }) + expect(response).toEqual({ + LocalAuthorities: 'North Yorkshire', + isEngland: true, + EmailAddress: 'neyorkshire@environment-agency.gov.uk', + AreaName: 'Yorkshire', + useAutomatedService: true + }) + }) + + it('should return the combined contents of getCustomerTeam and getLocalAuthority for a point', async () => { + const response = await getContacts({ + geometryType: 'esriGeometryPoint', + x: 123, + y: 456 + }) + expect(response).toEqual({ + LocalAuthorities: 'Winchester', + isEngland: true, + EmailAddress: 'wessexenquiries@environment-agency.gov.uk', + AreaName: 'Wessex', + useAutomatedService: false + }) + }) +}) diff --git a/server/services/agol/__tests__/getCustomerTeam.spec.js b/server/services/agol/__tests__/getCustomerTeam.spec.js new file mode 100644 index 00000000..175e890d --- /dev/null +++ b/server/services/agol/__tests__/getCustomerTeam.spec.js @@ -0,0 +1,46 @@ +const { mockEsriRequest, stopMockingEsriRequests, mockEsriRequestWithThrow } = require('../../__tests__/__mocks__/agol') +const { getCustomerTeam } = require('../getCustomerTeam') + +describe('getCustomerTeam', () => { + beforeEach(async () => { + mockEsriRequest([{ + attributes: { + contact_email: 'neyorkshire@environment-agency.gov.uk', + area_name_1: 'Environment Agency team in Yorkshire', + use_automated_service: true + } + }]) + }) + + afterEach(async () => { + stopMockingEsriRequests() + }) + + it('should assign values as expected', async () => { + const response = await getCustomerTeam({}) + expect(response).toEqual({ + isEngland: true, + EmailAddress: 'neyorkshire@environment-agency.gov.uk', + AreaName: 'Environment Agency team in Yorkshire', + useAutomatedService: true + }) + }) + + it('should throw if EsriRequest throws', async () => { + mockEsriRequestWithThrow() + try { + await getCustomerTeam({}) + } catch (error) { + expect(error.message).toEqual('mocked error') + } + }) + + it('should throw if EsriRequest is invalid', async () => { + mockEsriRequest('NOT AN ARRAY') + try { + await getCustomerTeam({}) + } catch (error) { + expect(error.message).toEqual('Invalid response from AGOL customerTeam request') + } + }) +}) diff --git a/server/services/agol/__tests__/getEsriToken.spec.js b/server/services/agol/__tests__/getEsriToken.spec.js new file mode 100644 index 00000000..d6f19150 --- /dev/null +++ b/server/services/agol/__tests__/getEsriToken.spec.js @@ -0,0 +1,19 @@ +const { getEsriToken } = require('../getEsriToken') +const { _invalidateToken, _resetToken } = require('@esri/arcgis-rest-request') + +describe('getEsriToken', () => { + afterEach(_resetToken) + + it('should return TEST_TOKEN', async () => { + expect(await getEsriToken()).toEqual('TEST_TOKEN') + }) + + it('should return REFRESHED_TOKEN after token is invalidated', async () => { + _invalidateToken() + expect(await getEsriToken()).toEqual('REFRESHED_TOKEN') + }) + + it('should return TEST_TOKEN again', async () => { + expect(await getEsriToken()).toEqual('TEST_TOKEN') + }) +}) diff --git a/server/services/agol/__tests__/getLocalAuthority.spec.js b/server/services/agol/__tests__/getLocalAuthority.spec.js new file mode 100644 index 00000000..d2237b32 --- /dev/null +++ b/server/services/agol/__tests__/getLocalAuthority.spec.js @@ -0,0 +1,33 @@ +const { mockEsriRequest, mockEsriRequestWithThrow, stopMockingEsriRequests } = require('../../__tests__/__mocks__/agol') +const { getLocalAuthority } = require('../getLocalAuthority') + +describe('getLocalAuthority', () => { + beforeEach(async () => { + mockEsriRequest([{ attributes: { authority_name: 'Ryedale' } }]) + }) + + afterEach(stopMockingEsriRequests) + + it('should assign values as expected', async () => { + const response = await getLocalAuthority({}) + expect(response).toEqual({ LocalAuthorities: 'Ryedale' }) + }) + + it('should throw if EsriRequest throws', async () => { + mockEsriRequestWithThrow() + try { + await getLocalAuthority({}) + } catch (error) { + expect(error.message).toEqual('mocked error') + } + }) + + it('should throw if EsriRequest is invalid', async () => { + mockEsriRequest('NOT AN ARRAY') + try { + await getLocalAuthority({}) + } catch (error) { + expect(error.message).toEqual('Invalid response from AGOL localAuthority request') + } + }) +}) diff --git a/server/services/agol/esriRequest.js b/server/services/agol/esriRequest.js new file mode 100644 index 00000000..8590ee11 --- /dev/null +++ b/server/services/agol/esriRequest.js @@ -0,0 +1,20 @@ +const { config } = require('../../../config') +const { queryFeatures } = require('@esri/arcgis-rest-feature-service') +const { getEsriToken } = require('./getEsriToken') + +const esriRequest = async (endPoint, geometry, geometryType) => { + const esriToken = await getEsriToken() + const requestObject = { + url: `${config.agol.serviceUrl}${endPoint}`, + geometry, + geometryType, + spatialRel: 'esriSpatialRelIntersects', + returnGeometry: 'false', + authentication: esriToken, + outFields: '*' + } + const result = await queryFeatures(requestObject) + return result.features +} + +module.exports = { esriRequest } diff --git a/server/services/agol/getContacts.js b/server/services/agol/getContacts.js index 9c46e70b..28c7b657 100644 --- a/server/services/agol/getContacts.js +++ b/server/services/agol/getContacts.js @@ -1,45 +1,17 @@ -const { config } = require('../../../config') -const { esriRequest, makePointGeometry, makePolygonGeometry } = require('./') +const { makePointGeometry, makePolygonGeometry } = require('./') +const { getCustomerTeam } = require('./getCustomerTeam') +const { getLocalAuthority } = require('./getLocalAuthority') -const getContacts = async (options = {}) => { - const geometry = options.geometryType === 'esriGeometryPolygon' +const getContacts = async (options) => { + options.geometry = options.geometryType === 'esriGeometryPolygon' ? makePolygonGeometry(options.polygon) : makePointGeometry(options.x, options.y) - - const response = { - isEngland: false, - EmailAddress: '', - AreaName: '', - LocalAuthorities: '', - useAutomatedService: false - } return await Promise.all([ - esriRequest(config.agol.customerTeamEndPoint, geometry, options.geometryType) - .then((esriResult) => { - // Note This request WILL NOT return data for areas that are outside of England - if (!esriResult || !Array.isArray(esriResult)) { - throw new Error('Invalid response from AGOL customerTeam request') - } - response.isEngland = esriResult.length > 0 - const { attributes } = esriResult[0] - // console.log('\ncustomerTeam Results ', esriResult) - Object.assign(response, { - EmailAddress: attributes.contact_email, - AreaName: attributes.area_name_1, // This will change back to area_name once Paul fixes the data. - useAutomatedService: Boolean(attributes.use_automated_service) - }) - }), - esriRequest(config.agol.localAuthorityEndPoint, geometry, options.geometryType) - .then((esriResult) => { - // Note This request WILL return data for areas that are outside of England (Paul is going to fix that) - if (!esriResult || !Array.isArray(esriResult)) { - throw new Error('Invalid response from AGOL localAuthority request') - } - const { attributes } = esriResult[0] - // console.log('\nlocalAuthority Results ', esriResult) - Object.assign(response, { LocalAuthorities: attributes.authority_name }) - }) - ]).then(() => response) + getCustomerTeam(options), + getLocalAuthority(options) + ]).then((responseArray) => { + return Object.assign({}, ...responseArray) + }) } module.exports = { getContacts } diff --git a/server/services/agol/getCustomerTeam.js b/server/services/agol/getCustomerTeam.js new file mode 100644 index 00000000..27c05836 --- /dev/null +++ b/server/services/agol/getCustomerTeam.js @@ -0,0 +1,34 @@ +const { config } = require('../../../config') +const { esriRequest } = require('./') + +const errorMessage = 'Invalid response from AGOL customerTeam request' + +const getCustomerTeam = (options) => { + const response = { + isEngland: false, + EmailAddress: '', + AreaName: '', + useAutomatedService: false + } + return esriRequest(config.agol.customerTeamEndPoint, options.geometry, options.geometryType) + .then((esriResult) => { + // Note This request WILL NOT return data for areas that are outside of England + if (!esriResult || !Array.isArray(esriResult)) { + console.log(errorMessage, 'response was', esriResult) + throw new Error(errorMessage) + } + response.isEngland = esriResult.length > 0 + const { attributes } = esriResult[0] + // console.log('\ncustomerTeam Results ', esriResult) + return Object.assign(response, { + EmailAddress: attributes.contact_email, + AreaName: attributes.area_name_1, // This will change back to area_name once Paul fixes the data. + useAutomatedService: Boolean(attributes.use_automated_service) + }) + }).catch((error) => { + console.log('Error thrown in getCustomerTeam', error) + throw error + }) +} + +module.exports = { getCustomerTeam } diff --git a/server/services/agol/getLocalAuthority.js b/server/services/agol/getLocalAuthority.js new file mode 100644 index 00000000..e555723a --- /dev/null +++ b/server/services/agol/getLocalAuthority.js @@ -0,0 +1,15 @@ +const { config } = require('../../../config') +const { esriRequest } = require('./') + +const getLocalAuthority = (options) => { + return esriRequest(config.agol.localAuthorityEndPoint, options.geometry, options.geometryType) + .then((esriResult) => { + if (!esriResult || !Array.isArray(esriResult)) { + throw new Error('Invalid response from AGOL localAuthority request') + } + const { attributes } = esriResult[0] + return { LocalAuthorities: attributes.authority_name } + }) +} + +module.exports = { getLocalAuthority } diff --git a/server/services/agol/index.js b/server/services/agol/index.js index 6ada0d02..13bb51fd 100644 --- a/server/services/agol/index.js +++ b/server/services/agol/index.js @@ -1,6 +1,4 @@ -const { config } = require('../../../config') -const { queryFeatures } = require('@esri/arcgis-rest-feature-service') -const { getEsriToken } = require('./getEsriToken') +const { esriRequest } = require('./esriRequest') const makePointGeometry = (x, y) => ({ x, y, spatialReference: { wkid: 27700 } }) @@ -12,20 +10,4 @@ const makePolygonGeometry = (polygon) => { } } -const esriRequest = async (endPoint, geometry, geometryType) => { - const esriToken = await getEsriToken() - const requestObject = { - url: `${config.agol.serviceUrl}${endPoint}`, - geometry, - geometryType, - spatialRel: 'esriSpatialRelIntersects', - returnGeometry: 'false', - authentication: esriToken, - outFields: '*' - } - // console.dir(requestObject, { depth: null }) - const result = await queryFeatures(requestObject) - return result.features -} - module.exports = { esriRequest, makePointGeometry, makePolygonGeometry } diff --git a/server/views/results.html b/server/views/results.html index 6ecc42dd..77bfc323 100644 --- a/server/views/results.html +++ b/server/views/results.html @@ -30,9 +30,11 @@ <h3 class="govuk-heading-s govuk-warning-text__text" data-testid="understanding- {% endif %} <div class="govuk-grid-column-two-thirds"> - <a href="contact?polygon={{polygon}}" class="govuk-button" data-module="govuk-button"> - Order flood risk data - </a> + {% if showOrderProduct4Button %} + <a href="contact?polygon={{polygon}}" class="govuk-button" data-module="govuk-button" data-testid="order-product4"> + Order flood risk data + </a> + {% endif %} </div> </div> {% endblock %} From c4eee7b531150a72236618bac1b9ea2e5348b296 Mon Sep 17 00:00:00 2001 From: Tedd <teddmason@gmail.com> Date: Wed, 22 Jan 2025 11:45:50 +0000 Subject: [PATCH 5/5] FCRM-5487 - setting datasets and map feature toggles to expanded (#455) --- client/js/defra-map/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/js/defra-map/index.js b/client/js/defra-map/index.js index b678868f..2b46db4d 100644 --- a/client/js/defra-map/index.js +++ b/client/js/defra-map/index.js @@ -400,7 +400,7 @@ getDefraMapConfig().then((defraMapConfig) => { keyDisplay: 'min', segments: [{ heading: 'Datasets', - collapse: 'collapse', + collapse: 'expanded', items: [ { id: 'fz', @@ -583,7 +583,7 @@ getDefraMapConfig().then((defraMapConfig) => { { heading: 'Map features', parentIds: ['fz'], - collapse: 'collapse', + collapse: 'expanded', items: [ keyItemDefinitions.floodZone1, keyItemDefinitions.floodZone2, @@ -595,7 +595,7 @@ getDefraMapConfig().then((defraMapConfig) => { { heading: 'Map features', parentIds: ['rsd', 'rsu', 'sw'], - collapse: 'collapse', + collapse: 'expanded', items: [ keyItemDefinitions.floodExtents, keyItemDefinitions.waterStorageAreas, @@ -606,7 +606,7 @@ getDefraMapConfig().then((defraMapConfig) => { { heading: 'Map features', parentIds: ['mo'], - collapse: 'collapse', + collapse: 'expanded', items: [ keyItemDefinitions.waterStorageAreas, keyItemDefinitions.floodDefences,