From 2693cfc03a91b161cb24acbb689d973e11b8603c Mon Sep 17 00:00:00 2001 From: Joakim Larsson Date: Mon, 12 Dec 2022 10:48:24 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20updates=20throws=20on=20validati?= =?UTF-8?q?on=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../updaters/__test/email-updater.test.ts | 11 +++++- .../updaters/__test/phone-updater.test.ts | 25 +++++-------- src/about-me/person/updaters/email-updater.ts | 35 ++++++++++++------ src/about-me/person/updaters/phone-updater.ts | 37 ++++++++++++------- .../person/updaters/validation-error.ts | 9 +++++ 5 files changed, 75 insertions(+), 42 deletions(-) create mode 100644 src/about-me/person/updaters/validation-error.ts diff --git a/src/about-me/person/updaters/__test/email-updater.test.ts b/src/about-me/person/updaters/__test/email-updater.test.ts index 681112a..cbcdeda 100644 --- a/src/about-me/person/updaters/__test/email-updater.test.ts +++ b/src/about-me/person/updaters/__test/email-updater.test.ts @@ -43,7 +43,7 @@ describe('emailUpdater', () => { }, }) }) - it('ignores malformed emails', async () => { + it('throws on malformed emails', async () => { const updater = createEmailUpdater() const initialEmail: Email = { @@ -52,6 +52,14 @@ describe('emailUpdater', () => { verifiedDate: '2020-02-02', } + await expect(() => updater.updatePerson( + makePerson({ + email: initialEmail, + }), + { email: 'badly formattted email' })) + .rejects + .toThrow('Validation error') + /* const updated = await updater.updatePerson( makePerson({ email: initialEmail, @@ -61,5 +69,6 @@ describe('emailUpdater', () => { expect(updated).toMatchObject({ email: initialEmail, }) + */ }) }) \ No newline at end of file diff --git a/src/about-me/person/updaters/__test/phone-updater.test.ts b/src/about-me/person/updaters/__test/phone-updater.test.ts index d8071e3..4ee1c59 100644 --- a/src/about-me/person/updaters/__test/phone-updater.test.ts +++ b/src/about-me/person/updaters/__test/phone-updater.test.ts @@ -45,10 +45,9 @@ describe('phoneUpdater', () => { }) it.each([ - [null], ['bad number'], ['123'], - ])('ignores update on invalid number "%s"', async phoneNumber => { + ])('throws on update with invalid number "%s"', async phoneNumber => { const updater = createPhoneUpdater('SE') const initialPhone: Phone = { @@ -57,18 +56,16 @@ describe('phoneUpdater', () => { verifiedDate: '2020-02-02', } - const updated = await updater.updatePerson( + await expect(() => updater.updatePerson( makePerson({ phone: initialPhone, }), - { phoneNumber }) - - expect(updated).toMatchObject({ - phone: initialPhone, - }) + { phoneNumber })) + .rejects + .toThrow('Validation error') }) - it('ignores malformed phone numbers', async () => { + it('throws on malformed phone numbers', async () => { const updater = createPhoneUpdater('SE') const initialPhone: Phone = { @@ -77,14 +74,12 @@ describe('phoneUpdater', () => { verifiedDate: '2020-02-02', } - const updated = await updater.updatePerson( + await expect(() => updater.updatePerson( makePerson({ phone: initialPhone, }), - { phoneNumber: 'badly formattted phone number' }) - - expect(updated).toMatchObject({ - phone: initialPhone, - }) + { phoneNumber: 'badly formattted phone number' })) + .rejects + .toThrow('Validation error') }) }) diff --git a/src/about-me/person/updaters/email-updater.ts b/src/about-me/person/updaters/email-updater.ts index 0f08004..e759cbd 100644 --- a/src/about-me/person/updaters/email-updater.ts +++ b/src/about-me/person/updaters/email-updater.ts @@ -1,22 +1,33 @@ import { randomUUID } from 'crypto' import * as EmailValidator from 'email-validator' import { Email, PersonUpdater } from '../types' +import { throwValidationErrorForField } from './validation-error' + +const normalizeStringInput = (s?: string): string|null => (typeof s === 'string') + ? s.trim() + : null export const createEmailUpdater = (): PersonUpdater => ({ updatePerson: async (person, update) => ({ ...person, - email: patchEmail(person?.email, update?.email?.trim()), + email: patchEmail(person?.email || null, getValidatedEmail(normalizeStringInput(update?.email))), }), }) -const patchEmail = (email?: Email, update?: string): Email | null => - update === '' - ? null - : update && EmailValidator.validate(update) && (update !== email?.address) - ? ({ - address: update, - isVerified: false, - verifiedDate: null, - verificationCode: randomUUID(), - }) - : email +const getValidatedEmail = (email: string): string|null|false => email && EmailValidator.validate(email) && email + +const patchEmail = (email: Email, update: string|null|false): Email | null => + update === null + ? email + : update === '' + ? null + : !update + ? (throwValidationErrorForField('email'), email) + : update !== email?.address + ? ({ + address: update, + isVerified: false, + verifiedDate: null, + verificationCode: randomUUID(), + }) + : email diff --git a/src/about-me/person/updaters/phone-updater.ts b/src/about-me/person/updaters/phone-updater.ts index f641f8b..1e5d123 100644 --- a/src/about-me/person/updaters/phone-updater.ts +++ b/src/about-me/person/updaters/phone-updater.ts @@ -2,32 +2,41 @@ import { getEnv } from '@helsingborg-stad/gdi-api-node' import { parsePhoneNumber } from 'awesome-phonenumber' import { randomUUID } from 'crypto' import { PersonUpdater, Phone } from '../types' +import { throwValidationErrorForField } from './validation-error' export const createPhoneUpdaterFromEnv = (regionCode = 'SE'): PersonUpdater => createPhoneUpdater(getEnv('PHONENUMBER_REGION', { fallback: regionCode })) +const normalizeStringInput = (s?: string): string|null => (typeof s === 'string') + ? s.trim() + : null + export const createPhoneUpdater = (regionCode: string): PersonUpdater => ({ updatePerson: async (person, update) => ({ ...person, - phone: patchPhone(person?.phone, getValidatedPhoneNumber(update?.phoneNumber?.trim(), regionCode)), + phone: patchPhone(person?.phone || null, getValidatedPhoneNumber(normalizeStringInput(update?.phoneNumber), regionCode)), }), }) const getValidatedPhoneNumber = (number: string, regionCode: string): string => { - if (number === '') { - return '' + if (!number) { + return number } - const parsed = parsePhoneNumber(number || '', regionCode) + const parsed = parsePhoneNumber(number.trim(), regionCode) return parsed?.isPossible() && parsed?.getNumber('e164') } const patchPhone = (phone?: Phone, update?: string): Phone | null => - update === '' - ? null - : update && (update !== phone?.number) - ? ({ - number: update, - isVerified: false, - verifiedDate: null, - verificationCode: randomUUID(), - }) - : phone + update === null + ? phone + : update === '' + ? null + : !update + ? (throwValidationErrorForField('phoneNumber'), phone) + : (update !== phone?.number) + ? ({ + number: update, + isVerified: false, + verifiedDate: null, + verificationCode: randomUUID(), + }) + : phone diff --git a/src/about-me/person/updaters/validation-error.ts b/src/about-me/person/updaters/validation-error.ts new file mode 100644 index 0000000..892b5ec --- /dev/null +++ b/src/about-me/person/updaters/validation-error.ts @@ -0,0 +1,9 @@ +import { GraphQLError } from 'graphql' + +export const throwValidationErrorForField = (fieldName: string): void => { + throw new GraphQLError('Validation error', { + extensions: { + 'validation-failed-for-field': fieldName, + }, + }) +} \ No newline at end of file