@defra-fish/gafl-webapp-service 1.57.0-rc.3 → 1.57.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,49 +1,31 @@
1
- import Joi from 'joi'
2
1
  import moment from 'moment-timezone'
3
-
4
- import JoiDate from '@hapi/joi-date'
5
2
  import { START_AFTER_PAYMENT_MINUTES, ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
6
3
  import { LICENCE_TO_START } from '../../../uri.js'
7
4
  import pageRoute from '../../../routes/page-route.js'
8
- import { dateFormats } from '../../../constants.js'
9
5
  import { nextPage } from '../../../routes/next-page.js'
10
-
11
- const JoiX = Joi.extend(JoiDate)
12
-
13
- const validator = payload => {
14
- const licenceStartDate = `${payload['licence-start-date-year']}-${payload['licence-start-date-month']}-${payload['licence-start-date-day']}`
15
- Joi.assert(
16
- {
17
- 'licence-start-date': licenceStartDate,
18
- 'licence-to-start': payload['licence-to-start']
19
- },
20
- Joi.object({
21
- 'licence-to-start': Joi.string().valid('after-payment', 'another-date').required(),
22
- 'licence-start-date': Joi.alternatives().conditional('licence-to-start', {
23
- is: 'another-date',
24
- then: JoiX.date()
25
- .format(dateFormats)
26
- .min(moment().tz(SERVICE_LOCAL_TIME).startOf('day'))
27
- .max(moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days'))
28
- .required(),
29
- otherwise: Joi.string().empty('')
30
- })
31
- }).options({ abortEarly: false, allowUnknown: true })
32
- )
33
- }
6
+ import { getDateErrorFlags, startDateValidator } from '../../../schema/validators/validators.js'
34
7
 
35
8
  export const getData = async request => {
36
9
  const fmt = 'DD MM YYYY'
37
10
  const { isLicenceForYou } = await request.cache().helpers.transaction.getCurrentPermission()
38
-
39
- return {
11
+ const page = await request.cache().helpers.page.getCurrentPermission(LICENCE_TO_START.page)
12
+ const pageData = {
40
13
  isLicenceForYou,
41
14
  exampleStartDate: moment().tz(SERVICE_LOCAL_TIME).add(1, 'days').format(fmt),
42
15
  minStartDate: moment().tz(SERVICE_LOCAL_TIME).format(fmt),
43
16
  maxStartDate: moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days').format(fmt),
44
17
  advancedPurchaseMaxDays: ADVANCED_PURCHASE_MAX_DAYS,
45
- startAfterPaymentMinutes: START_AFTER_PAYMENT_MINUTES
18
+ startAfterPaymentMinutes: START_AFTER_PAYMENT_MINUTES,
19
+ ...getDateErrorFlags(page?.error)
20
+ }
21
+
22
+ if (page?.error) {
23
+ const [errorKey] = Object.keys(page.error)
24
+ const errorValue = page.error[errorKey]
25
+ pageData.error = { errorKey, errorValue }
46
26
  }
27
+
28
+ return pageData
47
29
  }
48
30
 
49
- export default pageRoute(LICENCE_TO_START.page, LICENCE_TO_START.uri, validator, nextPage, getData)
31
+ export default pageRoute(LICENCE_TO_START.page, LICENCE_TO_START.uri, startDateValidator, nextPage, getData)
@@ -116,6 +116,9 @@ describe('The easy renewal identification page', () => {
116
116
  referenceNumber: 'ABC123'
117
117
  }),
118
118
  setCurrentPermission: () => {}
119
+ },
120
+ page: {
121
+ getCurrentPermission: async () => ({})
119
122
  }
120
123
  }
121
124
  })
@@ -0,0 +1,126 @@
1
+ import pageRoute from '../../../../routes/page-route.js'
2
+ import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js'
3
+ import { getData, validator } from '../route.js'
4
+ import { IDENTIFY, NEW_TRANSACTION } from '../../../../uri.js'
5
+ import { dateOfBirthValidator, getDateErrorFlags } from '../../../../schema/validators/validators.js'
6
+
7
+ jest.mock('../../../../routes/page-route.js', () => jest.fn())
8
+ jest.mock('../../../../uri.js', () => ({
9
+ IDENTIFY: { page: 'identify page', uri: 'identify uri' },
10
+ AUTHENTICATE: { uri: Symbol('authenticate uri') },
11
+ NEW_TRANSACTION: { uri: Symbol('new transaction uri') }
12
+ }))
13
+ jest.mock('../../../../processors/uri-helper.js')
14
+ jest.mock('../../../../schema/validators/validators.js')
15
+
16
+ describe('getData', () => {
17
+ const getMockRequest = (referenceNumber, pageGet = async () => ({})) => ({
18
+ cache: () => ({
19
+ helpers: {
20
+ status: {
21
+ getCurrentPermission: () => ({
22
+ referenceNumber: referenceNumber
23
+ })
24
+ },
25
+ page: {
26
+ getCurrentPermission: pageGet
27
+ }
28
+ }
29
+ })
30
+ })
31
+
32
+ it('addLanguageCodeToUri is called with the expected arguments', async () => {
33
+ const request = getMockRequest('013AH6')
34
+ await getData(request)
35
+ expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, NEW_TRANSACTION.uri)
36
+ })
37
+
38
+ it('getData returns correct URI', async () => {
39
+ const expectedUri = Symbol('decorated uri')
40
+ addLanguageCodeToUri.mockReturnValueOnce(expectedUri)
41
+
42
+ const result = await getData(getMockRequest('013AH6'))
43
+ expect(result.uri.new).toEqual(expectedUri)
44
+ })
45
+
46
+ it.each([['09F6VF'], ['013AH6'], ['LK563F']])('getData returns referenceNumber', async referenceNumber => {
47
+ const result = await getData(getMockRequest(referenceNumber))
48
+ expect(result.referenceNumber).toEqual(referenceNumber)
49
+ })
50
+
51
+ it('adds return value of getErrorFlags to the page data', async () => {
52
+ const errorFlags = { unique: Symbol('error-flags') }
53
+ getDateErrorFlags.mockReturnValueOnce(errorFlags)
54
+ const result = await getData(getMockRequest())
55
+ expect(result).toEqual(expect.objectContaining(errorFlags))
56
+ })
57
+
58
+ it('passes error to getErrorFlags', async () => {
59
+ const error = Symbol('error')
60
+ await getData(getMockRequest(undefined, async () => ({ error })))
61
+ expect(getDateErrorFlags).toHaveBeenCalledWith(error)
62
+ })
63
+
64
+ it('passes correct page name when getting page cache', async () => {
65
+ const pageGet = jest.fn(() => ({}))
66
+ await getData(getMockRequest(undefined, pageGet))
67
+ expect(pageGet).toHaveBeenCalledWith(IDENTIFY.page)
68
+ })
69
+ })
70
+
71
+ describe('default', () => {
72
+ it('should call the pageRoute with date-of-birth, /buy/date-of-birth, dateOfBirthValidator and nextPage', async () => {
73
+ expect(pageRoute).toBeCalledWith(IDENTIFY.page, IDENTIFY.uri, validator, expect.any(Function), getData)
74
+ })
75
+ })
76
+
77
+ describe('page route next', () => {
78
+ const nextPage = pageRoute.mock.calls[0][3]
79
+ beforeEach(jest.clearAllMocks)
80
+
81
+ it('passes a function as the nextPage argument', () => {
82
+ expect(typeof nextPage).toBe('function')
83
+ })
84
+
85
+ it('calls addLanguageCodeToUri', () => {
86
+ nextPage()
87
+ expect(addLanguageCodeToUri).toHaveBeenCalled()
88
+ })
89
+
90
+ it('passes request to addLanguageCodeToUri', () => {
91
+ const request = Symbol('request')
92
+ nextPage(request)
93
+ expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, expect.anything())
94
+ })
95
+
96
+ it('next page returns result of addLanguageCodeToUri', () => {
97
+ const expectedResult = Symbol('add language code to uri')
98
+ addLanguageCodeToUri.mockReturnValueOnce(expectedResult)
99
+ expect(nextPage()).toBe(expectedResult)
100
+ })
101
+ })
102
+
103
+ describe('validator', () => {
104
+ const getMockRequest = (postcode = 'AA1 1AA', referenceNumber = 'A1B2C3') => ({
105
+ postcode,
106
+ referenceNumber
107
+ })
108
+
109
+ it('fails if dateOfBirth validator fails', () => {
110
+ const expectedError = new Error('expected error')
111
+ dateOfBirthValidator.mockImplementationOnce(() => {
112
+ throw expectedError
113
+ })
114
+ expect(() => validator(getMockRequest)).toThrow(expectedError)
115
+ })
116
+
117
+ it('passes if dateOfBirth validator passes', () => {
118
+ expect(() => validator(getMockRequest())).not.toThrow()
119
+ })
120
+
121
+ it('passes payload to dateOfBirth validator', () => {
122
+ const payload = getMockRequest()
123
+ validator(payload)
124
+ expect(dateOfBirthValidator).toHaveBeenCalledWith(payload)
125
+ })
126
+ })
@@ -21,14 +21,40 @@
21
21
  ref: "#ref"
22
22
  }
23
23
  },
24
- 'date-of-birth': {
25
- 'date.format': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday },
26
- 'date.max': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday_max },
27
- 'date.min': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday_min }
28
- },
29
24
  'postcode': {
30
25
  'string.empty': { ref: '#postcode', text: mssgs.identify_error_empty_postcode },
31
26
  'string.pattern.base': { ref: '#postcode', text: mssgs.identify_error_pattern_postcode }
27
+ },
28
+ 'full-date': {
29
+ 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error }
30
+ },
31
+ 'day-and-month': {
32
+ 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_month }
33
+ },
34
+ 'day-and-year': {
35
+ 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_year }
36
+ },
37
+ 'month-and-year': {
38
+ 'object.missing': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month_and_year }
39
+ },
40
+ 'day': {
41
+ 'any.required': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day }
42
+ },
43
+ 'month': {
44
+ 'any.required': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month }
45
+ },
46
+ 'year': {
47
+ 'any.required': { ref: '#date-of-birth-year', text: mssgs.dob_error_missing_year }
48
+ },
49
+ 'non-numeric': {
50
+ 'number.base': { ref: '#date-of-birth-day', text: mssgs.dob_error_non_numeric }
51
+ },
52
+ 'invalid-date': {
53
+ 'any.custom': { ref: '#date-of-birth-day', text: mssgs.dob_error_date_real }
54
+ },
55
+ 'date-range': {
56
+ 'date.min': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_min },
57
+ 'date.max': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_max }
32
58
  }
33
59
  }
34
60
  %}
@@ -37,21 +63,21 @@
37
63
  {
38
64
  label: mssgs.dob_day,
39
65
  name: "day",
40
- classes: "govuk-input--width-2",
66
+ classes: "govuk-input--width-2 govuk-input--error" if data.isDayError else "govuk-input--width-2",
41
67
  value: payload['date-of-birth-day'],
42
68
  attributes: { maxlength : 2 }
43
69
  },
44
70
  {
45
71
  label: mssgs.dob_month,
46
72
  name: "month",
47
- classes: "govuk-input--width-2",
73
+ classes: "govuk-input--width-2 govuk-input--error" if data.isMonthError else "govuk-input--width-2",
48
74
  value: payload['date-of-birth-month'],
49
75
  attributes: { maxlength : 2 }
50
76
  },
51
77
  {
52
78
  label: mssgs.dob_year,
53
79
  name: "year",
54
- classes: "govuk-input--width-4",
80
+ classes: "govuk-input--width-4 govuk-input--error" if data.isYearError else "govuk-input--width-4",
55
81
  value: payload['date-of-birth-year'],
56
82
  attributes: { maxlength : 4 }
57
83
  }
@@ -4,10 +4,12 @@ import Joi from 'joi'
4
4
  import { validation } from '@defra-fish/business-rules-lib'
5
5
  import { addLanguageCodeToUri } from '../../../processors/uri-helper.js'
6
6
  import GetDataRedirect from '../../../handlers/get-data-redirect.js'
7
+ import { dateOfBirthValidator, getDateErrorFlags } from '../../../schema/validators/validators.js'
7
8
 
8
9
  export const getData = async request => {
9
10
  // If we are supplied a permission number, validate it or throw 400
10
11
  const permission = await request.cache().helpers.status.getCurrentPermission()
12
+ const page = await request.cache().helpers.page.getCurrentPermission(IDENTIFY.page)
11
13
 
12
14
  if (permission.referenceNumber) {
13
15
  const validatePermissionNumber = validation.permission
@@ -23,25 +25,23 @@ export const getData = async request => {
23
25
  referenceNumber: permission.referenceNumber,
24
26
  uri: {
25
27
  new: addLanguageCodeToUri(request, NEW_TRANSACTION.uri)
26
- }
28
+ },
29
+ ...getDateErrorFlags(page?.error)
27
30
  }
28
31
  }
29
32
 
30
- const schema = Joi.object({
31
- referenceNumber: validation.permission.permissionNumberUniqueComponentValidator(Joi),
32
- 'date-of-birth': validation.contact.createBirthDateValidator(Joi),
33
- postcode: validation.contact.createOverseasPostcodeValidator(Joi)
34
- }).options({ abortEarly: false, allowUnknown: true })
33
+ export const validator = payload => {
34
+ dateOfBirthValidator(payload)
35
35
 
36
- const validator = async payload => {
37
- const dateOfBirth = `${payload['date-of-birth-year']}-${payload['date-of-birth-month']}-${payload['date-of-birth-day']}`
38
36
  Joi.assert(
39
37
  {
40
- 'date-of-birth': dateOfBirth,
41
38
  postcode: payload.postcode,
42
39
  referenceNumber: payload.referenceNumber
43
40
  },
44
- schema
41
+ Joi.object({
42
+ referenceNumber: validation.permission.permissionNumberUniqueComponentValidator(Joi),
43
+ postcode: validation.contact.createOverseasPostcodeValidator(Joi)
44
+ }).options({ abortEarly: false })
45
45
  )
46
46
  }
47
47
 
@@ -0,0 +1,32 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`dateSchemaInput matches expected format 1`] = `
4
+ Object {
5
+ "day": "1",
6
+ "day-and-month": Object {
7
+ "day": "1",
8
+ "month": "2",
9
+ },
10
+ "day-and-year": Object {
11
+ "day": "1",
12
+ "year": "2023",
13
+ },
14
+ "full-date": Object {
15
+ "day": "1",
16
+ "month": "2",
17
+ "year": "2023",
18
+ },
19
+ "invalid-date": "2023-02-01",
20
+ "month": "2",
21
+ "month-and-year": Object {
22
+ "month": "2",
23
+ "year": "2023",
24
+ },
25
+ "non-numeric": Object {
26
+ "day": "1",
27
+ "month": "2",
28
+ "year": "2023",
29
+ },
30
+ "year": "2023",
31
+ }
32
+ `;
@@ -0,0 +1,64 @@
1
+ import Joi from 'joi'
2
+ import { dateSchemaInput, dateSchema } from '../date.schema.js'
3
+
4
+ describe('dateSchemaInput', () => {
5
+ it('matches expected format', () => {
6
+ expect(dateSchemaInput('1', '2', '2023')).toMatchSnapshot()
7
+ })
8
+
9
+ it.each`
10
+ desc | day | month | year | result
11
+ ${'all empty'} | ${''} | ${''} | ${''} | ${{ 'full-date': { day: undefined, month: undefined, year: undefined } }}
12
+ ${'day and month empty'} | ${''} | ${''} | ${'2020'} | ${{ 'day-and-month': { day: undefined, month: undefined } }}
13
+ ${'day and year empty'} | ${''} | ${'11'} | ${''} | ${{ 'day-and-year': { day: undefined, year: undefined } }}
14
+ ${'month and year empty'} | ${'12'} | ${''} | ${''} | ${{ 'month-and-year': { month: undefined, year: undefined } }}
15
+ ${'day empty'} | ${''} | ${'3'} | ${'2021'} | ${{ day: undefined }}
16
+ ${'month empty'} | ${'4'} | ${''} | ${'2003'} | ${{ month: undefined }}
17
+ ${'year empty'} | ${'15'} | ${'11'} | ${''} | ${{ year: undefined }}
18
+ `('maps empty strings to undefined values when $desc', ({ day, month, year, result }) => {
19
+ expect(dateSchemaInput(day, month, year)).toEqual(expect.objectContaining(result))
20
+ })
21
+ })
22
+
23
+ describe('dateSchema', () => {
24
+ it.each`
25
+ payload | expectedError | payloadDesc
26
+ ${{}} | ${'full-date'} | ${'empty day, month and year'}
27
+ ${{ year: '1' }} | ${'day-and-month'} | ${'empty day and month'}
28
+ ${{ month: '2' }} | ${'day-and-year'} | ${'empty day and year'}
29
+ ${{ day: '3' }} | ${'month-and-year'} | ${'empty month and year'}
30
+ ${{ month: '5', year: '2023' }} | ${'day'} | ${'empty day'}
31
+ ${{ day: '12', year: '2024' }} | ${'month'} | ${'empty month'}
32
+ ${{ day: '15', month: '3' }} | ${'year'} | ${'empty year'}
33
+ ${{ day: 'Ides', month: 'March', year: '44 B.C.' }} | ${'non-numeric.day'} | ${'non-numerics entered'}
34
+ ${{ day: 'Thirteenth', month: '11', year: '1978' }} | ${'non-numeric.day'} | ${'non-numeric day'}
35
+ ${{ day: '29', month: 'MAR', year: '2002' }} | ${'non-numeric.month'} | ${'non-numeric month '}
36
+ ${{ day: '13', month: '1', year: 'Two thousand and five' }} | ${'non-numeric.year'} | ${'non-numeric year'}
37
+ ${{ day: '30', month: '2', year: '1994' }} | ${'invalid-date'} | ${'an invalid date - 1994-02-40'}
38
+ ${{ day: '1', month: '13', year: '2022' }} | ${'invalid-date'} | ${'an invalid date - 2022-13-01'}
39
+ ${{ day: '29', month: '2', year: '2023' }} | ${'invalid-date'} | ${'an invalid date - 1994-02-40'}
40
+ ${{ day: '-1.15', month: '18', year: '22.2222' }} | ${'invalid-date'} | ${'an invalid date - 22.2222-18-1.15'}
41
+ `('Error has $expectedError in details when payload has $payloadDesc', ({ payload: { day, month, year }, expectedError }) => {
42
+ expect(() => {
43
+ Joi.assert(dateSchemaInput(day, month, year), dateSchema)
44
+ }).toThrow(
45
+ expect.objectContaining({
46
+ details: expect.arrayContaining([
47
+ expect.objectContaining({
48
+ path: expectedError.split('.'),
49
+ context: expect.objectContaining({
50
+ label: expectedError,
51
+ key: expectedError.split('.').pop()
52
+ })
53
+ })
54
+ ])
55
+ })
56
+ )
57
+ })
58
+
59
+ it('valid date passes validation', () => {
60
+ expect(() => {
61
+ Joi.assert(dateSchemaInput('12', '10', '1987'), dateSchema)
62
+ }).not.toThrow()
63
+ })
64
+ })
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+ import Joi from 'joi'
3
+
4
+ export const dateSchemaInput = (unparsedDay, unparsedMonth, unparsedYear) => {
5
+ const day = unparsedDay === '' ? undefined : unparsedDay
6
+ const month = unparsedMonth === '' ? undefined : unparsedMonth
7
+ const year = unparsedYear === '' ? undefined : unparsedYear
8
+
9
+ return {
10
+ 'full-date': { day, month, year },
11
+ 'day-and-month': { day, month },
12
+ 'day-and-year': { day, year },
13
+ 'month-and-year': { month, year },
14
+ day,
15
+ month,
16
+ year,
17
+ 'non-numeric': { day, month, year },
18
+ 'invalid-date': `${year}-${(month || '').padStart(2, '0')}-${(day || '').padStart(2, '0')}`
19
+ }
20
+ }
21
+
22
+ export const dateSchema = Joi.object({
23
+ 'full-date': Joi.object()
24
+ .keys({
25
+ day: Joi.any(),
26
+ month: Joi.any(),
27
+ year: Joi.any()
28
+ })
29
+ .or('day', 'month', 'year'),
30
+ 'day-and-month': Joi.object()
31
+ .keys({
32
+ day: Joi.any(),
33
+ month: Joi.any()
34
+ })
35
+ .or('day', 'month'),
36
+ 'day-and-year': Joi.object()
37
+ .keys({
38
+ day: Joi.any(),
39
+ year: Joi.any()
40
+ })
41
+ .or('day', 'year'),
42
+ 'month-and-year': Joi.object()
43
+ .keys({
44
+ month: Joi.any(),
45
+ year: Joi.any()
46
+ })
47
+ .or('month', 'year'),
48
+ day: Joi.any().required(),
49
+ month: Joi.any().required(),
50
+ year: Joi.any().required(),
51
+ 'non-numeric': Joi.object().keys({
52
+ day: Joi.number(),
53
+ month: Joi.number(),
54
+ year: Joi.number()
55
+ }),
56
+ 'invalid-date': Joi.custom((dateToValidate, helpers) => {
57
+ if (new Date(dateToValidate).toISOString() !== `${dateToValidate}T00:00:00.000Z`) {
58
+ throw helpers.error('invalid-date')
59
+ }
60
+
61
+ return dateToValidate
62
+ })
63
+ }).options({ abortEarly: true })