From 79930347f97f251e1068b62be2ed212f848d18a7 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 29 Oct 2020 16:18:07 -0400 Subject: [PATCH] Fix number input validation bug --- awx/ui_next/src/api/models/Settings.js | 2 +- .../ActivityStream/ActivityStream.test.jsx | 28 ++++++++++++++++++- .../ActivityStreamEdit/ActivityStreamEdit.jsx | 23 ++++++--------- .../AzureAD/AzureADEdit/AzureADEdit.jsx | 28 ++++++++----------- .../screens/Setting/Logging/Logging.test.jsx | 23 +++++++++++++++ .../Logging/LoggingEdit/LoggingEdit.jsx | 25 ++++++----------- .../Logging/LoggingEdit/LoggingEdit.test.jsx | 6 ++-- .../Setting/MiscSystem/MiscSystem.test.jsx | 23 +++++++++++++++ .../MiscSystemEdit/MiscSystemEdit.jsx | 23 ++++++--------- .../src/screens/Setting/Settings.test.jsx | 2 +- .../screens/Setting/shared/RevertAllAlert.jsx | 4 +-- .../Setting/shared/RevertButton.test.jsx | 8 +++--- .../screens/Setting/shared/SharedFields.jsx | 17 +++++++---- ...eFields.test.jsx => SharedFields.test.jsx} | 0 awx/ui_next/src/util/useModal.js | 5 ++-- awx/ui_next/src/util/useModal.test.jsx | 11 +++++++- awx/ui_next/src/util/validators.jsx | 2 +- 17 files changed, 147 insertions(+), 83 deletions(-) rename awx/ui_next/src/screens/Setting/shared/{ShareFields.test.jsx => SharedFields.test.jsx} (100%) diff --git a/awx/ui_next/src/api/models/Settings.js b/awx/ui_next/src/api/models/Settings.js index 8610fc029f..3c85f68da6 100644 --- a/awx/ui_next/src/api/models/Settings.js +++ b/awx/ui_next/src/api/models/Settings.js @@ -22,7 +22,7 @@ class Settings extends Base { return this.http.options(`${this.baseUrl}${category}/`); } - test(category, data) { + createTest(category, data) { return this.http.post(`${this.baseUrl}${category}/test/`, data); } } diff --git a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStream.test.jsx b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStream.test.jsx index 90500406b0..14b698cefb 100644 --- a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStream.test.jsx +++ b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStream.test.jsx @@ -1,7 +1,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import ActivityStream from './ActivityStream'; import { SettingsAPI } from '../../../api'; @@ -56,4 +59,27 @@ describe('', () => { }); expect(wrapper.find('ContentError').length).toBe(1); }); + + test('should redirect to details for users without system admin permissions', async () => { + const history = createMemoryHistory({ + initialEntries: ['/settings/activity_stream/edit'], + }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { + router: { + history, + }, + config: { + me: { + is_superuser: false, + }, + }, + }, + }); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ActivityStreamDetail').length).toBe(1); + expect(wrapper.find('ActivityStreamEdit').length).toBe(0); + }); }); diff --git a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamEdit/ActivityStreamEdit.jsx b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamEdit/ActivityStreamEdit.jsx index 4397c9cf48..489979c748 100644 --- a/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamEdit/ActivityStreamEdit.jsx +++ b/awx/ui_next/src/screens/Setting/ActivityStream/ActivityStreamEdit/ActivityStreamEdit.jsx @@ -51,24 +51,17 @@ function ActivityStreamEdit() { request(); }, [request]); - const { - error: submitError, - request: submitForm, - result: submitResult, - } = useRequest( - useCallback(async values => { - const result = await SettingsAPI.updateAll(values); - return result; - }, []), + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/activity_stream/details'); + }, + [history] + ), null ); - useEffect(() => { - if (submitResult) { - history.push('/settings/activity_stream/details'); - } - }, [submitResult, history]); - const handleSubmit = async form => { await submitForm(form); }; diff --git a/awx/ui_next/src/screens/Setting/AzureAD/AzureADEdit/AzureADEdit.jsx b/awx/ui_next/src/screens/Setting/AzureAD/AzureADEdit/AzureADEdit.jsx index 115a854e8d..2bee3ae277 100644 --- a/awx/ui_next/src/screens/Setting/AzureAD/AzureADEdit/AzureADEdit.jsx +++ b/awx/ui_next/src/screens/Setting/AzureAD/AzureADEdit/AzureADEdit.jsx @@ -43,24 +43,17 @@ function AzureADEdit() { fetchAzureAD(); }, [fetchAzureAD]); - const { - error: submitError, - request: submitForm, - result: submitResult, - } = useRequest( - useCallback(async values => { - const result = await SettingsAPI.updateAll(values); - return result; - }, []), + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/azure/details'); + }, + [history] + ), null ); - useEffect(() => { - if (submitResult) { - history.push('/settings/azure/details'); - } - }, [submitResult, history]); - const handleSubmit = async form => { await submitForm({ ...form, @@ -90,7 +83,10 @@ function AzureADEdit() { const initialValues = fields => Object.keys(fields).reduce((acc, key) => { if (fields[key].type === 'list' || fields[key].type === 'nested object') { - acc[key] = JSON.stringify(fields[key].value, null, 2); + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; } else { acc[key] = fields[key].value ?? ''; } diff --git a/awx/ui_next/src/screens/Setting/Logging/Logging.test.jsx b/awx/ui_next/src/screens/Setting/Logging/Logging.test.jsx index 1fd76f8b3c..14f7bc5b15 100644 --- a/awx/ui_next/src/screens/Setting/Logging/Logging.test.jsx +++ b/awx/ui_next/src/screens/Setting/Logging/Logging.test.jsx @@ -80,4 +80,27 @@ describe('', () => { }); expect(wrapper.find('ContentError').length).toBe(1); }); + + test('should redirect to details for users without system admin permissions', async () => { + const history = createMemoryHistory({ + initialEntries: ['/settings/logging/edit'], + }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { + router: { + history, + }, + config: { + me: { + is_superuser: false, + }, + }, + }, + }); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('LoggingDetail').length).toBe(1); + expect(wrapper.find('LoggingEdit').length).toBe(0); + }); }); diff --git a/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.jsx b/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.jsx index 9411332b7c..d930d4ea27 100644 --- a/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.jsx +++ b/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.jsx @@ -51,24 +51,17 @@ function LoggingEdit({ i18n }) { fetchLogging(); }, [fetchLogging]); - const { - error: submitError, - request: submitForm, - result: submitResult, - } = useRequest( - useCallback(async values => { - const result = await SettingsAPI.updateAll(values); - return result; - }, []), + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/logging/details'); + }, + [history] + ), null ); - useEffect(() => { - if (submitResult) { - history.push('/settings/logging/details'); - } - }, [submitResult, history]); - const handleSubmit = async form => { await submitForm({ ...form, @@ -95,7 +88,7 @@ function LoggingEdit({ i18n }) { setValue: setTestLogging, } = useRequest( useCallback(async () => { - const result = await SettingsAPI.test('logging', {}); + const result = await SettingsAPI.createTest('logging', {}); return result; }, []), null diff --git a/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.test.jsx b/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.test.jsx index 4b97720cf0..5caf83714b 100644 --- a/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.test.jsx @@ -212,15 +212,15 @@ describe('', () => { }); test('should display successful toast when test button is clicked', async () => { - SettingsAPI.test.mockResolvedValue({}); - expect(SettingsAPI.test).toHaveBeenCalledTimes(0); + SettingsAPI.createTest.mockResolvedValue({}); + expect(SettingsAPI.createTest).toHaveBeenCalledTimes(0); expect(wrapper.find('LoggingTestAlert')).toHaveLength(0); await act(async () => { wrapper.find('button[aria-label="Test logging"]').invoke('onClick')(); }); wrapper.update(); await waitForElement(wrapper, 'LoggingTestAlert'); - expect(SettingsAPI.test).toHaveBeenCalledTimes(1); + expect(SettingsAPI.createTest).toHaveBeenCalledTimes(1); await act(async () => { wrapper.find('AlertActionCloseButton button').invoke('onClick')(); }); diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystem.test.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystem.test.jsx index 5ccc70487b..59bb991a82 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystem.test.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystem.test.jsx @@ -58,4 +58,27 @@ describe('', () => { }); expect(wrapper.find('ContentError').length).toBe(1); }); + + test('should redirect to details for users without system admin permissions', async () => { + const history = createMemoryHistory({ + initialEntries: ['/settings/miscellaneous_system/edit'], + }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { + router: { + history, + }, + config: { + me: { + is_superuser: false, + }, + }, + }, + }); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('MiscSystemDetail').length).toBe(1); + expect(wrapper.find('MiscSystemEdit').length).toBe(0); + }); }); diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx index f4800d6a81..94c64f91bb 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx @@ -107,15 +107,14 @@ function MiscSystemEdit({ i18n }) { fetchSystem(); }, [fetchSystem]); - const { - error: submitError, - request: submitForm, - result: submitResult, - } = useRequest( - useCallback(async values => { - const result = await SettingsAPI.updateAll(values); - return result; - }, []), + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/miscellaneous_system/details'); + }, + [history] + ), null ); @@ -138,12 +137,6 @@ function MiscSystemEdit({ i18n }) { }); }; - useEffect(() => { - if (submitResult) { - history.push('/settings/miscellaneous_system/details'); - } - }, [submitResult, history]); - const handleRevertAll = async () => { const { ACCESS_TOKEN_EXPIRE_SECONDS, diff --git a/awx/ui_next/src/screens/Setting/Settings.test.jsx b/awx/ui_next/src/screens/Setting/Settings.test.jsx index faead37231..24a82986f8 100644 --- a/awx/ui_next/src/screens/Setting/Settings.test.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.test.jsx @@ -46,7 +46,7 @@ describe('', () => { expect(wrapper.find('SettingList').length).toBe(0); }); - test('should render Settings for users with permissions system admin or auditor permissions', async () => { + test('should render Settings for users with system admin or auditor permissions', async () => { const history = createMemoryHistory({ initialEntries: ['/settings'], }); diff --git a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx index 42855de751..55b23437b9 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx @@ -30,8 +30,8 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) { , ]} > - {i18n._(t`This will revert all configuration values to their - factory defaults. Are you sure you want to proceed?`)} + {i18n._(t`This will revert all configuration values on this page to + their factory defaults. Are you sure you want to proceed?`)} ); } diff --git a/awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx b/awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx index 0592082bdf..1af4e7f223 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertButton.test.jsx @@ -18,7 +18,7 @@ describe('RevertButton', () => { test_input: 'foo', }} > - {() => } + ); expect(wrapper.find('button').text()).toEqual('Revert'); @@ -34,7 +34,7 @@ describe('RevertButton', () => { test_input: 'bar', }} > - {() => } + ); expect(wrapper.find('button').text()).toEqual('Revert'); @@ -47,7 +47,7 @@ describe('RevertButton', () => { test_input: 'foo', }} > - {() => } + ); expect(wrapper.find('button').text()).toEqual('Revert'); @@ -68,7 +68,7 @@ describe('RevertButton', () => { test_input: 'bar', }} > - {() => } + ); expect(wrapper.find('button').text()).toEqual('Revert'); diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx index 29c084550b..6922dfa624 100644 --- a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx +++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx @@ -15,7 +15,13 @@ import CodeMirrorInput from '../../../components/CodeMirrorInput'; import { PasswordInput } from '../../../components/FormField'; import { FormFullWidthLayout } from '../../../components/FormLayout'; import Popover from '../../../components/Popover'; -import { combine, required, url, minMaxValue } from '../../../util/validators'; +import { + combine, + required, + url, + integer, + minMaxValue, +} from '../../../util/validators'; import RevertButton from './RevertButton'; const FormGroup = styled(PFFormGroup)` @@ -176,9 +182,11 @@ const InputField = withI18n()( max_value = Number.MAX_SAFE_INTEGER, } = config; const validators = [ - isRequired ? required(null, i18n) : null, - type === 'url' ? url(i18n) : null, - type === 'number' ? minMaxValue(min_value, max_value, i18n) : null, + ...(isRequired ? [required(null, i18n)] : []), + ...(type === 'url' ? [url(i18n)] : []), + ...(type === 'number' + ? [integer(i18n), minMaxValue(min_value, max_value, i18n)] + : []), ]; const [field, meta] = useField({ name, validate: combine(validators) }); const isValid = !(meta.touched && meta.error); @@ -197,7 +205,6 @@ const InputField = withI18n()( id={name} isRequired={isRequired} placeholder={config.placeholder} - type={type} validated={isValid ? 'default' : 'error'} value={field.value} onBlur={field.onBlur} diff --git a/awx/ui_next/src/screens/Setting/shared/ShareFields.test.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Setting/shared/ShareFields.test.jsx rename to awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx diff --git a/awx/ui_next/src/util/useModal.js b/awx/ui_next/src/util/useModal.js index acc853c703..ffddd59ada 100644 --- a/awx/ui_next/src/util/useModal.js +++ b/awx/ui_next/src/util/useModal.js @@ -2,6 +2,7 @@ import { useState } from 'react'; /** * useModal hook provides a way to read and update modal visibility + * Param: boolean that sets initial modal state * Returns: { * isModalOpen: boolean that indicates if modal is open * toggleModal: function that toggles the modal open and close @@ -9,8 +10,8 @@ import { useState } from 'react'; * } */ -export default function useModal() { - const [isModalOpen, setIsModalOpen] = useState(false); +export default function useModal(isOpen = false) { + const [isModalOpen, setIsModalOpen] = useState(isOpen); function toggleModal() { setIsModalOpen(!isModalOpen); diff --git a/awx/ui_next/src/util/useModal.test.jsx b/awx/ui_next/src/util/useModal.test.jsx index 6d1c11faf6..414c5b8b2c 100644 --- a/awx/ui_next/src/util/useModal.test.jsx +++ b/awx/ui_next/src/util/useModal.test.jsx @@ -17,7 +17,7 @@ describe('useModal hook', () => { let isModalOpen; let toggleModal; - test('should return expected initial values', () => { + test('isModalOpen should return expected default value', () => { testHook(() => { ({ isModalOpen, toggleModal, closeModal } = useModal()); }); @@ -26,6 +26,15 @@ describe('useModal hook', () => { expect(closeModal).toBeInstanceOf(Function); }); + test('isModalOpen should return expected initialized value', () => { + testHook(() => { + ({ isModalOpen, toggleModal, closeModal } = useModal(true)); + }); + expect(isModalOpen).toEqual(true); + expect(toggleModal).toBeInstanceOf(Function); + expect(closeModal).toBeInstanceOf(Function); + }); + test('should return expected isModalOpen value after modal toggle', () => { testHook(() => { ({ isModalOpen, toggleModal, closeModal } = useModal()); diff --git a/awx/ui_next/src/util/validators.jsx b/awx/ui_next/src/util/validators.jsx index 499957f6e5..561f3051ca 100644 --- a/awx/ui_next/src/util/validators.jsx +++ b/awx/ui_next/src/util/validators.jsx @@ -69,7 +69,7 @@ export function noWhiteSpace(i18n) { export function integer(i18n) { return value => { const str = String(value); - if (/[^0-9]/.test(str)) { + if (!Number.isInteger(value) && /[^0-9]/.test(str)) { return i18n._(t`This field must be an integer`); } return undefined;