From 8a950cbdc148320800b32e83d0edf8419b583214 Mon Sep 17 00:00:00 2001 From: Phil Benson Date: Fri, 15 Nov 2024 11:21:08 +0000 Subject: [PATCH] Added validation for licence to start page --- .../gafl-webapp-service/src/locales/cy.json | 11 +- .../gafl-webapp-service/src/locales/en.json | 16 ++- .../pages/concessions/date-of-birth/route.js | 1 - .../licence-to-start/__tests__/route.spec.js | 108 +++++++++++++++++- .../licence-to-start/licence-to-start.njk | 61 +++++----- .../licence-details/licence-to-start/route.js | 88 +++----------- 6 files changed, 172 insertions(+), 113 deletions(-) diff --git a/packages/gafl-webapp-service/src/locales/cy.json b/packages/gafl-webapp-service/src/locales/cy.json index ab75cc41b4..2d0183b261 100644 --- a/packages/gafl-webapp-service/src/locales/cy.json +++ b/packages/gafl-webapp-service/src/locales/cy.json @@ -408,16 +408,17 @@ "licence_num": "Rhif trwydded", "licence_start_days": " diwrnod nesaf", "licence_start_enter_todays_date": "Rhowch ddyddiad heddiw os ydych chi am i’r drwydded 1 diwrnod neu 8 diwrnod ddechrau yn hwyrach heddiw.", - "licence_start_error_choose_when": "Dewiswch pryd y dylai'r drwydded ddechrau", - "licence_start_error_format": "Nodwch y dyddiad y mae angen i'r drwydded ddechrau a chynnwys diwrnod, mis a blwyddyn", - "licence_start_error_within": "Nodwch ddyddiad o fewn y ", "licence_start_error_date_real": "Mae’n rhaid i ddyddiad dechrau’r drwydded fod yn ddyddiad dilys", + "licence_start_error_missing_day_and_month": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod a mis", + "licence_start_error_missing_day_and_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod a blwyddyn", + "licence_start_error_missing_month_and_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys mis a blwyddyn", + "licence_start_error": "Rhowch ddyddiad dechrau’r drwydded", "licence_start_error_missing_day": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod", "licence_start_error_missing_month": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys mis", "licence_start_error_missing_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys blwyddyn", "licence_start_error_non_numeric": "Rhowch rifau yn unig", - "licence_start_error_year_min": "Licence start date is too long ago", - "licence_start_error_year_max": "The licence start date must be in the past", + "licence_start_error_choose_when": "Dewiswch pryd y dylai'r drwydded ddechrau", + "licence_start_error_within": "Nodwch ddyddiad o fewn y ", "licence_start_hint": "Rhowch ddyddiad hyd at a chan gynnwys ", "licence_start_later": "Yn hwyrach", "licence_start_minutes_after": " munud ar ôl derbyn y taliad", diff --git a/packages/gafl-webapp-service/src/locales/en.json b/packages/gafl-webapp-service/src/locales/en.json index 2f48a88e9a..34c16bd762 100644 --- a/packages/gafl-webapp-service/src/locales/en.json +++ b/packages/gafl-webapp-service/src/locales/en.json @@ -265,6 +265,7 @@ "disability_concession_title_you": "Do you receive any of the following?", "dob_day": "day", "dob_entry_hint": "For example, 23 11 1979", + "dob_error_date_real": "Date of birth must be a real date", "dob_error_missing_day_and_month": "Date of birth must include a day and month", "dob_error_missing_day_and_year": "Date of birth must include a day and year", @@ -276,6 +277,7 @@ "dob_error_year_min": "Date of birth is too long ago", "dob_error_year_max": "The date of birth must be in the past", "dob_error": "Enter a date of birth", + "dob_month": "month", "dob_privacy_link_prefix": "If you do not provide a correct date of birth, this may cause delays when a licence is renewed or mean that a licence is not valid. Read about ", "dob_privacy_link": "how we use personal information (opens in new tab)", @@ -408,16 +410,20 @@ "licence_num": "Licence number", "licence_start_days": " days", "licence_start_enter_todays_date": "Enter today’s date if you want the 1-day or 8-day licence to start later today.", - "licence_start_error_choose_when": "Choose when the licence should start", - "licence_start_error_format": "Enter the date the licence needs to start, include a day, month and year", - "licence_start_error_within": "Enter a date within the next ", + "licence_start_error_date_real": "Licence start date must be a real date", + "licence_start_error_missing_day_and_month": "Licence start date must include a day and month", + "licence_start_error_missing_day_and_year": "Licence start date must include a day and year", + "licence_start_error_missing_month_and_year": "Licence start date must include a month and year", "licence_start_error_missing_day": "Licence start date must include a day", "licence_start_error_missing_month": "Licence start date must include a month", "licence_start_error_missing_year": "Licence start date must include a year", "licence_start_error_non_numeric": "Enter only numbers", - "licence_start_error_year_min": "Licence start date is too long ago", - "licence_start_error_year_max": "The licence start date must be in the past", + "licence_start_error": "Enter a licence start date", + + "licence_start_error_choose_when": "Choose when the licence should start", + "licence_start_error_within": "Enter a date within the next ", + "licence_start_hint": "Enter a date up to and including ", "licence_start_later": "Later", "licence_start_minutes_after": " minutes after payment", diff --git a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js index 7b67fd5dda..c6980e697e 100644 --- a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js +++ b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js @@ -1,7 +1,6 @@ import { DATE_OF_BIRTH, LICENCE_FOR } from '../../../uri.js' import Joi from 'joi' import pageRoute from '../../../routes/page-route.js' -// import { validation } from '@defra-fish/business-rules-lib' import { nextPage } from '../../../routes/next-page.js' import GetDataRedirect from '../../../handlers/get-data-redirect.js' import { dateSchema, dateSchemaInput } from '../../../schema/date.schema.js' diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js index 3d76e2b007..3e34d0bf4d 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js @@ -1,4 +1,7 @@ -import { getData } from '../route' +import Joi from 'joi' +import { getData, validator } from '../route' +import moment from 'moment' +const dateSchema = require('../../../../schema/date.schema.js') jest.mock('../../../../processors/uri-helper.js') @@ -28,4 +31,107 @@ describe('licence-to-start > route', () => { expect(result.isLicenceForYou).toBeFalsy() }) }) + + describe('validation', () => { + beforeEach(jest.clearAllMocks) + + const getSamplePayload = ({ day = '', month = '', year = '' } = {}) => ({ + 'licence-start-date-day': day, + 'licence-start-date-month': month, + 'licence-start-date-year': year, + 'licence-to-start': 'another-date' + }) + + const setupMocks = () => { + Joi.originalAssert = Joi.assert + dateSchema.originalDateSchema = dateSchema.dateSchema + dateSchema.originalDateSchemaInput = dateSchema.dateSchemaInput + + Joi.assert = jest.fn() + dateSchema.dateSchema = Symbol('dateSchema') + dateSchema.dateSchemaInput = jest.fn() + } + + const tearDownMocks = () => { + Joi.assert = Joi.originalAssert + dateSchema.dateSchema = dateSchema.originalDateSchema + dateSchema.dateSchemaInput = dateSchema.originalDateSchemaInput + } + + it('throws an error for a licence starting before today', () => { + const invalidStartDate = moment().subtract(1, 'day') + const samplePayload = getSamplePayload({ + day: invalidStartDate.format('DD'), + month: invalidStartDate.format('MM'), + year: invalidStartDate.format('YYYY') + }) + expect(() => validator(samplePayload)).toThrow() + }) + + it('throws an error for a licence starting more than 30 days hence', () => { + const invalidStartDate = moment().add(31, 'days') + const samplePayload = getSamplePayload({ + day: invalidStartDate.format('DD'), + month: invalidStartDate.format('MM'), + year: invalidStartDate.format('YYYY') + }) + expect(() => validator(samplePayload)).toThrow() + }) + + it('validates for a date within the next 30 days', () => { + const validStartDate = moment().add(4, 'days') + const samplePayload = getSamplePayload({ + day: validStartDate.format('DD'), + month: validStartDate.format('MM'), + year: validStartDate.format('YYYY') + }) + expect(() => validator(samplePayload)).not.toThrow() + }) + + it.each([ + ['1-3-2024', moment('2024-02-28')], + ['9-7-2024', moment('2024-07-08')] + ])('handles single digit date %s', (date, now) => { + jest.useFakeTimers() + jest.setSystemTime(now.toDate()) + + const [day, month, year] = date.split('-') + const samplePayload = getSamplePayload({ + day, + month, + year + }) + expect(() => validator(samplePayload)).not.toThrow() + jest.useRealTimers() + }) + + it.each([ + ['01', '03', '1994'], + ['10', '12', '2004'] + ])('passes start date day (%s), month (%s) and year (%s) to dateSchemaInput', (day, month, year) => { + setupMocks() + validator(getSamplePayload({ day, month, year })) + expect(dateSchema.dateSchemaInput).toHaveBeenCalledWith(day, month, year) + tearDownMocks() + }) + + it('passes dateSchemaInput output and dateSchema to Joi.assert', () => { + setupMocks() + const dsi = Symbol('dsi') + dateSchema.dateSchemaInput.mockReturnValueOnce(dsi) + validator(getSamplePayload()) + expect(Joi.assert).toHaveBeenCalledWith(dsi, dateSchema.dateSchema) + tearDownMocks() + }) + + it('passes validation if licence is set to start after payment', () => { + const samplePayload = { 'licence-to-start': 'after-payment' } + expect(() => validator(samplePayload)).not.toThrow() + }) + + it('throws an error if licence-to-start is set to an invalid value', () => { + const samplePayload = { 'licence-to-start': '12th-of-never' } + expect(() => validator(samplePayload)).toThrow() + }) + }) }) diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk index 35fc908766..4f455f4018 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk @@ -8,36 +8,37 @@ {% set errorMap = { - 'licence-to-start': { - 'any.required': { ref: '#licence-to-start', text: mssgs.licence_start_error_choose_when } - }, - 'licence-start-date-old': { - 'date.format': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_format }, - 'date.max': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days }, - 'date.min': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days }, - 'date.real': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real } - }, - 'day': { - 'any.required': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day }, - 'number.base': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_non_numeric }, - 'number.integer': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real }, - 'number.min': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real }, - 'number.max': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real } - }, - 'month': { - 'any.required': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month }, - 'number.base': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_non_numeric }, - 'number.integer': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_date_real }, - 'number.min': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_date_real }, - 'number.max': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_date_real } - }, - 'year': { - 'any.required': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_missing_year }, - 'number.base': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_non_numeric }, - 'number.integer': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_date_real }, - 'number.min': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_year_min }, - 'number.max': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_year_max } - } + 'full-date': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error } + }, + 'day-and-month': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_month } + }, + 'day-and-year': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_year } + }, + 'month-and-year': { + 'object.missing': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month_and_year } + }, + 'day': { + 'any.required': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day } + }, + 'month': { + 'any.required': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month } + }, + 'year': { + 'any.required': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_missing_year } + }, + 'non-numeric': { + 'number.base': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_non_numeric } + }, + 'invalid-date': { + 'any.custom': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real } + }, + 'startDate': { + 'date.min': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days }, + 'date.max': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days } + } } %} diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js index 54a1175025..e8cf767510 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js @@ -1,86 +1,32 @@ import Joi from 'joi' import moment from 'moment-timezone' -import { START_AFTER_PAYMENT_MINUTES, ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME, validation } from '@defra-fish/business-rules-lib' +import { START_AFTER_PAYMENT_MINUTES, ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib' import { LICENCE_TO_START } from '../../../uri.js' import pageRoute from '../../../routes/page-route.js' import { nextPage } from '../../../routes/next-page.js' +import { dateSchema, dateSchemaInput } from '../../../schema/date.schema.js' const LICENCE_TO_START_FIELD = 'licence-to-start' const AFTER_PAYMENT = 'after-payment' const ANOTHER_DATE = 'another-date' -const minTime = moment().tz(SERVICE_LOCAL_TIME).startOf('day') -const maxTime = moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days') -const minYear = minTime.year() -const maxYear = maxTime.year() - -const daySchema = () => { - Joi.when(LICENCE_TO_START_FIELD, { - is: ANOTHER_DATE, - then: Joi.any().required().concat(validation.date.createDayValidator(Joi)), - otherwise: Joi.any() - }) -} - -const monthSchema = () => { - Joi.when(LICENCE_TO_START_FIELD, { - is: ANOTHER_DATE, - then: Joi.any().required().concat(validation.date.createMonthValidator(Joi)) - }) -} - -const yearSchema = () => { - Joi.when(LICENCE_TO_START_FIELD, { - is: ANOTHER_DATE, - then: Joi.any().required().concat(validation.date.createYearValidator(Joi, minYear, maxYear)) - }) -} - -const licenceStartDateSchema = () => { - Joi.when(LICENCE_TO_START_FIELD, { - is: ANOTHER_DATE, - then: Joi.when( - Joi.object({ - day: Joi.any().required().concat(validation.date.createDayValidator(Joi)), - month: Joi.any().required().concat(validation.date.createMonthValidator(Joi)), - year: Joi.any().required().concat(validation.date.createYearValidator(Joi, minYear, maxYear)) - }).unknown(), - { - then: validation.date.createRealDateValidator(Joi) - } - ) - }) -} - -const validator = payload => { - const day = payload['licence-start-date-day'] - const month = payload['licence-start-date-month'] - const year = payload['licence-start-date-year'] - - const licenceStartDate = { day: parseInt(day), month: parseInt(month), year: parseInt(year) } - const licenceStartDateForRange = `${year}-${month}-${day}` - +export const validator = payload => { Joi.assert( - { - 'licence-start-date-for-range': licenceStartDateForRange, - 'licence-to-start': payload[LICENCE_TO_START_FIELD], - day: day || undefined, - month: month || undefined, - year: year || undefined, - 'licence-start-date': licenceStartDate - }, - Joi.object({ - 'licence-to-start': Joi.string().valid(AFTER_PAYMENT, ANOTHER_DATE).required(), - 'licence-start-date-for-range': Joi.when(LICENCE_TO_START_FIELD, { - is: ANOTHER_DATE, - then: Joi.date().min(minTime).max(maxTime).required() - }), - day: daySchema, - month: monthSchema, - year: yearSchema, - 'licence-start-date': licenceStartDateSchema - }).options({ abortEarly: false, allowUnknown: true }) + { 'licence-to-start': payload[LICENCE_TO_START_FIELD] }, + Joi.object({ 'licence-to-start': Joi.string().valid(AFTER_PAYMENT, ANOTHER_DATE).required() }) ) + if (payload[LICENCE_TO_START_FIELD] === ANOTHER_DATE) { + const minDate = moment().tz(SERVICE_LOCAL_TIME).startOf('day') + const maxDate = moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days') + + const day = payload['licence-start-date-day'] + const month = payload['licence-start-date-month'] + const year = payload['licence-start-date-year'] + + Joi.assert(dateSchemaInput(day, month, year), dateSchema) + const startDate = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T00:00:00.000Z`) + Joi.assert({ startDate }, Joi.object({ startDate: Joi.date().min(minDate.toDate()).max(maxDate.toDate()) })) + } } export const getData = async request => {