@defra-fish/gafl-webapp-service 1.57.0-rc.1 → 1.57.0-rc.10

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.
Files changed (25) hide show
  1. package/package.json +4 -4
  2. package/src/locales/cy.json +27 -8
  3. package/src/locales/en.json +29 -8
  4. package/src/pages/concessions/date-of-birth/__tests__/route.spec.js +75 -20
  5. package/src/pages/concessions/date-of-birth/date-of-birth.njk +35 -9
  6. package/src/pages/concessions/date-of-birth/route.js +10 -14
  7. package/src/pages/licence-details/licence-to-start/__tests__/route.spec.js +65 -2
  8. package/src/pages/licence-details/licence-to-start/licence-to-start.njk +38 -12
  9. package/src/pages/licence-details/licence-to-start/route.js +14 -32
  10. package/src/pages/renewals/identify/__tests__/identity.spec.js +3 -0
  11. package/src/pages/renewals/identify/__tests__/route.spec.js +126 -0
  12. package/src/pages/renewals/identify/identify.njk +34 -8
  13. package/src/pages/renewals/identify/route.js +10 -10
  14. package/src/pages/summary/licence-summary/__tests__/__snapshots__/route.spec.js.snap +0 -12
  15. package/src/pages/summary/licence-summary/__tests__/route.spec.js +26 -12
  16. package/src/pages/summary/licence-summary/route.js +2 -1
  17. package/src/schema/__tests__/__snapshots__/date.schema.test.js.snap +32 -0
  18. package/src/schema/__tests__/date.schema.test.js +64 -0
  19. package/src/schema/date.schema.js +63 -0
  20. package/src/schema/validators/__tests__/validators.spec.js +208 -0
  21. package/src/schema/validators/validators.js +65 -0
  22. package/src/services/payment/__test__/govuk-pay-service.spec.js +8 -8
  23. package/src/services/payment/govuk-pay-service.js +3 -3
  24. package/src/pages/renewals/identify/__tests__/identity.next-page.spec.js +0 -36
  25. package/src/pages/renewals/identify/__tests__/route-spec.js +0 -38
@@ -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
 
@@ -1128,18 +1128,6 @@ Array [
1128
1128
  },
1129
1129
  },
1130
1130
  Object {
1131
- "actions": Object {
1132
- "items": Array [
1133
- Object {
1134
- "attributes": Object {
1135
- "id": "change-licence-length",
1136
- },
1137
- "href": "/buy/licence-length",
1138
- "text": "contact_summary_change",
1139
- "visuallyHiddenText": "licence_summary_length",
1140
- },
1141
- ],
1142
- },
1143
1131
  "key": Object {
1144
1132
  "text": "licence_summary_length",
1145
1133
  },
@@ -7,7 +7,11 @@ import { licenceTypeDisplay } from '../../../../processors/licence-type-display.
7
7
  import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js'
8
8
  import mappingConstants from '../../../../processors/mapping-constants.js'
9
9
  import { displayPermissionPrice } from '../../../../processors/price-display.js'
10
+ import { hasJunior } from '../../../../processors/concession-helper.js'
10
11
 
12
+ jest.mock('../../../../processors/concession-helper.js', () => ({
13
+ hasJunior: jest.fn(() => false)
14
+ }))
11
15
  jest.mock('../../../../processors/licence-type-display.js', () => ({
12
16
  licenceTypeDisplay: jest.fn(() => 'Special Canal Licence, Shopping Trollies and Old Wellies')
13
17
  }))
@@ -370,20 +374,30 @@ describe('licence-summary > route', () => {
370
374
  )
371
375
  })
372
376
 
377
+ it('calls hasJunior with permission', async () => {
378
+ const currentPermission = getMockNewPermission()
379
+ const mockRequest = getMockRequest({ currentPermission })
380
+
381
+ await getData(mockRequest)
382
+
383
+ expect(hasJunior).toHaveBeenCalledWith(currentPermission)
384
+ })
385
+
373
386
  describe('licence summary rows', () => {
374
387
  it.each`
375
- desc | currentPermission
376
- ${'1 year renewal'} | ${getMockPermission()}
377
- ${'1 year new licence'} | ${getMockNewPermission()}
378
- ${'1 year senior renewal'} | ${getMockSeniorPermission()}
379
- ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }}
380
- ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }}
381
- ${'Junior licence'} | ${getMockJuniorPermission()}
382
- ${'Blue badge concession'} | ${getMockBlueBadgePermission()}
383
- ${'Continuing permission'} | ${getMockContinuingPermission()}
384
- ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }}
385
- ${'1 year new three rod licence '} | ${{ ...getMockNewPermission(), numberOfRods: '3' }}
386
- `('creates licence summary name rows for $desc', async ({ currentPermission }) => {
388
+ desc | currentPermission | junior
389
+ ${'1 year renewal'} | ${getMockPermission()} | ${false}
390
+ ${'1 year new licence'} | ${getMockNewPermission()} | ${false}
391
+ ${'1 year senior renewal'} | ${getMockSeniorPermission()} | ${false}
392
+ ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }} | ${false}
393
+ ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }} | ${false}
394
+ ${'Junior licence'} | ${getMockJuniorPermission()} | ${true}
395
+ ${'Blue badge concession'} | ${getMockBlueBadgePermission()} | ${false}
396
+ ${'Continuing permission'} | ${getMockContinuingPermission()} | ${false}
397
+ ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }} | ${false}
398
+ ${'1 year new three rod licence '} | ${{ ...getMockNewPermission(), numberOfRods: '3' }} | ${false}
399
+ `('creates licence summary name rows for $desc', async ({ currentPermission, junior }) => {
400
+ hasJunior.mockReturnValueOnce(junior)
387
401
  const mockRequest = getMockRequest({ currentPermission })
388
402
  const data = await getData(mockRequest)
389
403
  expect(data.licenceSummaryRows).toMatchSnapshot()
@@ -22,6 +22,7 @@ import { CONCESSION, CONCESSION_PROOF } from '../../../processors/mapping-consta
22
22
  import { nextPage } from '../../../routes/next-page.js'
23
23
  import { addLanguageCodeToUri } from '../../../processors/uri-helper.js'
24
24
  import { displayPermissionPrice } from '../../../processors/price-display.js'
25
+ import { hasJunior } from '../../../processors/concession-helper.js'
25
26
  import db from 'debug'
26
27
  const debug = db('webapp:licence-summary')
27
28
 
@@ -115,7 +116,7 @@ class RowGenerator {
115
116
  generateLicenceLengthRow () {
116
117
  const args = ['licence_summary_length', this.labels[`licence_type_${this.permission.licenceLength.toLowerCase()}`]]
117
118
 
118
- if (this.permission.numberOfRods !== '3') {
119
+ if (this.permission.numberOfRods !== '3' && !hasJunior(this.permission)) {
119
120
  args.push(LICENCE_LENGTH.uri, 'change-licence-length')
120
121
  }
121
122
 
@@ -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 })