@defra-fish/business-rules-lib 1.49.0-rc.2 → 1.49.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/business-rules-lib",
3
- "version": "1.49.0-rc.2",
3
+ "version": "1.49.0-rc.4",
4
4
  "description": "Shared business rules for the rod licensing digital services",
5
5
  "type": "module",
6
6
  "engines": {
@@ -37,5 +37,5 @@
37
37
  "moment": "^2.29.1",
38
38
  "uuid": "^8.3.2"
39
39
  },
40
- "gitHead": "f4b3ab8367b4dfd3c6db0db118728b21344b5d8a"
40
+ "gitHead": "5c9d0205887751b34c54c9e28d86c09ae16183ca"
41
41
  }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as contactValidation from './validators/contact.validators.js'
2
+ import * as dateValidation from './validators/date.validators.js'
2
3
  import * as permissionValidation from './validators/permission.validators.js'
3
4
  export * from './util/ages.js'
4
5
  export * from './util/permissions.js'
@@ -6,5 +7,6 @@ export * from './constants.js'
6
7
 
7
8
  export const validation = {
8
9
  contact: contactValidation,
10
+ date: dateValidation,
9
11
  permission: permissionValidation
10
12
  }
@@ -5,6 +5,9 @@ import moment from 'moment'
5
5
  const INVALID_DATE_ERROR_MESSAGE = '"value" must be in [YYYY-MM-DD] format'
6
6
 
7
7
  describe('contact validators', () => {
8
+ beforeEach(() => {
9
+ jest.resetAllMocks()
10
+ })
8
11
  describe('birthDateValidator', () => {
9
12
  const validDate = moment().subtract(1, 'day')
10
13
 
@@ -128,6 +131,24 @@ describe('contact validators', () => {
128
131
  '"value" must contain at least 3 alpha characters'
129
132
  )
130
133
  })
134
+
135
+ it.each([
136
+ 'M̵i̵c̵h̵a̵e̵l̵',
137
+ 'M̷i̷c̷h̷a̷e̷l̷',
138
+ 'M͟i͟c͟h͟a͟e͟l͟',
139
+ '𝔐𝔦𝔠𝔥𝔞𝔢𝔩',
140
+ '𝕄𝕚𝕔𝕙𝕒𝕖𝕝',
141
+ 'ןǝɐɥɔıW',
142
+ 'Ⓜⓘⓒⓗⓐⓔⓛ',
143
+ '🄼🄸🄲🄷🄰🄴🄻',
144
+ 'Mɪᴄʜᴀᴇʟ',
145
+ 'ᴹᶦᶜʰᵃᵉˡ',
146
+ '𖢑𖥣𖥐𖦙𖧥𖠢ꛚ',
147
+ 'ℳ𝒾𝒸𝒽𝒶ℯ𝓁',
148
+ '𝙼𝚒𝚌𝚑𝚊𝚎𝚕'
149
+ ])('prohibits a string with non-standard characters: %s', async c => {
150
+ await expect(contactValidation.createFirstNameValidator(Joi).validateAsync(c)).rejects.toThrow()
151
+ })
131
152
  })
132
153
 
133
154
  describe('lastNameValidator', () => {
@@ -229,6 +250,24 @@ describe('contact validators', () => {
229
250
  '"value" must contain at least 3 alpha characters'
230
251
  )
231
252
  })
253
+
254
+ it.each([
255
+ 'M̵i̵c̵h̵a̵e̵l̵',
256
+ 'M̷i̷c̷h̷a̷e̷l̷',
257
+ 'M͟i͟c͟h͟a͟e͟l͟',
258
+ '𝔐𝔦𝔠𝔥𝔞𝔢𝔩',
259
+ '𝕄𝕚𝕔𝕙𝕒𝕖𝕝',
260
+ 'ןǝɐɥɔıW',
261
+ 'Ⓜⓘⓒⓗⓐⓔⓛ',
262
+ '🄼🄸🄲🄷🄰🄴🄻',
263
+ 'Mɪᴄʜᴀᴇʟ',
264
+ 'ᴹᶦᶜʰᵃᵉˡ',
265
+ '𖢑𖥣𖥐𖦙𖧥𖠢ꛚ',
266
+ 'ℳ𝒾𝒸𝒽𝒶ℯ𝓁',
267
+ '𝙼𝚒𝚌𝚑𝚊𝚎𝚕'
268
+ ])('prohibits a string with non-standard characters: %s', async c => {
269
+ await expect(contactValidation.createLastNameValidator(Joi).validateAsync(c)).rejects.toThrow()
270
+ })
232
271
  })
233
272
 
234
273
  describe('emailValidator', () => {
@@ -241,6 +280,28 @@ describe('contact validators', () => {
241
280
  '"value" must be a valid email'
242
281
  )
243
282
  })
283
+
284
+ it('allows a range of unicode characters from plane 1', async () => {
285
+ const internationStr = 'æçéñøķť@email.com'
286
+ await expect(contactValidation.createEmailValidator(Joi).validateAsync(internationStr)).resolves.toEqual('æçéñøķť@email.com')
287
+ })
288
+
289
+ it.each([
290
+ 'ᵐᵢᶜₕᵃₑˡ@ᵉₘᵃᵢˡ.ᶜₒᵐ',
291
+ '𝓂𝒾𝒸𝒽𝒶ℯ𝓁@ℯ𝓂𝒶𝒾𝓁.𝒸ℴ𝓂',
292
+ 'm̶i̶c̶h̶a̶e̶l̶@̶e̶m̶a̶i̶l̶.̶c̶o̶m̶',
293
+ 'm̷i̷c̷h̷a̷e̷l̷@̷e̷m̷a̷i̷l̷.̷c̷o̷m̷',
294
+ '𝖒𝖎𝖈𝖍𝖆𝖊𝖑@𝖊𝖒𝖆𝖎𝖑.𝖈𝖔𝖒',
295
+ 'm̲i̲c̲h̲a̲e̲l̲@̲e̲m̲a̲i̲l̲.̲c̲o̲m̲',
296
+ 'ꕮꕯꖀꖾꗇꗍꝆ@ꗍꕮꗇꕯꝆ.ꖀꗞꕮ',
297
+ '🅼🅘🅲🅗🅰🅔🅻@🅴🅜🅰🅘🅻.🅲🅞🅼',
298
+ 'ɯoɔ˙ןıɐɯǝ@ןǝɐɥɔıɯ',
299
+ 'ꮇꮖꮯꮋꭺꭼꮮ@ꭼꮇꭺꮖꮮ.ꮯꮎꮇ',
300
+ 'michael@email.com',
301
+ 'ₘᵢcₕaₑₗ@ₑₘaᵢₗ.cₒₘ'
302
+ ])('prohibits a string with non-standard characters: %s', async c => {
303
+ await expect(contactValidation.createEmailValidator(Joi).validateAsync(c)).rejects.toThrow()
304
+ })
244
305
  })
245
306
 
246
307
  describe('mobilePhoneValidator', () => {
@@ -251,6 +312,24 @@ describe('contact validators', () => {
251
312
  it.each(['test', '07700 test'])('rejects the invalid number %s', async number => {
252
313
  await expect(contactValidation.createMobilePhoneValidator(Joi).validateAsync(number)).rejects.toThrow()
253
314
  })
315
+
316
+ it.each([
317
+ '𝟘𝟟𝟟𝟘𝟘 𝟡𝟘𝟘𝟘𝟠𝟠',
318
+ '0𝟟7𝟘0 9𝟘0𝟘8𝟠',
319
+ '0𝟽𝟽00 𝟿000𝟾𝟾',
320
+ '0̵7̵7̵0̵0̵ ̵9̵0̵0̵0̵8̵8̵',
321
+ '0͟7͟7͟0͟0͟ ͟9͟0͟0͟0͟8͟8͟',
322
+ '0̸7̸7̸0̸0̸ ̸9̸0̸0̸0̸8̸8̸',
323
+ '07700 900088',
324
+ '⁰⁷⁷⁰⁰ ⁹⁰⁰⁰⁸⁸',
325
+ '⓿➐➐⓿⓿ ➒⓿⓿⓿➑➑',
326
+ '+ㄐㄐワワㄖㄖ ㄢㄖㄖㄖ曰曰',
327
+ '07700👏 900088',
328
+ '0̐̈7̐̈7̐̈0̐̈0̐̈ ̐̈9̐̈0̐̈0̐̈0̐̈8̐̈8̐̈',
329
+ '+4④7⑦0⓪ ⑨0⓪0⑧8'
330
+ ])('prohibits a string with non-standard characters: %s', async c => {
331
+ await expect(contactValidation.createMobilePhoneValidator(Joi).validateAsync(c)).rejects.toThrow()
332
+ })
254
333
  })
255
334
 
256
335
  describe('ukPostcodeValidator', () => {
@@ -265,33 +344,74 @@ describe('contact validators', () => {
265
344
  it('expects a minimum of 1 character', async () => {
266
345
  await expect(contactValidation.createUKPostcodeValidator(Joi).validateAsync('')).rejects.toThrow('"value" is not allowed to be empty')
267
346
  })
347
+
268
348
  it('expects a maximum of 12 characters', async () => {
269
349
  await expect(contactValidation.createUKPostcodeValidator(Joi).validateAsync('0123456789ABC')).rejects.toThrow(
270
350
  '"value" length must be less than or equal to 12 characters long'
271
351
  )
272
352
  })
353
+
273
354
  it('expects postcodes to conform to the pattern used in the UK', async () => {
274
355
  await expect(contactValidation.createUKPostcodeValidator(Joi).validateAsync('0123456789')).rejects.toThrow(
275
356
  /fails to match the required pattern/
276
357
  )
277
358
  })
359
+
360
+ it.each([
361
+ 'A⃣B⃣1⃣2⃣ 1⃣A⃣B⃣',
362
+ '🄐🄑⑴⑵ ⑴🄐🄑',
363
+ '丹乃丨己 丨丹乃',
364
+ 'A͢B͢1͢2͢ ͢1͢A͢B͢',
365
+ 'A⃟B⃟1⃟2⃟ 1⃟A⃟B⃟',
366
+ 'AB12👏 1AB',
367
+ '🅐Ⓑ➊② ①🅐Ⓑ',
368
+ '𝘼𝐵𝟭𝟮 1𝘼𝘽',
369
+ '🅰𝙱12 1𝕬𝓑',
370
+ '1ᴬAᴮB¹²',
371
+ 'ᴬᴮ¹² ¹ᴬᴮ',
372
+ 'q∀1 21q∀',
373
+ 'A̶B̶1̶2̶ ̶1̶A̶B̶'
374
+ ])('prohibits a string with non-standard characters: %s', async c => {
375
+ await expect(contactValidation.createUKPostcodeValidator(Joi).validateAsync(c)).rejects.toThrow()
376
+ })
278
377
  })
279
378
 
280
379
  describe('overseasPostcodeValidator', () => {
281
380
  it('converts to uppercase and trims', async () => {
282
381
  await expect(contactValidation.createOverseasPostcodeValidator(Joi).validateAsync('a ')).resolves.toEqual('A')
283
382
  })
383
+
284
384
  it('expects a minimum of 1 character', async () => {
285
385
  await expect(contactValidation.createOverseasPostcodeValidator(Joi).validateAsync('')).rejects.toThrow(
286
386
  '"value" is not allowed to be empty'
287
387
  )
288
388
  })
389
+
289
390
  it('expects a maximum of 12 characters', async () => {
290
391
  await expect(contactValidation.createOverseasPostcodeValidator(Joi).validateAsync('123456789AAAA')).rejects.toThrow()
291
392
  })
393
+
292
394
  it('will not accept special characters', async () => {
293
395
  await expect(contactValidation.createOverseasPostcodeValidator(Joi).validateAsync('12£4')).rejects.toThrow()
294
396
  })
397
+
398
+ it.each([
399
+ 'A⃣B⃣1⃣2⃣ 1⃣A⃣B⃣',
400
+ '🄐🄑⑴⑵ ⑴🄐🄑',
401
+ '丹乃丨己 丨丹乃',
402
+ 'A͢B͢1͢2͢ ͢1͢A͢B͢',
403
+ 'A⃟B⃟1⃟2⃟ 1⃟A⃟B⃟',
404
+ 'AB12👏 1AB',
405
+ '🅐Ⓑ➊② ①🅐Ⓑ',
406
+ '𝘼𝐵𝟭𝟮 1𝘼𝘽',
407
+ '🅰𝙱12 1𝕬𝓑',
408
+ '1ᴬAᴮB¹²',
409
+ 'ᴬᴮ¹² ¹ᴬᴮ',
410
+ 'q∀1 21q∀',
411
+ 'A̶B̶1̶2̶ ̶1̶A̶B̶'
412
+ ])('prohibits a string with non-standard characters: %s', async c => {
413
+ await expect(contactValidation.createOverseasPostcodeValidator(Joi).validateAsync(c)).rejects.toThrow()
414
+ })
295
415
  })
296
416
 
297
417
  describe('premisesValidator', () => {
@@ -308,6 +428,23 @@ describe('contact validators', () => {
308
428
  '"value" length must be less than or equal to 50 characters long'
309
429
  )
310
430
  })
431
+
432
+ it.each([
433
+ '15 𐝥ꗞꕷꗍ ꖀꗞꖡꖡꗇꗱꗍ',
434
+ '15 Rose Cottage',
435
+ '¹⁵ ᴿᵒˢᵉ ᶜᵒᵗᵗᵃᵍᵉ',
436
+ '15 ᖇᐤᔆᕪ ᐸᐤᐩᐩᐞᕐᕪ',
437
+ '15👏 Rose👏 Cottage',
438
+ '1̶5̶ ̶R̶o̶s̶e̶ ̶C̶o̶t̶t̶a̶g̶e̶',
439
+ '1̲5̲ ̲R̲o̲s̲e̲ ̲C̲o̲t̲t̲a̲g̲e̲',
440
+ '1̸5̸ ̸R̸o̸s̸e̸ ̸C̸o̸t̸t̸a̸g̸e̸',
441
+ '➊➎ 🅡🅞🅢🅔 🅒🅞🅣🅣🅐🅖🅔',
442
+ '①➎ 🅡ⓞ🅢ⓔ Ⓒ🅞ⓣ🅣ⓐ🅖ⓔ',
443
+ '1⑤ 𝑅𝐨𝑠ⓔ 𝒞𝓸𝘁𝕥𝒶ℊ🄴',
444
+ '𝟏𝟓 𝐑𝐨𝐬𝐞 𝐂𝐨𝐭𝐭𝐚𝐠𝐞'
445
+ ])('prohibits a string with non-standard characters: %s', async c => {
446
+ await expect(contactValidation.createPremisesValidator(Joi).validateAsync(c)).rejects.toThrow()
447
+ })
311
448
  })
312
449
 
313
450
  describe('streetValidator', () => {
@@ -324,6 +461,22 @@ describe('contact validators', () => {
324
461
  '"value" length must be less than or equal to 100 characters long'
325
462
  )
326
463
  })
464
+
465
+ it.each([
466
+ 'Bond👏 Street',
467
+ '🅑ⓞ🅝ⓓ ⓢⓣⓡⓔⓔⓣ',
468
+ 'Bond Street',
469
+ 'ᴮᵒⁿᵈ ˢᵗʳᵉᵉᵗ',
470
+ 'B̲o̲n̲d̲ ̲S̲t̲r̲e̲e̲t̲',
471
+ 'B̸o̸n̸d̸ ̸S̸t̸r̸e̸e̸t̸',
472
+ '🅑🅞🅝🅓 🅢🅣🅡🅔🅔🅣',
473
+ 'B𝑜𝓷d S𝓉𝓇𝑒𝑒𝓉',
474
+ 'もB囗o几n问d 丂S匕t尺r乇eモe匕t',
475
+ 'B⃠o⃠n⃠d⃠ S⃠t⃠r⃠e⃠e⃠t⃠',
476
+ '𝐁𝐨𝐧𝐝 𝐒𝐭𝐫𝐞𝐞𝐭'
477
+ ])('prohibits a string with non-standard characters: %s', async c => {
478
+ await expect(contactValidation.createStreetValidator(Joi).validateAsync(c)).rejects.toThrow()
479
+ })
327
480
  })
328
481
 
329
482
  describe('localityValidator', () => {
@@ -340,6 +493,20 @@ describe('contact validators', () => {
340
493
  '"value" length must be less than or equal to 100 characters long'
341
494
  )
342
495
  })
496
+
497
+ it.each([
498
+ 'M̲a̲y̲f̲a̲i̲r̲',
499
+ 'M̸a̸y̸f̸a̸i̸r̸',
500
+ 'Mayfair',
501
+ 'ᴹᵃʸᶠᵃⁱʳ',
502
+ '🅜🅐🅨🅕🅐🅘🅡',
503
+ '🅜ⓐ🅨ⓕⓐⓘⓡ',
504
+ '爪M丹aリy乍f丹a工i尺r',
505
+ 'M⃠a⃠y⃠f⃠a⃠i⃠r⃠',
506
+ '𝐌𝐚𝐲𝐟𝐚𝐢𝐫'
507
+ ])('prohibits a string with non-standard characters: %s', async c => {
508
+ await expect(contactValidation.createLocalityValidator(Joi).validateAsync(c)).rejects.toThrow()
509
+ })
343
510
  })
344
511
 
345
512
  describe('townValidator', () => {
@@ -373,6 +540,13 @@ describe('contact validators', () => {
373
540
  '"value" length must be less than or equal to 100 characters long'
374
541
  )
375
542
  })
543
+
544
+ it.each(['London', 'ᴸᵒⁿᵈᵒⁿ', '𝐋𝐨𝐧𝐝𝐨𝐧', 'ㄥL口o几n冂d口o几n', 'L⃠o⃠n⃠d⃠o⃠n⃠', '🅛🅞🅝🅓🅞🅝', 'L̸o̸n̸d̸o̸n̸', 'L̲o̲n̲d̲o̲n̲'])(
545
+ 'prohibits a string with non-standard characters: %s',
546
+ async c => {
547
+ await expect(contactValidation.createTownValidator(Joi).validateAsync(c)).rejects.toThrow()
548
+ }
549
+ )
376
550
  })
377
551
 
378
552
  describe('nationalInsuranceNumberValidator', () => {
@@ -381,11 +555,28 @@ describe('contact validators', () => {
381
555
  'AB 12 34 56 A'
382
556
  )
383
557
  })
558
+
384
559
  it('Disallows invalid NI number QQ123456A', async () => {
385
560
  await expect(contactValidation.createNationalInsuranceNumberValidator(Joi).validateAsync('QQ123456A')).rejects.toThrow()
386
561
  })
562
+
387
563
  it('Disallows invalid NI number BG123456A', async () => {
388
564
  await expect(contactValidation.createNationalInsuranceNumberValidator(Joi).validateAsync('QQ123456A')).rejects.toThrow()
389
565
  })
566
+
567
+ it.each([
568
+ 'A̲B̲1̲2̲3̲4̲5̲6̲A̲',
569
+ 'A̸B̸1̸2̸3̸4̸5̸6̸A̸',
570
+ 'AB123456A',
571
+ 'ᴬᴮ¹²³⁴⁵⁶ᴬ',
572
+ '🅐🅑①②③④⑤⑥🅐',
573
+ '🅐ⓑ①②③④⑤⑥ⓐ',
574
+ 'A⃠B⃠1⃠2⃠3⃠4⃠5⃠6⃠A⃠',
575
+ '升A马B丨1己2ヨ3ㄐ4丂5石6刄A',
576
+ '✌️AB123456A✌️',
577
+ '𝐀𝐁𝟏𝟐𝟑𝟒𝟓𝟔𝐀'
578
+ ])('prohibits a string with non-standard characters: %s', async c => {
579
+ await expect(contactValidation.createNationalInsuranceNumberValidator(Joi).validateAsync(c)).rejects.toThrow()
580
+ })
390
581
  })
391
582
  })
@@ -78,6 +78,12 @@ const createDateStringValidator = joi =>
78
78
  }
79
79
  })
80
80
 
81
+ const allowedUnicodeBlocks = '\\u0000-\\u024F'
82
+ const forbiddenCharsRegex = new RegExp(`[^${allowedUnicodeBlocks}\\s'’()-]`, 'gu')
83
+ const forbiddenEmailRegex = new RegExp(`[^A-Za-z0-9\\s'’@._${allowedUnicodeBlocks}]`, 'gu')
84
+ const forbiddenCharsNumbersRegex = new RegExp(`[^A-Za-z0-9\\s${allowedUnicodeBlocks}]`, 'gu')
85
+ const forbiddenMobileRegex = /[^+()0-9\-.\s]+/g
86
+
81
87
  /**
82
88
  * Create a validator to check a contact's birth date
83
89
  *
@@ -92,7 +98,8 @@ export const createBirthDateValidator = joi => createDateStringValidator(joi).tr
92
98
  * @param {Joi.Root} joi the joi validator used by the consuming project
93
99
  * @returns {Joi.StringSchema}
94
100
  */
95
- export const createEmailValidator = joi => joi.string().trim().email().max(100).lowercase().example('person@example.com')
101
+ export const createEmailValidator = joi =>
102
+ checkCopyPasteValidator(joi, forbiddenEmailRegex).string().allowable().trim().email().max(100).lowercase().example('person@example.com')
96
103
 
97
104
  export const mobilePhoneRegex = /^[+]*[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/
98
105
  /**
@@ -101,7 +108,10 @@ export const mobilePhoneRegex = /^[+]*[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/
101
108
  * @param {Joi.Root} joi the joi validator used by the consuming project
102
109
  * @returns {Joi.StringSchema}
103
110
  */
104
- export const createMobilePhoneValidator = joi => joi.string().trim().pattern(mobilePhoneRegex).example('+44 7700 900088')
111
+ export const createMobilePhoneValidator = joi =>
112
+ checkCopyPasteValidator(joi, forbiddenMobileRegex).string().allowable().trim().pattern(mobilePhoneRegex).example('person@example.com')
113
+
114
+ const maxPremises = 50
105
115
 
106
116
  /**
107
117
  * Create a validator to check a contact's address premises
@@ -109,7 +119,16 @@ export const createMobilePhoneValidator = joi => joi.string().trim().pattern(mob
109
119
  * @param {Joi.Root} joi the joi validator used by the consuming project
110
120
  * @returns {Joi.StringSchema}
111
121
  */
112
- export const createPremisesValidator = joi => joi.string().trim().min(1).max(50).external(toTitleCase()).required().example('Example House')
122
+ export const createPremisesValidator = joi =>
123
+ checkCopyPasteValidator(joi, forbiddenCharsNumbersRegex)
124
+ .string()
125
+ .allowable()
126
+ .trim()
127
+ .min(1)
128
+ .max(maxPremises)
129
+ .external(toTitleCase())
130
+ .required()
131
+ .example('Example House')
113
132
 
114
133
  /**
115
134
  * Create a validator to check a contact's address street
@@ -117,7 +136,15 @@ export const createPremisesValidator = joi => joi.string().trim().min(1).max(50)
117
136
  * @param {Joi.Root} joi the joi validator used by the consuming project
118
137
  * @returns {Joi.StringSchema}
119
138
  */
120
- export const createStreetValidator = joi => joi.string().trim().max(100).external(toTitleCase()).empty('').example('Example Street')
139
+ export const createStreetValidator = joi =>
140
+ checkCopyPasteValidator(joi, forbiddenCharsRegex)
141
+ .string()
142
+ .allowable()
143
+ .trim()
144
+ .max(100)
145
+ .external(toTitleCase())
146
+ .empty('')
147
+ .example('Example Street')
121
148
 
122
149
  /**
123
150
  * Create a validator to check a contact's address locality
@@ -125,7 +152,15 @@ export const createStreetValidator = joi => joi.string().trim().max(100).externa
125
152
  * @param {Joi.Root} joi the joi validator used by the consuming project
126
153
  * @returns {Joi.StringSchema}
127
154
  */
128
- export const createLocalityValidator = joi => joi.string().trim().max(100).external(toTitleCase()).empty('').example('Near Sample')
155
+ export const createLocalityValidator = joi =>
156
+ checkCopyPasteValidator(joi, forbiddenCharsRegex)
157
+ .string()
158
+ .allowable()
159
+ .trim()
160
+ .max(100)
161
+ .external(toTitleCase())
162
+ .empty('')
163
+ .example('Near Sample')
129
164
 
130
165
  /**
131
166
  * Create a validator to check a contact's address town
@@ -134,8 +169,9 @@ export const createLocalityValidator = joi => joi.string().trim().max(100).exter
134
169
  * @returns {Joi.StringSchema}
135
170
  */
136
171
  export const createTownValidator = joi =>
137
- joi
172
+ checkCopyPasteValidator(joi, forbiddenCharsRegex)
138
173
  .string()
174
+ .allowable()
139
175
  .trim()
140
176
  .max(100)
141
177
  .external(toTitleCase(['under', 'upon', 'in', 'on', 'cum', 'next', 'the', 'en', 'le', 'super']))
@@ -144,6 +180,7 @@ export const createTownValidator = joi =>
144
180
 
145
181
  export const ukPostcodeRegex = /^([A-PR-UWYZ][0-9]{1,2}[A-HJKPSTUW]?|[A-PR-UWYZ][A-HK-Y][0-9]{1,2}[ABEHMNPRVWXY]?)\s{0,6}([0-9][A-Z]{2})$/i
146
182
  export const overseasPostcodeRegex = /^([a-zA-Z0-9 ]{1,12})$/i
183
+ const maxPostcode = 12
147
184
 
148
185
  /**
149
186
  * Create a validator to check a contact's postcode
@@ -154,7 +191,17 @@ export const overseasPostcodeRegex = /^([a-zA-Z0-9 ]{1,12})$/i
154
191
  * @returns {Joi.StringSchema}
155
192
  */
156
193
  export const createUKPostcodeValidator = joi =>
157
- joi.string().trim().min(1).max(12).required().pattern(ukPostcodeRegex).replace(ukPostcodeRegex, '$1 $2').uppercase().example('AB12 3CD')
194
+ checkCopyPasteValidator(joi, forbiddenCharsNumbersRegex)
195
+ .string()
196
+ .allowable()
197
+ .trim()
198
+ .min(1)
199
+ .max(maxPostcode)
200
+ .required()
201
+ .pattern(ukPostcodeRegex)
202
+ .replace(ukPostcodeRegex, '$1 $2')
203
+ .uppercase()
204
+ .example('AB12 3CD')
158
205
 
159
206
  /**
160
207
  * Create a validator to check/format overseas postcodes
@@ -162,7 +209,15 @@ export const createUKPostcodeValidator = joi =>
162
209
  * @returns {Joi.StringSchema}
163
210
  */
164
211
  export const createOverseasPostcodeValidator = joi =>
165
- joi.string().trim().min(1).max(12).uppercase().required().pattern(overseasPostcodeRegex)
212
+ checkCopyPasteValidator(joi, forbiddenCharsNumbersRegex)
213
+ .string()
214
+ .allowable()
215
+ .trim()
216
+ .min(1)
217
+ .max(maxPostcode)
218
+ .uppercase()
219
+ .required()
220
+ .pattern(overseasPostcodeRegex)
166
221
 
167
222
  const nameStringSubstitutions = [
168
223
  /*
@@ -251,7 +306,7 @@ const createNameStringValidator = (joi, { minimumLength }) =>
251
306
  return helpers.error('string.min')
252
307
  }
253
308
  value = standardiseName(value)
254
- if (!nameStringRegex.test(value)) {
309
+ if (!nameStringRegex.test(value) || forbiddenCharsRegex.test(value)) {
255
310
  return helpers.error('string.forbidden')
256
311
  }
257
312
  return value
@@ -302,8 +357,9 @@ const ukNINORegEx =
302
357
  /^([ABCEGHJ-PRSTW-Z][ABCEGHJ-NPRSTW-Z])(?<!(?:BG|GB|KN|NK|NT|TN|ZZ))\s?([0-9]{2})\s{0,3}([0-9]{2})\s{0,3}([0-9]{2})\s{0,3}([ABCD])$/
303
358
 
304
359
  export const createNationalInsuranceNumberValidator = joi =>
305
- joi
360
+ checkCopyPasteValidator(joi, forbiddenCharsNumbersRegex)
306
361
  .string()
362
+ .allowable()
307
363
  .trim()
308
364
  .uppercase()
309
365
  .pattern(ukNINORegEx)
@@ -311,3 +367,22 @@ export const createNationalInsuranceNumberValidator = joi =>
311
367
  .required()
312
368
  .description('A UK national insurance number')
313
369
  .example('NH 12 34 56 A')
370
+
371
+ const checkCopyPasteValidator = (joi, forbiddenRegex) =>
372
+ joi.extend({
373
+ type: 'string',
374
+ base: joi.string(),
375
+ rules: {
376
+ allowable: {
377
+ validate (value, helpers) {
378
+ if (forbiddenRegex.test(value)) {
379
+ return helpers.error('string.forbidden')
380
+ }
381
+ return value
382
+ }
383
+ }
384
+ },
385
+ messages: {
386
+ 'string.forbidden': '{{#label}} contains forbidden characters'
387
+ }
388
+ })