From 15e50c20cc7956478711466f2c1697143ddc6093 Mon Sep 17 00:00:00 2001 From: sujithvg Date: Tue, 17 Sep 2024 14:50:24 +0100 Subject: [PATCH] =?UTF-8?q?SIR-932=20:=20Story=20=E2=80=93=20Odour=20?= =?UTF-8?q?=E2=80=93=20Remit=20-=20Can=20you=20give=20details=20about=20th?= =?UTF-8?q?e=20source=20of=20the=20smell=3F=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Technical implementation for SIR-932 * fixing flakey date test * Fixed sonar issue * Fixed lint issue * Fixed sonar issue --------- Co-authored-by: Tedd Mason --- .../__tests__/smell/source-details.spec.js | 160 ++++++++++++++++++ .../__tests__/smell/start-date-time.spec.js | 3 +- server/routes/smell/source-details.js | 117 ++++++++++++- server/routes/smell/source.js | 1 + server/utils/question-sets.js | 23 +++ server/views/smell/source-details.html | 99 ++++++++++- 6 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 server/routes/__tests__/smell/source-details.spec.js diff --git a/server/routes/__tests__/smell/source-details.spec.js b/server/routes/__tests__/smell/source-details.spec.js new file mode 100644 index 0000000..272c09b --- /dev/null +++ b/server/routes/__tests__/smell/source-details.spec.js @@ -0,0 +1,160 @@ +import { submitGetRequest, submitPostRequest } from '../../../__test-helpers__/server.js' +import { questionSets } from '../../../utils/question-sets.js' +import constants from '../../../utils/constants.js' + +const url = constants.routes.SMELL_SOURCE_DETAILS +const question = questionSets.SMELL.questions.SMELL_SOURCE_DETAILS +const header = question.text +const baseAnswer = { + questionId: question.questionId, + questionAsked: question.text, + questionResponse: true +} + +const payload = { + answerId: 'yes', + siteName: 'Site name', + sourceAddress: 'Address Line', + sourceTown: 'town or city', + sourcePostcode: 'WA4 1HT' +} + +describe(url, () => { + describe('GET', () => { + it(`Should return success response and correct view for ${url}`, async () => { + await submitGetRequest({ url }, header) + }) + }) + + describe('POST', () => { + it('Happy: accepts valid answerId no and redirects to smell/contact-local-council', async () => { + const answerId = 'no' + const options = { + url, + payload: { + answerId + } + } + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(constants.routes.SMELL_CONTACT_LOCAL_COUNCIL) + }) + + // Happy: Accepts a complete address + it('Happy: accepts valid answerId yes and complete address with valid postcode', async () => { + const options = { + url, + payload + } + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(constants.routes.SMELL_LOCATION_HOME) + expect(response.request.yar.get(constants.redisKeys.SMELL_SOURCE_DETAILS)).toEqual([{ + ...baseAnswer, + answerId: question.answers.siteName.answerId, + otherDetails: payload.siteName + }, { + ...baseAnswer, + answerId: question.answers.sourceAddress.answerId, + otherDetails: payload.sourceAddress + }, { + ...baseAnswer, + answerId: question.answers.sourceTown.answerId, + otherDetails: payload.sourceTown + }, { + ...baseAnswer, + answerId: question.answers.sourcePostcode.answerId, + otherDetails: payload.sourcePostcode + }]) + }) + // Happy: Accepts a partial address, but with complete mandatory fields + it('Happy: accepts valid answerId yes and complete address with valid postcode', async () => { + const partialPayload = JSON.parse(JSON.stringify(payload)) + partialPayload.sourceAddress = '' + + const options = { + url, + payload: partialPayload + } + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(constants.routes.SMELL_LOCATION_HOME) + expect(response.request.yar.get(constants.redisKeys.SMELL_SOURCE_DETAILS)).toEqual([{ + ...baseAnswer, + answerId: question.answers.siteName.answerId, + otherDetails: payload.siteName + }, { + ...baseAnswer, + answerId: question.answers.sourceAddress.answerId, + otherDetails: '' + }, { + ...baseAnswer, + answerId: question.answers.sourceTown.answerId, + otherDetails: payload.sourceTown + }, { + ...baseAnswer, + answerId: question.answers.sourcePostcode.answerId, + otherDetails: payload.sourcePostcode + }]) + }) + it('Happy: accepts valid answerId yes and strips out postcode with special characters', async () => { + const partialPayload = JSON.parse(JSON.stringify(payload)) + partialPayload.sourcePostcode = 'WA4 &^%$%$--1HT' + + const options = { + url, + payload: partialPayload + } + const response = await submitPostRequest(options) + expect(response.headers.location).toEqual(constants.routes.SMELL_LOCATION_HOME) + expect(response.request.yar.get(constants.redisKeys.SMELL_SOURCE_DETAILS)).toEqual([{ + ...baseAnswer, + answerId: question.answers.siteName.answerId, + otherDetails: payload.siteName + }, { + ...baseAnswer, + answerId: question.answers.sourceAddress.answerId, + otherDetails: payload.sourceAddress + }, { + ...baseAnswer, + answerId: question.answers.sourceTown.answerId, + otherDetails: payload.sourceTown + }, { + ...baseAnswer, + answerId: question.answers.sourcePostcode.answerId, + otherDetails: 'WA4 1HT' + }]) + }) + it('Sad: errors on no fields provided', async () => { + const options = { + url, + payload: {} + } + const response = await submitPostRequest(options, constants.statusCodes.OK) + expect(response.payload).toContain('There is a problem') + expect(response.payload).toContain('Answer yes if you can give details about where the smell is coming from') + }) + it('Sad: valid answerId yes but errors on no fields provided', async () => { + const options = { + url, + payload: { + answerId: 'yes' + } + } + const response = await submitPostRequest(options, constants.statusCodes.OK) + expect(response.payload).toContain('There is a problem') + expect(response.payload).toContain('Enter a name') + expect(response.payload).toContain('Enter a town or city') + expect(response.payload).toContain('Enter a postcode') + }) + it('Sad: errors on invalid postcode provided', async () => { + const options = { + url, + payload: { + ...payload, + sourcePostcode: 'sdgfsfdgfdsgfdg' + } + } + const response = await submitPostRequest(options, constants.statusCodes.OK) + expect(response.payload).toContain('There is a problem') + expect(response.payload).toContain('Enter a full UK postcode') + }) + }) +}) diff --git a/server/routes/__tests__/smell/start-date-time.spec.js b/server/routes/__tests__/smell/start-date-time.spec.js index 133f6f0..3d4da0d 100644 --- a/server/routes/__tests__/smell/start-date-time.spec.js +++ b/server/routes/__tests__/smell/start-date-time.spec.js @@ -59,6 +59,7 @@ describe(url, () => { it('Happy: accept a valid time for today and continue to SMELL_CURRENT', async () => { const date = new Date() const period = date.getHours() > 11 ? 'pm' : 'am' + const hours = date.getHours() > 12 ? date.getHours() - 12 : date.getHours() const options = { url, payload: { @@ -66,7 +67,7 @@ describe(url, () => { 'date-day': '1', 'date-month': '1', 'date-year': '2024', - hour: [date.getHours().toString(), '1', '1'], + hour: [hours.toString(), '1', '1'], minute: ['0', '1', '1'], period: [period, 'am', 'am'] } diff --git a/server/routes/smell/source-details.js b/server/routes/smell/source-details.js index 6b6e132..07b9c19 100644 --- a/server/routes/smell/source-details.js +++ b/server/routes/smell/source-details.js @@ -1,15 +1,130 @@ import constants from '../../utils/constants.js' +import { getErrorSummary } from '../../utils/helpers.js' +import { questionSets } from '../../utils/question-sets.js' + +const question = questionSets.SMELL.questions.SMELL_SOURCE_DETAILS +const postcodeRegExp = /^([A-Za-z][A-Ha-hJ-Yj-y]?\d[A-Za-z0-9]? ?\d[A-Za-z]{2}|[Gg][Ii][Rr] ?0[Aa]{2})$/ // https://stackoverflow.com/a/51885364 + +const baseAnswer = { + questionId: question.questionId, + questionAsked: question.text, + questionResponse: true +} const handlers = { get: async (_request, h) => { - return h.view(constants.views.SMELL_SOURCE_DETAILS) + return h.view(constants.views.SMELL_SOURCE_DETAILS, { + ...getContext() + }) + }, + post: async (request, h) => { + // cleanse postcode for special characters https://design-system.service.gov.uk/patterns/addresses/#allow-different-postcode-formats + if (request.payload.sourcePostcode) { + request.payload.sourcePostcode = request.payload.sourcePostcode.replace(/[^\w\s]/gi, '') + } + + // validate payload for errors + const errorSummary = validatePayload(request.payload) + if (errorSummary.errorList.length > 0) { + return h.view(constants.views.SMELL_SOURCE_DETAILS, { + ...getContext(), + errorSummary, + ...request.payload, + yesChecked: request.payload.answerId === 'yes' + }) + } + + // handle redirects + if (request.payload.answerId === 'yes') { + // set answer in session + request.yar.set(constants.redisKeys.SMELL_SOURCE_DETAILS, buildAnswers(request.payload)) + return h.redirect(constants.routes.SMELL_LOCATION_HOME) + } else if (request.payload.answerId === 'no') { + return h.redirect(constants.routes.SMELL_CONTACT_LOCAL_COUNCIL) + } else { + // do nothing + } + + return null + } +} + +const getContext = () => { + return { + question } } +const validatePayload = payload => { + const errorSummary = getErrorSummary() + if (!payload.answerId) { + errorSummary.errorList.push({ + text: 'Answer yes if you can give details about where the smell is coming from', + href: '#answerId' + }) + } else if (payload.answerId === 'yes') { + if (!payload.siteName) { + errorSummary.errorList.push({ + text: 'Enter a name', + href: '#siteName' + }) + } + if (!payload.sourceTown) { + errorSummary.errorList.push({ + text: 'Enter a town or city', + href: '#sourceTown' + }) + } + if (!payload.sourcePostcode) { + errorSummary.errorList.push({ + text: 'Enter a postcode', + href: '#sourcePostcode' + }) + } else if (!postcodeRegExp.test(payload.sourcePostcode)) { + errorSummary.errorList.push({ + text: 'Enter a full UK postcode', + href: '#sourcePostcode' + }) + } else { + // do nothing + } + } else { + // do nothing + } + + return errorSummary +} + +const buildAnswers = payload => { + return [{ + ...baseAnswer, + answerId: question.answers.siteName.answerId, + otherDetails: payload.siteName + }, { + ...baseAnswer, + answerId: question.answers.sourceAddress.answerId, + otherDetails: payload.sourceAddress + }, { + ...baseAnswer, + answerId: question.answers.sourceTown.answerId, + otherDetails: payload.sourceTown + }, + { + ...baseAnswer, + answerId: question.answers.sourcePostcode.answerId, + otherDetails: payload.sourcePostcode + }] +} + export default [ { method: 'GET', path: constants.routes.SMELL_SOURCE_DETAILS, handler: handlers.get + }, + { + method: 'POST', + path: constants.routes.SMELL_SOURCE_DETAILS, + handler: handlers.post } ] diff --git a/server/routes/smell/source.js b/server/routes/smell/source.js index 073a531..2a8c0fc 100644 --- a/server/routes/smell/source.js +++ b/server/routes/smell/source.js @@ -35,6 +35,7 @@ const handlers = { // set answer in session request.yar.set(constants.redisKeys.SMELL_SOURCE, buildAnswers(answerId)) + // handle redirects if (answerId === question.answers.local.answerId || answerId === question.answers.neighbour.answerId || answerId === question.answers.rubbish.answerId) { return h.redirect(constants.routes.SMELL_REPORT_LOCAL_COUNCIL) } else if (answerId === question.answers.unknown.answerId) { diff --git a/server/utils/question-sets.js b/server/utils/question-sets.js index e35a6b2..b78e9f3 100644 --- a/server/utils/question-sets.js +++ b/server/utils/question-sets.js @@ -331,6 +331,29 @@ const questionSets = { } } }, + SMELL_SOURCE_DETAILS: { + questionId: 200, + key: constants.redisKeys.SMELL_SOURCE_DETAILS, + text: 'Can you give details about where the smell is coming from?', + answers: { + siteName: { + answerId: 203, + text: 'Name of person or site' + }, + sourceAddress: { + answerId: 204, + text: 'Street name and number (if known)' + }, + sourceTown: { + answerId: 205, + text: 'Town or city' + }, + sourcePostcode: { + answerId: 205, + text: 'Postcode (if known)' + } + } + }, SMELL_LOCATION_HOME: { questionId: 1, key: constants.redisKeys.SMELL_LOCATION_HOME, diff --git a/server/views/smell/source-details.html b/server/views/smell/source-details.html index 7f315a8..01a6079 100644 --- a/server/views/smell/source-details.html +++ b/server/views/smell/source-details.html @@ -1,15 +1,98 @@ {% extends 'form-layout.html' %} - -{% set pageTitle = 'Source details' %} +{% set pageTitle = question.text.replace('?', '') %} {% block formContent %}
-
-

- Source Details -

-
-
+
+ {% set yesDetailsHtml %} + + {{ govukInput({ + errorMessage: findErrorMessageById(errorSummary, "siteName"), + label: { + text: question.answers.siteName.text, + classes: "govuk-fieldset__legend--s" + }, + id: "siteName", + name: "siteName", + autocomplete: "siteName", + value: siteName + }) }} + + {{ govukInput({ + label: { + text: question.answers.sourceAddress.text, + classes: "govuk-fieldset__legend--s" + }, + id: "sourceAddress", + name: "sourceAddress", + autocomplete: "sourceAddress", + value: sourceAddress + }) }} + + {{ govukInput({ + errorMessage: findErrorMessageById(errorSummary, "sourceTown"), + label: { + text: question.answers.sourceTown.text, + classes: "govuk-fieldset__legend--s" + }, + classes: "govuk-!-width-two-thirds", + id: "sourceTown", + name: "sourceTown", + autocomplete: "sourceTown", + value: sourceTown + }) }} + + {{ govukInput({ + errorMessage: findErrorMessageById(errorSummary, "sourcePostcode"), + label: { + text: question.answers.sourcePostcode.text, + classes: "govuk-fieldset__legend--s" + }, + classes: "govuk-input--width-10", + id: "sourcePostcode", + name: "sourcePostcode", + autocomplete: "sourcePostcode", + value: sourcePostcode + }) }} + + {% endset -%} + + {{ govukRadios({ + classes: "govuk-radios", + name: "answerId", + id: "answerId", + errorMessage: findErrorMessageById(errorSummary, "answerId"), + fieldset: { + legend: { + text: question.text, + isPageHeading: true, + classes: "govuk-fieldset__legend--l" + } + }, + hint: { + text: "For example, the name of the site or person responsible, or an address." + }, + items: [ + { + value: "yes", + text: "Yes", + checked: yesChecked, + conditional: { + html: yesDetailsHtml + } + }, + { + value: "no", + text: "No" + } + ] + }) }} + + {{ govukButton({ + text: "Continue" + }) }} +
+ {% endblock %}