From 6e62e3845e3bc918c1da4bf58d08cf8c7acc9956 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 10 Apr 2026 15:00:48 +0100 Subject: [PATCH 1/6] Enhanced validation for email fields --- .../server/src/routes/forms/contact/email.js | 8 ++-- designer/server/src/routes/forms/create.js | 4 +- .../src/routes/forms/notification-email.js | 9 ++++- designer/server/src/routes/manage/user.js | 12 ++++-- model/src/form/form-audit/index.ts | 8 ++-- model/src/form/form-definition/index.ts | 15 ++------ model/src/form/form-editor/index.ts | 10 +++++ model/src/form/form-metadata/index.ts | 15 ++++---- model/src/utils/helpers.test.js | 37 ++++++++++++++++++- model/src/utils/helpers.ts | 20 ++++++++++ 10 files changed, 104 insertions(+), 34 deletions(-) diff --git a/designer/server/src/routes/forms/contact/email.js b/designer/server/src/routes/forms/contact/email.js index c3ae981794..973bb158c9 100644 --- a/designer/server/src/routes/forms/contact/email.js +++ b/designer/server/src/routes/forms/contact/email.js @@ -1,6 +1,7 @@ import { Scopes, - emailAddressSchema, + UNICODE_EMAIL_ERROR_MESSAGE, + emailAddressNoUnicodeSchema, emailResponseTimeSchema } from '@defra/forms-model' import { StatusCodes } from 'http-status-codes' @@ -20,13 +21,14 @@ export const ROUTE_PATH_EDIT_EMAIL_CONTACT = '/library/{slug}/edit/contact/email' export const emailContactSchema = Joi.object().keys({ - address: emailAddressSchema + address: emailAddressNoUnicodeSchema .required() .when('_delete', { is: true, then: Joi.allow('') }) .messages({ 'string.empty': 'Enter an email address for dedicated support', 'string.email': - 'Enter an email address for dedicated support in the correct format' + 'Enter an email address for dedicated support in the correct format', + 'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE }), responseTime: emailResponseTimeSchema .required() diff --git a/designer/server/src/routes/forms/create.js b/designer/server/src/routes/forms/create.js index 054ec9675b..75e1b667dc 100644 --- a/designer/server/src/routes/forms/create.js +++ b/designer/server/src/routes/forms/create.js @@ -1,5 +1,6 @@ import { Scopes, + UNICODE_EMAIL_ERROR_MESSAGE, getErrorMessage, organisationSchema, slugify, @@ -41,7 +42,8 @@ export const schema = Joi.object().keys({ }), teamEmail: teamEmailSchema.messages({ 'string.empty': 'Enter a shared team email address', - 'string.email': 'Enter a shared team email address in the correct format' + 'string.email': 'Enter a shared team email address in the correct format', + 'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE }) }) diff --git a/designer/server/src/routes/forms/notification-email.js b/designer/server/src/routes/forms/notification-email.js index 71d8492805..b5529b24a0 100644 --- a/designer/server/src/routes/forms/notification-email.js +++ b/designer/server/src/routes/forms/notification-email.js @@ -1,4 +1,8 @@ -import { Scopes, notificationEmailAddressSchema } from '@defra/forms-model' +import { + Scopes, + UNICODE_EMAIL_ERROR_MESSAGE, + notificationEmailAddressSchema +} from '@defra/forms-model' import { StatusCodes } from 'http-status-codes' import Joi from 'joi' @@ -21,7 +25,8 @@ export const schema = Joi.object().keys({ notificationEmail: notificationEmailAddressSchema.required().messages({ 'string.empty': EMPTY_MESSAGE, 'string.email': INCORRECT_FORMAT_MESSAGE, - 'string.pattern.base': INCORRECT_FORMAT_MESSAGE + 'string.pattern.base': INCORRECT_FORMAT_MESSAGE, + 'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE }) }) diff --git a/designer/server/src/routes/manage/user.js b/designer/server/src/routes/manage/user.js index 2d8ec46591..3e622059da 100644 --- a/designer/server/src/routes/manage/user.js +++ b/designer/server/src/routes/manage/user.js @@ -1,4 +1,9 @@ -import { Roles, Scopes } from '@defra/forms-model' +import { + Roles, + Scopes, + UNICODE_EMAIL_ERROR_MESSAGE, + emailAddressNoUnicodeSchema +} from '@defra/forms-model' import Boom from '@hapi/boom' import { StatusCodes } from 'http-status-codes' import Joi from 'joi' @@ -15,11 +20,10 @@ import * as userService from '~/src/services/userService.js' const errorKey = sessionNames.validationFailure.manageUsers const addUserSchema = Joi.object({ - emailAddress: Joi.string() - .email() - .trim() + emailAddress: emailAddressNoUnicodeSchema .required() .messages({ + 'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE, '*': 'Enter an email address in the correct format' }) .description('Email address of user'), diff --git a/model/src/form/form-audit/index.ts b/model/src/form/form-audit/index.ts index 8c9cd76bab..308b82a033 100644 --- a/model/src/form/form-audit/index.ts +++ b/model/src/form/form-audit/index.ts @@ -52,9 +52,9 @@ import { type FormUploadedMessageData, type FormsBackupRequestedMessageData } from '~/src/form/form-audit/types.js' +import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js' import { contactSchema, - emailAddressSchema, emailResponseTimeSchema, idSchema, notificationEmailAddressSchema, @@ -181,7 +181,7 @@ export const formSupportOnlineChanges = Joi.object() export const formSupportEmailChanges = Joi.object() .keys({ - address: emailAddressSchema.optional(), + address: emailAddressNoUnicodeSchema.optional(), responseTime: emailResponseTimeSchema.optional() }) .description('Changes schema for FORM_SUPPORT_EMAIL_UPDATED event') @@ -238,7 +238,7 @@ export const entitlementMessageData = Joi.object().keys( { userId: Joi.string().required(), displayName: Joi.string().required(), - email: Joi.string().email().required(), + email: emailAddressNoUnicodeSchema.required(), roles: Joi.array().items(Joi.string()) } ) @@ -260,7 +260,7 @@ export const excelGenerationMessageData = .keys({ formId: Joi.string().required(), formName: Joi.string().required(), - notificationEmail: Joi.string().email().required() + notificationEmail: emailAddressNoUnicodeSchema.required() }) .required() .description( diff --git a/model/src/form/form-definition/index.ts b/model/src/form/form-definition/index.ts index a780f60708..91776eaebb 100644 --- a/model/src/form/form-definition/index.ts +++ b/model/src/form/form-definition/index.ts @@ -47,6 +47,7 @@ import { type RepeatSchema, type Section } from '~/src/form/form-definition/types.js' +import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js' import { checkErrors } from '~/src/form/form-manager/errors.js' import { FormDefinitionError, @@ -1041,13 +1042,7 @@ const feedbackSchema = Joi.object() .description( 'URL to an external feedback form when not using built-in feedback' ), - emailAddress: Joi.string() - .trim() - .email({ - tlds: { - allow: false - } - }) + emailAddress: emailAddressNoUnicodeSchema .optional() .description('Email address where feedback is sent') }) @@ -1155,8 +1150,7 @@ export const formDefinitionSchema = Joi.object() .optional() .description('Phase banner configuration'), options: optionsSchema.optional().description('Options for the form'), - outputEmail: Joi.string() - .trim() + outputEmail: emailAddressNoUnicodeSchema .email({ tlds: { allow: ['uk'] } }) .optional() .description('Email address where form submissions are sent'), @@ -1165,8 +1159,7 @@ export const formDefinitionSchema = Joi.object() .description('Configuration for submission output format'), outputs: Joi.array() .items({ - emailAddress: Joi.string() - .trim() + emailAddress: emailAddressNoUnicodeSchema .email({ tlds: { allow: ['uk'] } }) .description('Email address where form submissions are sent'), audience: Joi.string() diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 90ab711cdd..221bdacf32 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -16,6 +16,16 @@ import { type GovukFieldUsePostcodeLookup, type GovukStringField } from '~/src/form/form-editor/types.js' +import { preventUnicodeInEmail } from '~/src/utils/helpers.js' + +export const emailAddressNoUnicodeSchema = Joi.string() + .trim() + .email() + .custom((value, helpers) => preventUnicodeInEmail(value, helpers)) + .description('Email address preventing unicode characters') + +export const UNICODE_EMAIL_ERROR_MESSAGE = + 'An invalid special character has been detected. Enter an email address in the correct format' export enum QuestionTypeSubGroup { WrittenAnswerSubGroup = 'writtenAnswerSub', diff --git a/model/src/form/form-metadata/index.ts b/model/src/form/form-metadata/index.ts index d3e5e818d3..36182bcd6f 100644 --- a/model/src/form/form-metadata/index.ts +++ b/model/src/form/form-metadata/index.ts @@ -1,5 +1,6 @@ import Joi from 'joi' +import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js' import { type FormMetadata, type FormMetadataAuthor, @@ -53,7 +54,7 @@ export const teamNameSchema = Joi.string() .required() .description('Name of the team responsible for the form') -export const teamEmailSchema = Joi.string() +export const teamEmailSchema = emailAddressNoUnicodeSchema .email({ tlds: { allow: ['uk'] } }) .trim() .required() @@ -63,9 +64,7 @@ export const phoneSchema = Joi.string() .trim() .description('Phone number for form-related inquiries') -export const emailAddressSchema = Joi.string() - .email() - .trim() +export const emailAddressSchema = emailAddressNoUnicodeSchema .required() .description('Email address for form-related inquiries') @@ -132,10 +131,10 @@ export const termsAndConditionsAgreedSchema = Joi.boolean().description( 'Whether the data protection terms and conditions have been agreed to' ) -export const notificationEmailAddressSchema = Joi.string() - .email() - .trim() - .description('Email address to receive form submission notifications') +export const notificationEmailAddressSchema = + emailAddressNoUnicodeSchema.description( + 'Email address to receive form submission notifications' + ) export const authoredAtSchema = Joi.date() .iso() diff --git a/model/src/utils/helpers.test.js b/model/src/utils/helpers.test.js index 13f9dee56a..8454e0b1a1 100644 --- a/model/src/utils/helpers.test.js +++ b/model/src/utils/helpers.test.js @@ -1,4 +1,8 @@ -import { getErrorMessage, slugify } from '~/src/utils/helpers.js' +import { + getErrorMessage, + preventUnicodeInEmail, + slugify +} from '~/src/utils/helpers.js' describe('Helpers', () => { describe('slugify', () => { @@ -107,4 +111,35 @@ describe('Helpers', () => { ) }) }) + + describe('preventUnicodeInEmail', () => { + const mockHelpers = { + error: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it.each([ + 'first.last@domain.co.uk', + 'first-last@domain.uk', + 'only@domain.uk' + ])('handles a valid emails', (/** @type {string} */ email) => { + // @ts-expect-error - partial helper mock + expect(preventUnicodeInEmail(email, mockHelpers)).toBe(email) + expect(mockHelpers.error).not.toHaveBeenCalled() + }) + + const enDash = '\u2013' + const emDash = '\u2014' + it.each([ + `first${enDash}last@domain.co.uk`, + `first${emDash}}last@domain.uk` + ])('handles an invalid email', (/** @type {string} */ email) => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail(email, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.unicode') + }) + }) }) diff --git a/model/src/utils/helpers.ts b/model/src/utils/helpers.ts index 902ccb2913..aabdd572b3 100644 --- a/model/src/utils/helpers.ts +++ b/model/src/utils/helpers.ts @@ -1,3 +1,4 @@ +import { type CustomHelpers } from 'joi' import slug from 'slug' /** @@ -23,3 +24,22 @@ export function slugify(input = '', options?: slug.Options) { export function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error) } + +/** + * Custom Joi validator to prevent Unicode characters in say an email address. + * Although Joi has Joi.email({ allowUnicode: false }), we can't differentiate from a general + * email format error or a Unicode error - hance the custom validator to allow a different error message + * @param {CustomHelpers} value + * @param {unknown} helpers + */ +export function preventUnicodeInEmail( + value: unknown, + helpers: CustomHelpers +) { + if (!value || typeof value !== 'string') { + return helpers.error('any.required') + } + const invalidCharsRegex = /[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~@-]/ + const invalid = invalidCharsRegex.exec(value) + return invalid ? helpers.error('string.unicode') : value +} From 80b31de92c59e29fd143e90a085ec14d8a2aa32e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 10 Apr 2026 15:12:11 +0100 Subject: [PATCH 2/6] Revised the error message after review --- model/src/form/form-editor/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 221bdacf32..4d645bb591 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -25,7 +25,7 @@ export const emailAddressNoUnicodeSchema = Joi.string() .description('Email address preventing unicode characters') export const UNICODE_EMAIL_ERROR_MESSAGE = - 'An invalid special character has been detected. Enter an email address in the correct format' + 'The email address you entered includes invalid characters, for example, long dashes' export enum QuestionTypeSubGroup { WrittenAnswerSubGroup = 'writtenAnswerSub', From 2169ba440800999dd2c42cdbdcbeb50e331ae135 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 10 Apr 2026 15:35:50 +0100 Subject: [PATCH 3/6] Refactored to resolve build issue --- model/src/form/form-definition/constants.ts | 2 ++ model/src/form/form-definition/index.ts | 7 ++++--- model/src/form/form-editor/index.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 model/src/form/form-definition/constants.ts diff --git a/model/src/form/form-definition/constants.ts b/model/src/form/form-definition/constants.ts new file mode 100644 index 0000000000..b0be34eb6c --- /dev/null +++ b/model/src/form/form-definition/constants.ts @@ -0,0 +1,2 @@ +export const MIN_NUMBER_OF_REPEAT_ITEMS = 1 +export const MAX_NUMBER_OF_REPEAT_ITEMS = 200 diff --git a/model/src/form/form-definition/index.ts b/model/src/form/form-definition/index.ts index 91776eaebb..d7008b5fda 100644 --- a/model/src/form/form-definition/index.ts +++ b/model/src/form/form-definition/index.ts @@ -25,6 +25,10 @@ import { type RelativeDateValueData, type RelativeDateValueDataV2 } from '~/src/conditions/types.js' +import { + MAX_NUMBER_OF_REPEAT_ITEMS, + MIN_NUMBER_OF_REPEAT_ITEMS +} from '~/src/form/form-definition/constants.js' import { isConditionListItemRefValueData, isFormDefinition @@ -349,9 +353,6 @@ const conditionSchema = Joi.object() ) }) -export const MIN_NUMBER_OF_REPEAT_ITEMS = 1 -export const MAX_NUMBER_OF_REPEAT_ITEMS = 200 - export const conditionDataSchemaV2 = Joi.object() .description('Condition definition') .keys({ diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 4d645bb591..7f6a07b143 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -5,7 +5,7 @@ import { ComponentType } from '~/src/components/enums.js' import { MAX_NUMBER_OF_REPEAT_ITEMS, MIN_NUMBER_OF_REPEAT_ITEMS -} from '~/src/form/form-definition/index.js' +} from '~/src/form/form-definition/constants.js' import { type FormEditorInputCheckAnswersSettings, type FormEditorInputPage, From d45c7219df7c75870947f46023dc8659e28acb6e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 10 Apr 2026 15:45:15 +0100 Subject: [PATCH 4/6] Fix export issue + extra tests --- model/src/form/form-definition/index.ts | 5 +++++ model/src/utils/helpers.test.js | 12 ++++++++++++ model/src/utils/helpers.ts | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/model/src/form/form-definition/index.ts b/model/src/form/form-definition/index.ts index d7008b5fda..2a5cace512 100644 --- a/model/src/form/form-definition/index.ts +++ b/model/src/form/form-definition/index.ts @@ -1238,6 +1238,11 @@ export const formDefinitionV2Schema = formDefinitionSchema }) .description('Form definition schema for V2') +export { + MAX_NUMBER_OF_REPEAT_ITEMS, + MIN_NUMBER_OF_REPEAT_ITEMS +} from '~/src/form/form-definition/constants.js' + // Maintain compatibility with legacy named export // E.g. `import { Schema } from '@defra/forms-model'` export const Schema = formDefinitionSchema diff --git a/model/src/utils/helpers.test.js b/model/src/utils/helpers.test.js index 8454e0b1a1..09bde5c375 100644 --- a/model/src/utils/helpers.test.js +++ b/model/src/utils/helpers.test.js @@ -121,6 +121,18 @@ describe('Helpers', () => { jest.clearAllMocks() }) + it('should handle undefined', () => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail(undefined, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') + }) + + it('should handle not a string', () => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail({ abc: 123 }, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') + }) + it.each([ 'first.last@domain.co.uk', 'first-last@domain.uk', diff --git a/model/src/utils/helpers.ts b/model/src/utils/helpers.ts index aabdd572b3..b9fae19790 100644 --- a/model/src/utils/helpers.ts +++ b/model/src/utils/helpers.ts @@ -37,7 +37,7 @@ export function preventUnicodeInEmail( helpers: CustomHelpers ) { if (!value || typeof value !== 'string') { - return helpers.error('any.required') + return helpers.error('string.empty') } const invalidCharsRegex = /[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~@-]/ const invalid = invalidCharsRegex.exec(value) From ffc72a44cbe853cee5b8524b9fa9960b74aba4ad Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 13 Apr 2026 16:32:43 +0100 Subject: [PATCH 5/6] Restructured slightly --- model/src/form/form-editor/index.ts | 2 +- model/src/form/utils/index.ts | 1 + model/src/form/utils/prevent-unicode.js | 19 ++++++++ model/src/form/utils/prevent-unicode.test.js | 44 ++++++++++++++++++ model/src/utils/helpers.test.js | 49 +------------------- model/src/utils/helpers.ts | 20 -------- package-lock.json | 9 ---- 7 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 model/src/form/utils/prevent-unicode.js create mode 100644 model/src/form/utils/prevent-unicode.test.js diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 7f6a07b143..133db1beef 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -16,7 +16,7 @@ import { type GovukFieldUsePostcodeLookup, type GovukStringField } from '~/src/form/form-editor/types.js' -import { preventUnicodeInEmail } from '~/src/utils/helpers.js' +import { preventUnicodeInEmail } from '~/src/index.js' export const emailAddressNoUnicodeSchema = Joi.string() .trim() diff --git a/model/src/form/utils/index.ts b/model/src/form/utils/index.ts index c95d0f404d..fac55e1deb 100644 --- a/model/src/form/utils/index.ts +++ b/model/src/form/utils/index.ts @@ -1,2 +1,3 @@ export { findStartPage } from '~/src/form/utils/find-start-page.js' export { findDefinitionListFromComponent } from '~/src/form/utils/list.js' +export { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js' diff --git a/model/src/form/utils/prevent-unicode.js b/model/src/form/utils/prevent-unicode.js new file mode 100644 index 0000000000..415f3ad694 --- /dev/null +++ b/model/src/form/utils/prevent-unicode.js @@ -0,0 +1,19 @@ +/** + * Custom Joi validator to prevent Unicode characters in say an email address. + * Although Joi has Joi.email({ allowUnicode: false }), we can't differentiate from a general + * email format error or a Unicode error - hance the custom validator to allow a different error message + * @param {unknown} value + * @param {CustomHelpers} helpers + */ +export function preventUnicodeInEmail(value, helpers) { + if (!value || typeof value !== 'string') { + return helpers.error('string.empty') + } + const invalidCharsRegex = /[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~@-]/ + const invalid = invalidCharsRegex.exec(value) + return invalid ? helpers.error('string.unicode') : value +} + +/** + * @import { type CustomHelpers } from 'joi' + */ diff --git a/model/src/form/utils/prevent-unicode.test.js b/model/src/form/utils/prevent-unicode.test.js new file mode 100644 index 0000000000..5ec0663151 --- /dev/null +++ b/model/src/form/utils/prevent-unicode.test.js @@ -0,0 +1,44 @@ +import { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js' + +describe('preventUnicodeInEmail', () => { + const mockHelpers = { + error: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should handle undefined', () => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail(undefined, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') + }) + + it('should handle not a string', () => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail({ abc: 123 }, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') + }) + + it.each([ + 'first.last@domain.co.uk', + 'first-last@domain.uk', + 'only@domain.uk' + ])('handles a valid emails', (/** @type {string} */ email) => { + // @ts-expect-error - partial helper mock + expect(preventUnicodeInEmail(email, mockHelpers)).toBe(email) + expect(mockHelpers.error).not.toHaveBeenCalled() + }) + + const enDash = '\u2013' + const emDash = '\u2014' + it.each([`first${enDash}last@domain.co.uk`, `first${emDash}}last@domain.uk`])( + 'handles an invalid email', + (/** @type {string} */ email) => { + // @ts-expect-error - partial helper mock + preventUnicodeInEmail(email, mockHelpers) + expect(mockHelpers.error).toHaveBeenCalledWith('string.unicode') + } + ) +}) diff --git a/model/src/utils/helpers.test.js b/model/src/utils/helpers.test.js index 09bde5c375..13f9dee56a 100644 --- a/model/src/utils/helpers.test.js +++ b/model/src/utils/helpers.test.js @@ -1,8 +1,4 @@ -import { - getErrorMessage, - preventUnicodeInEmail, - slugify -} from '~/src/utils/helpers.js' +import { getErrorMessage, slugify } from '~/src/utils/helpers.js' describe('Helpers', () => { describe('slugify', () => { @@ -111,47 +107,4 @@ describe('Helpers', () => { ) }) }) - - describe('preventUnicodeInEmail', () => { - const mockHelpers = { - error: jest.fn() - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should handle undefined', () => { - // @ts-expect-error - partial helper mock - preventUnicodeInEmail(undefined, mockHelpers) - expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') - }) - - it('should handle not a string', () => { - // @ts-expect-error - partial helper mock - preventUnicodeInEmail({ abc: 123 }, mockHelpers) - expect(mockHelpers.error).toHaveBeenCalledWith('string.empty') - }) - - it.each([ - 'first.last@domain.co.uk', - 'first-last@domain.uk', - 'only@domain.uk' - ])('handles a valid emails', (/** @type {string} */ email) => { - // @ts-expect-error - partial helper mock - expect(preventUnicodeInEmail(email, mockHelpers)).toBe(email) - expect(mockHelpers.error).not.toHaveBeenCalled() - }) - - const enDash = '\u2013' - const emDash = '\u2014' - it.each([ - `first${enDash}last@domain.co.uk`, - `first${emDash}}last@domain.uk` - ])('handles an invalid email', (/** @type {string} */ email) => { - // @ts-expect-error - partial helper mock - preventUnicodeInEmail(email, mockHelpers) - expect(mockHelpers.error).toHaveBeenCalledWith('string.unicode') - }) - }) }) diff --git a/model/src/utils/helpers.ts b/model/src/utils/helpers.ts index b9fae19790..902ccb2913 100644 --- a/model/src/utils/helpers.ts +++ b/model/src/utils/helpers.ts @@ -1,4 +1,3 @@ -import { type CustomHelpers } from 'joi' import slug from 'slug' /** @@ -24,22 +23,3 @@ export function slugify(input = '', options?: slug.Options) { export function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error) } - -/** - * Custom Joi validator to prevent Unicode characters in say an email address. - * Although Joi has Joi.email({ allowUnicode: false }), we can't differentiate from a general - * email format error or a Unicode error - hance the custom validator to allow a different error message - * @param {CustomHelpers} value - * @param {unknown} helpers - */ -export function preventUnicodeInEmail( - value: unknown, - helpers: CustomHelpers -) { - if (!value || typeof value !== 'string') { - return helpers.error('string.empty') - } - const invalidCharsRegex = /[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~@-]/ - const invalid = invalidCharsRegex.exec(value) - return invalid ? helpers.error('string.unicode') : value -} diff --git a/package-lock.json b/package-lock.json index 1c1450fb2f..d0495dd45e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29623,15 +29623,6 @@ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", "license": "ISC" }, - "node_modules/preact": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", - "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", From 1e130257b020bd47ef97824531f389e47747e83e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 13 Apr 2026 16:57:53 +0100 Subject: [PATCH 6/6] Fixed tests --- model/src/form/form-editor/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 133db1beef..65d6480c76 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -16,7 +16,7 @@ import { type GovukFieldUsePostcodeLookup, type GovukStringField } from '~/src/form/form-editor/types.js' -import { preventUnicodeInEmail } from '~/src/index.js' +import { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js' export const emailAddressNoUnicodeSchema = Joi.string() .trim()