From b572b07431991c1d95680834c005a25f27d97560 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Wed, 2 Oct 2024 19:23:03 +0200 Subject: [PATCH] allow Veeam capacity to have decimal: Change validation schema change useCapacity hook to accept float instead of int Tests for validation in veeam configuration and in edit modal Change Input : step to 0.01 and maximum capacity to 1024 (to take into account the binary base of capacity) --- .../Veeam/VeeamCapacityFormSection.tsx | 4 +- .../Veeam/VeeamCapacityModal.test.tsx | 48 +++++++++++ .../ui-elements/Veeam/VeeamCapacityModal.tsx | 13 ++- .../Veeam/VeeamCapacityOverviewRow.test.tsx | 2 +- .../Veeam/VeeamCapacityOverviewRow.tsx | 5 +- .../Veeam/VeeamConfiguration.test.tsx | 83 +++++++++++++++++-- .../ui-elements/Veeam/VeeamConfiguration.tsx | 13 ++- .../ui-elements/Veeam/useCapacityUnit.ts | 8 +- 8 files changed, 158 insertions(+), 18 deletions(-) diff --git a/src/react/ui-elements/Veeam/VeeamCapacityFormSection.tsx b/src/react/ui-elements/Veeam/VeeamCapacityFormSection.tsx index 7f1c4903f..b4b633369 100644 --- a/src/react/ui-elements/Veeam/VeeamCapacityFormSection.tsx +++ b/src/react/ui-elements/Veeam/VeeamCapacityFormSection.tsx @@ -96,8 +96,8 @@ export const VeeamCapacityFormSection = ({ type="number" size="1/3" min={1} - max={999} - step={1} + max={1024} + step={0.01} autoFocus={autoFocusEnabled} {...register('capacity')} /> diff --git a/src/react/ui-elements/Veeam/VeeamCapacityModal.test.tsx b/src/react/ui-elements/Veeam/VeeamCapacityModal.test.tsx index 1c6d0f048..f3f834086 100644 --- a/src/react/ui-elements/Veeam/VeeamCapacityModal.test.tsx +++ b/src/react/ui-elements/Veeam/VeeamCapacityModal.test.tsx @@ -67,6 +67,54 @@ describe('VeeamCapacityModal', () => { ); }); }); + it('should validate capacity value correctly : number less than 1', async () => { + fireEvent.click(selectors.editBtn()); + fireEvent.change(selectors.capacityInput(), { target: { value: '0' } }); + + await waitFor(async () => { + expect(selectors.editModalBtn()).toBeDisabled(); + expect( + screen.getByText(/"capacity" must be larger than or equal to 1/i), + ).toBeInTheDocument(); + }); + }); + it('should validate capacity value correctly : number greater than 1024', async () => { + fireEvent.click(selectors.editBtn()); + fireEvent.change(selectors.capacityInput(), { target: { value: '1025' } }); + + await waitFor(async () => { + expect(selectors.editModalBtn()).toBeDisabled(); + expect( + screen.getByText(/"capacity" must be less than or equal to 1024/i), + ).toBeInTheDocument(); + }); + }); + it('should validate capacity value correctly : number with more than 2 decimals', async () => { + fireEvent.click(selectors.editBtn()); + fireEvent.change(selectors.capacityInput(), { + target: { value: '12.345' }, + }); + + await waitFor(async () => { + expect(selectors.editModalBtn()).toBeDisabled(); + expect( + screen.getByText(/"capacity" must have at most 2 decimals/i), + ).toBeInTheDocument(); + }); + }); + it('should validate capacity value correctly : number is required', async () => { + fireEvent.click(selectors.editBtn()); + fireEvent.change(selectors.capacityInput(), { + target: { value: '' }, + }); + + await waitFor(async () => { + expect(selectors.editModalBtn()).toBeDisabled(); + expect( + screen.getByText(/"capacity" must be a number/i), + ).toBeInTheDocument(); + }); + }); it('should display error toast if mutation failed', async () => { server.use( diff --git a/src/react/ui-elements/Veeam/VeeamCapacityModal.tsx b/src/react/ui-elements/Veeam/VeeamCapacityModal.tsx index 8a88240b3..1745e2053 100644 --- a/src/react/ui-elements/Veeam/VeeamCapacityModal.tsx +++ b/src/react/ui-elements/Veeam/VeeamCapacityModal.tsx @@ -24,7 +24,18 @@ import { import { getCapacityBytes, useCapacityUnit } from './useCapacityUnit'; const schema = Joi.object({ - capacity: Joi.number().required().min(1).max(999).integer(), + capacity: Joi.number() + .required() + .min(1) + .max(1024) + .custom((value, helpers) => { + if (!Number.isInteger(value * 100)) { + return helpers.message({ + custom: '"capacity" must have at most 2 decimals', + }); + } + return value; + }), capacityUnit: Joi.string().required(), }); diff --git a/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.test.tsx b/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.test.tsx index 9881a3c47..2c175ac18 100644 --- a/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.test.tsx +++ b/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.test.tsx @@ -61,7 +61,7 @@ describe('VeeamCapacityOverviewRow', () => { expect(screen.getByText('Max repository Capacity')).toBeInTheDocument(); }); - expect(screen.getByText('100 GiB')).toBeInTheDocument(); + expect(screen.getByText('100.00 GiB')).toBeInTheDocument(); }); it('should not render the row if SOSAPI is not enabled', () => { diff --git a/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.tsx b/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.tsx index d55f2fd6d..9d2b197d3 100644 --- a/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.tsx +++ b/src/react/ui-elements/Veeam/VeeamCapacityOverviewRow.tsx @@ -40,13 +40,12 @@ export const VeeamCapacityOverviewRow = ({ const xml = veeamObject?.Body?.toString(); const regex = /([\s\S]*?)<\/Capacity>/; const matches = xml?.match(regex); - const capacity = parseInt( + const capacity = parseFloat( new DOMParser() ?.parseFromString(xml || '', 'application/xml') ?.querySelector('Capacity')?.textContent || matches?.[1] || '0', - 10, ); if (isSOSAPIEnabled) { @@ -60,7 +59,7 @@ export const VeeamCapacityOverviewRow = ({ ) : veeamObjectStatus === 'error' ? ( 'Error' ) : ( - + )} {veeamObjectStatus === 'success' && ( diff --git a/src/react/ui-elements/Veeam/VeeamConfiguration.test.tsx b/src/react/ui-elements/Veeam/VeeamConfiguration.test.tsx index 42a418265..4f7332640 100644 --- a/src/react/ui-elements/Veeam/VeeamConfiguration.test.tsx +++ b/src/react/ui-elements/Veeam/VeeamConfiguration.test.tsx @@ -93,8 +93,8 @@ describe('Veeam Configuration UI', () => { ).toBeInTheDocument(); //expect the immutable backup toogle to be active expect(screen.getByLabelText('enableImmutableBackup')).toBeEnabled(); - // verify the max capacity input is prefilled with 4 GiB - expect(selectors.maxCapacityInput()).toHaveValue(4); + // verify the max capacity input is prefilled with 80% of binary value of clusterCapacity: jestSetupAfterEnv.tsx + expect(selectors.maxCapacityInput()).toHaveValue(3.73); expect(screen.getByText(/GiB/i)).toBeInTheDocument(); await waitFor(() => { expect(selectors.continueButton()).toBeEnabled(); @@ -147,7 +147,7 @@ describe('Veeam Configuration UI', () => { accountName: 'Veeam', application: 'Veeam Backup for Microsoft 365', bucketName: 'veeam-bucket', - capacityBytes: '4294967296', + capacityBytes: '4005057004', enableImmutableBackup: false, }); }); @@ -190,17 +190,86 @@ describe('Veeam Configuration UI', () => { expect(selectors.accountNameInput()).toHaveValue('Veeam'); }); }); - - it('should throw validation error if the max capacity is not integer', async () => { + it('should show validation error if the max capacity is less than 1', async () => { + //S + mockUseAccountsImplementation(); + renderVeeamConfigurationForm(); + //E + await userEvent.clear(selectors.maxCapacityInput()); + await userEvent.type(selectors.maxCapacityInput(), '0'); + //V + expect( + screen.getByText(/"capacity" must be larger than or equal to 1/i), + ).toBeInTheDocument(); + await waitFor(() => { + expect(selectors.continueButton()).toBeDisabled(); + }); + }); + it('should show validation error and disable continue button if the max capacity is more than 1024', async () => { + //S + mockUseAccountsImplementation(); + renderVeeamConfigurationForm(); + await userEvent.type(selectors.accountNameInput(), 'Veeam'); + await userEvent.type(selectors.repositoryInput(), 'veeam-bucket'); + //E + await userEvent.clear(selectors.maxCapacityInput()); + await userEvent.type(selectors.maxCapacityInput(), '1025'); + //V + expect( + screen.getByText(/"capacity" must be less than or equal to 1024/i), + ).toBeInTheDocument(); + await waitFor(() => { + expect(selectors.continueButton()).toBeDisabled(); + }); + }); + it('should show validation error and disable continue button if the max capacity is not a number', async () => { //S mockUseAccountsImplementation(); renderVeeamConfigurationForm(); + await userEvent.type(selectors.accountNameInput(), 'Veeam'); + await userEvent.type(selectors.repositoryInput(), 'veeam-bucket'); //E await userEvent.clear(selectors.maxCapacityInput()); - await userEvent.type(selectors.maxCapacityInput(), '4.666'); + await userEvent.type(selectors.maxCapacityInput(), 'abc'); //V expect( - screen.getByText(/"capacity" must be an integer/i), + screen.getByText(/"capacity" must be a number/i), ).toBeInTheDocument(); + await waitFor(() => { + expect(selectors.continueButton()).toBeDisabled(); + }); + }); + it('should show validation error and disable continue button if the max capacity is empty', async () => { + //S + mockUseAccountsImplementation(); + renderVeeamConfigurationForm(); + await userEvent.type(selectors.accountNameInput(), 'Veeam'); + await userEvent.type(selectors.repositoryInput(), 'veeam-bucket'); + //E + await userEvent.clear(selectors.maxCapacityInput()); + //V + expect( + screen.getByText(/"capacity" must be a number/i), + ).toBeInTheDocument(); + await waitFor(() => { + expect(selectors.continueButton()).toBeDisabled(); + }); + }); + it('should show validation error if max capacity as more than 2 decimal points', async () => { + //S + mockUseAccountsImplementation(); + renderVeeamConfigurationForm(); + await userEvent.type(selectors.accountNameInput(), 'Veeam'); + await userEvent.type(selectors.repositoryInput(), 'veeam-bucket'); + //E + await userEvent.clear(selectors.maxCapacityInput()); + await userEvent.type(selectors.maxCapacityInput(), '1.123'); + //V + expect( + screen.getByText(/"capacity" must have at most 2 decimals/i), + ).toBeInTheDocument(); + await waitFor(() => { + expect(selectors.continueButton()).toBeDisabled(); + }); }); }); diff --git a/src/react/ui-elements/Veeam/VeeamConfiguration.tsx b/src/react/ui-elements/Veeam/VeeamConfiguration.tsx index e3ce86002..af111a3f3 100644 --- a/src/react/ui-elements/Veeam/VeeamConfiguration.tsx +++ b/src/react/ui-elements/Veeam/VeeamConfiguration.tsx @@ -45,7 +45,18 @@ const schema = Joi.object({ application: Joi.string().required(), capacity: Joi.when('application', { is: Joi.equal(VEEAM_BACKUP_REPLICATION_XML_VALUE), - then: Joi.number().required().min(1).max(999).integer(), + then: Joi.number() + .required() + .min(1) + .max(1024) + .custom((value, helpers) => { + if (!Number.isInteger(value * 100)) { + return helpers.message({ + custom: '"capacity" must have at most 2 decimals', + }); + } + return value; + }), otherwise: Joi.valid(), }), capacityUnit: Joi.when('application', { diff --git a/src/react/ui-elements/Veeam/useCapacityUnit.ts b/src/react/ui-elements/Veeam/useCapacityUnit.ts index a213eab57..afa4d4b9b 100644 --- a/src/react/ui-elements/Veeam/useCapacityUnit.ts +++ b/src/react/ui-elements/Veeam/useCapacityUnit.ts @@ -12,9 +12,9 @@ export const useCapacityUnit = ( const pBytesCapacity = prettyBytes(capacity, { locale: 'en', binary: true, - maximumFractionDigits: 0, + maximumFractionDigits: 2, }); - const capacityValue = pBytesCapacity.split(' ')[0]; + const capacityValue = pBytesCapacity.split(' ')[0].replace(',', ''); const capacityUnit = `${unitChoices[pBytesCapacity.split(' ')[1] as Units]}`; return { capacityValue, capacityUnit }; }; @@ -23,5 +23,7 @@ export const getCapacityBytes = ( capacityValue: string, capacityUnit: string, ) => { - return (parseInt(capacityValue, 10) * parseInt(capacityUnit, 10)).toString(); + return Math.round( + parseFloat(capacityValue) * parseFloat(capacityUnit), + ).toString(); };