From 90715c15d352e66f312274e9b5bbf9f038eaed2b Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 13 Jul 2023 15:50:56 +0100 Subject: [PATCH] Chore: Validate workflow stage names are unique --- .../pages/CreateView/CreateView.js | 6 +- .../pages/EditView/EditView.js | 16 +- .../utils/tests/validateWorkflow.test.js | 148 ++++++++++++++++++ ...alidationSchema.js => validateWorkflow.js} | 35 ++++- 4 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/validateWorkflow.test.js rename packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/{getWorkflowValidationSchema.js => validateWorkflow.js} (57%) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js index 6384f06bcf..ed2fcc286e 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js @@ -20,7 +20,7 @@ import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { REDUX_NAMESPACE } from '../../constants'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { reducer, initialState } from '../../reducer'; -import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; +import { validateWorkflow } from '../../utils/validateWorkflow'; export function ReviewWorkflowsCreateView() { const { formatMessage } = useIntl(); @@ -108,7 +108,9 @@ export function ReviewWorkflowsCreateView() { submitForm(); } }, - validationSchema: getWorkflowValidationSchema({ formatMessage }), + validate(values) { + return validateWorkflow({ values, formatMessage }); + }, }); useInjectReducer(REDUX_NAMESPACE, reducer); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js index b13935f9a3..f93af60205 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js @@ -25,7 +25,7 @@ import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { REDUX_NAMESPACE } from '../../constants'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { reducer, initialState } from '../../reducer'; -import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; +import { validateWorkflow } from '../../utils/validateWorkflow'; export function ReviewWorkflowsEditView() { const { workflowId } = useParams(); @@ -113,11 +113,11 @@ export function ReviewWorkflowsEditView() { if (currentWorkflowHasDeletedServerStages) { setIsConfirmDeleteDialogOpen(true); } else if (limits?.workflows && meta?.workflowCount > parseInt(limits.workflows, 10)) { - /** - * If the current license has a limit, check if the total count of workflows - * exceeds that limit and display the limits modal instead of sending the - * update, because it would throw an API error. - */ + /** + * If the current license has a limit, check if the total count of workflows + * exceeds that limit and display the limits modal instead of sending the + * update, because it would throw an API error. + */ setShowLimitModal('workflow'); /** @@ -134,7 +134,9 @@ export function ReviewWorkflowsEditView() { submitForm(); } }, - validationSchema: getWorkflowValidationSchema({ formatMessage }), + validate(values) { + return validateWorkflow({ values, formatMessage }); + }, }); useInjectReducer(REDUX_NAMESPACE, reducer); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/validateWorkflow.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/validateWorkflow.test.js new file mode 100644 index 0000000000..9fe921b9da --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/validateWorkflow.test.js @@ -0,0 +1,148 @@ +import { validateWorkflow } from '../validateWorkflow'; + +const generateStringWithLength = (length) => new Array(length + 1).join('_'); + +const formatMessage = (message) => message.defaultMessage; + +const setup = (values) => validateWorkflow({ values, formatMessage }); + +describe('Settings | Review Workflows | validateWorkflow()', () => { + test('name: valid input', async () => { + expect(await setup({ name: 'short name' })).toEqual(true); + }); + + test('name: empty', async () => { + expect(await setup({ name: '' })).toMatchInlineSnapshot(` + { + "name": "name is a required field", + } + `); + }); + + test('name: too long', async () => { + expect(await setup({ name: generateStringWithLength(256) })).toMatchInlineSnapshot(` + { + "name": "Name can not be longer than 255 characters", + } + `); + }); + + test('contentTypes: valid input', async () => { + expect(await setup({ name: 'name', contentTypes: ['1'] })).toEqual(true); + }); + + test('stages: empty array', async () => { + expect( + await setup({ + name: 'name', + stages: [], + }) + ).toMatchInlineSnapshot(` + { + "stages": "stages field must have at least 1 items", + } + `); + }); + + test('stages.name: valid input', async () => { + expect( + await setup({ + name: 'name', + stages: [ + { + name: 'stage-1', + color: '#ffffff', + }, + ], + }) + ).toEqual(true); + }); + + test('stages.name: duplicated name', async () => { + expect( + await setup({ + name: 'name', + stages: [ + { + name: 'stage-1', + color: '#ffffff', + }, + + { + name: 'stage-1', + color: '#ffffff', + }, + ], + }) + ).toMatchInlineSnapshot(` + { + "stages": [ + { + "name": "Stage name must be unique", + }, + { + "name": "Stage name must be unique", + }, + ], + } + `); + }); + + test('stages.name: name too long', async () => { + expect( + await setup({ + name: 'name', + stages: [ + { + name: generateStringWithLength(256), + color: '#ffffff', + }, + ], + }) + ).toMatchInlineSnapshot(` + { + "stages": [ + { + "name": "Name can not be longer than 255 characters", + }, + ], + } + `); + }); + + test('stages.color: valid input', async () => { + expect( + await setup({ + name: 'name', + stages: [ + { + name: 'stage-1', + color: '#ffffff', + }, + ], + }) + ).toEqual(true); + }); + + test('stages.color: invalid hex code', async () => { + expect( + await setup({ + name: 'name', + stages: [ + { + name: 'stage-1', + color: 'non-hex-code', + }, + ], + }) + ).toMatchInlineSnapshot(` + { + "stages": [ + { + "color": "stages[0].color must match the following: "/^#(?:[0-9a-fA-F]{3}){1,2}$/i"", + }, + ], + } + `); + }); +}); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js similarity index 57% rename from packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js rename to packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js index e7b2368b61..cdac8102ce 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js @@ -1,7 +1,8 @@ +import set from 'lodash/set'; import * as yup from 'yup'; -export function getWorkflowValidationSchema({ formatMessage }) { - return yup.object({ +export async function validateWorkflow({ values, formatMessage }) { + const schema = yup.object({ contentTypes: yup.array().of(yup.string()), name: yup .string() @@ -32,6 +33,20 @@ export function getWorkflowValidationSchema({ formatMessage }) { id: 'Settings.review-workflows.validation.stage.max-length', defaultMessage: 'Name can not be longer than 255 characters', }) + ) + .test( + 'unique-name', + formatMessage({ + id: 'Settings.review-workflows.validation.stage.duplicate', + defaultMessage: 'Stage name must be unique', + }), + function (stageName) { + const { + options: { context }, + } = this; + + return context.stages.filter((stage) => stage.name === stageName).length === 1; + } ), color: yup .string() @@ -46,4 +61,20 @@ export function getWorkflowValidationSchema({ formatMessage }) { ) .min(1), }); + + try { + await schema.validate(values, { abortEarly: false, context: values }); + + return true; + } catch (error) { + let errors = {}; + + if (error instanceof yup.ValidationError) { + error.inner.forEach((error) => { + set(errors, error.path, error.message); + }); + } + + return errors; + } }