mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
Fix number input validation bug
This commit is contained in:
@@ -22,7 +22,7 @@ class Settings extends Base {
|
|||||||
return this.http.options(`${this.baseUrl}${category}/`);
|
return this.http.options(`${this.baseUrl}${category}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
test(category, data) {
|
createTest(category, data) {
|
||||||
return this.http.post(`${this.baseUrl}${category}/test/`, data);
|
return this.http.post(`${this.baseUrl}${category}/test/`, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import ActivityStream from './ActivityStream';
|
import ActivityStream from './ActivityStream';
|
||||||
import { SettingsAPI } from '../../../api';
|
import { SettingsAPI } from '../../../api';
|
||||||
|
|
||||||
@@ -56,4 +59,27 @@ describe('<ActivityStream />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
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(<ActivityStream />, {
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,24 +51,17 @@ function ActivityStreamEdit() {
|
|||||||
request();
|
request();
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
const {
|
const { error: submitError, request: submitForm } = useRequest(
|
||||||
error: submitError,
|
useCallback(
|
||||||
request: submitForm,
|
async values => {
|
||||||
result: submitResult,
|
await SettingsAPI.updateAll(values);
|
||||||
} = useRequest(
|
history.push('/settings/activity_stream/details');
|
||||||
useCallback(async values => {
|
},
|
||||||
const result = await SettingsAPI.updateAll(values);
|
[history]
|
||||||
return result;
|
),
|
||||||
}, []),
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (submitResult) {
|
|
||||||
history.push('/settings/activity_stream/details');
|
|
||||||
}
|
|
||||||
}, [submitResult, history]);
|
|
||||||
|
|
||||||
const handleSubmit = async form => {
|
const handleSubmit = async form => {
|
||||||
await submitForm(form);
|
await submitForm(form);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,24 +43,17 @@ function AzureADEdit() {
|
|||||||
fetchAzureAD();
|
fetchAzureAD();
|
||||||
}, [fetchAzureAD]);
|
}, [fetchAzureAD]);
|
||||||
|
|
||||||
const {
|
const { error: submitError, request: submitForm } = useRequest(
|
||||||
error: submitError,
|
useCallback(
|
||||||
request: submitForm,
|
async values => {
|
||||||
result: submitResult,
|
await SettingsAPI.updateAll(values);
|
||||||
} = useRequest(
|
history.push('/settings/azure/details');
|
||||||
useCallback(async values => {
|
},
|
||||||
const result = await SettingsAPI.updateAll(values);
|
[history]
|
||||||
return result;
|
),
|
||||||
}, []),
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (submitResult) {
|
|
||||||
history.push('/settings/azure/details');
|
|
||||||
}
|
|
||||||
}, [submitResult, history]);
|
|
||||||
|
|
||||||
const handleSubmit = async form => {
|
const handleSubmit = async form => {
|
||||||
await submitForm({
|
await submitForm({
|
||||||
...form,
|
...form,
|
||||||
@@ -90,7 +83,10 @@ function AzureADEdit() {
|
|||||||
const initialValues = fields =>
|
const initialValues = fields =>
|
||||||
Object.keys(fields).reduce((acc, key) => {
|
Object.keys(fields).reduce((acc, key) => {
|
||||||
if (fields[key].type === 'list' || fields[key].type === 'nested object') {
|
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 {
|
} else {
|
||||||
acc[key] = fields[key].value ?? '';
|
acc[key] = fields[key].value ?? '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,4 +80,27 @@ describe('<Logging />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
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(<Logging />, {
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,24 +51,17 @@ function LoggingEdit({ i18n }) {
|
|||||||
fetchLogging();
|
fetchLogging();
|
||||||
}, [fetchLogging]);
|
}, [fetchLogging]);
|
||||||
|
|
||||||
const {
|
const { error: submitError, request: submitForm } = useRequest(
|
||||||
error: submitError,
|
useCallback(
|
||||||
request: submitForm,
|
async values => {
|
||||||
result: submitResult,
|
await SettingsAPI.updateAll(values);
|
||||||
} = useRequest(
|
history.push('/settings/logging/details');
|
||||||
useCallback(async values => {
|
},
|
||||||
const result = await SettingsAPI.updateAll(values);
|
[history]
|
||||||
return result;
|
),
|
||||||
}, []),
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (submitResult) {
|
|
||||||
history.push('/settings/logging/details');
|
|
||||||
}
|
|
||||||
}, [submitResult, history]);
|
|
||||||
|
|
||||||
const handleSubmit = async form => {
|
const handleSubmit = async form => {
|
||||||
await submitForm({
|
await submitForm({
|
||||||
...form,
|
...form,
|
||||||
@@ -95,7 +88,7 @@ function LoggingEdit({ i18n }) {
|
|||||||
setValue: setTestLogging,
|
setValue: setTestLogging,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const result = await SettingsAPI.test('logging', {});
|
const result = await SettingsAPI.createTest('logging', {});
|
||||||
return result;
|
return result;
|
||||||
}, []),
|
}, []),
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -212,15 +212,15 @@ describe('<LoggingEdit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should display successful toast when test button is clicked', async () => {
|
test('should display successful toast when test button is clicked', async () => {
|
||||||
SettingsAPI.test.mockResolvedValue({});
|
SettingsAPI.createTest.mockResolvedValue({});
|
||||||
expect(SettingsAPI.test).toHaveBeenCalledTimes(0);
|
expect(SettingsAPI.createTest).toHaveBeenCalledTimes(0);
|
||||||
expect(wrapper.find('LoggingTestAlert')).toHaveLength(0);
|
expect(wrapper.find('LoggingTestAlert')).toHaveLength(0);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button[aria-label="Test logging"]').invoke('onClick')();
|
wrapper.find('button[aria-label="Test logging"]').invoke('onClick')();
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
await waitForElement(wrapper, 'LoggingTestAlert');
|
await waitForElement(wrapper, 'LoggingTestAlert');
|
||||||
expect(SettingsAPI.test).toHaveBeenCalledTimes(1);
|
expect(SettingsAPI.createTest).toHaveBeenCalledTimes(1);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('AlertActionCloseButton button').invoke('onClick')();
|
wrapper.find('AlertActionCloseButton button').invoke('onClick')();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,4 +58,27 @@ describe('<MiscSystem />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
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(<MiscSystem />, {
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -107,15 +107,14 @@ function MiscSystemEdit({ i18n }) {
|
|||||||
fetchSystem();
|
fetchSystem();
|
||||||
}, [fetchSystem]);
|
}, [fetchSystem]);
|
||||||
|
|
||||||
const {
|
const { error: submitError, request: submitForm } = useRequest(
|
||||||
error: submitError,
|
useCallback(
|
||||||
request: submitForm,
|
async values => {
|
||||||
result: submitResult,
|
await SettingsAPI.updateAll(values);
|
||||||
} = useRequest(
|
history.push('/settings/miscellaneous_system/details');
|
||||||
useCallback(async values => {
|
},
|
||||||
const result = await SettingsAPI.updateAll(values);
|
[history]
|
||||||
return result;
|
),
|
||||||
}, []),
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -138,12 +137,6 @@ function MiscSystemEdit({ i18n }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (submitResult) {
|
|
||||||
history.push('/settings/miscellaneous_system/details');
|
|
||||||
}
|
|
||||||
}, [submitResult, history]);
|
|
||||||
|
|
||||||
const handleRevertAll = async () => {
|
const handleRevertAll = async () => {
|
||||||
const {
|
const {
|
||||||
ACCESS_TOKEN_EXPIRE_SECONDS,
|
ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ describe('<Settings />', () => {
|
|||||||
expect(wrapper.find('SettingList').length).toBe(0);
|
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({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/settings'],
|
initialEntries: ['/settings'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) {
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{i18n._(t`This will revert all configuration values to their
|
{i18n._(t`This will revert all configuration values on this page to
|
||||||
factory defaults. Are you sure you want to proceed?`)}
|
their factory defaults. Are you sure you want to proceed?`)}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('RevertButton', () => {
|
|||||||
test_input: 'foo',
|
test_input: 'foo',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{() => <RevertButton id="test_input" defaultValue="" />}
|
<RevertButton id="test_input" defaultValue="" />
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button').text()).toEqual('Revert');
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
@@ -34,7 +34,7 @@ describe('RevertButton', () => {
|
|||||||
test_input: 'bar',
|
test_input: 'bar',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
<RevertButton id="test_input" defaultValue="bar" />
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button').text()).toEqual('Revert');
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
@@ -47,7 +47,7 @@ describe('RevertButton', () => {
|
|||||||
test_input: 'foo',
|
test_input: 'foo',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
<RevertButton id="test_input" defaultValue="bar" />
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button').text()).toEqual('Revert');
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
@@ -68,7 +68,7 @@ describe('RevertButton', () => {
|
|||||||
test_input: 'bar',
|
test_input: 'bar',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{() => <RevertButton id="test_input" defaultValue="bar" />}
|
<RevertButton id="test_input" defaultValue="bar" />
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button').text()).toEqual('Revert');
|
expect(wrapper.find('button').text()).toEqual('Revert');
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ import CodeMirrorInput from '../../../components/CodeMirrorInput';
|
|||||||
import { PasswordInput } from '../../../components/FormField';
|
import { PasswordInput } from '../../../components/FormField';
|
||||||
import { FormFullWidthLayout } from '../../../components/FormLayout';
|
import { FormFullWidthLayout } from '../../../components/FormLayout';
|
||||||
import Popover from '../../../components/Popover';
|
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';
|
import RevertButton from './RevertButton';
|
||||||
|
|
||||||
const FormGroup = styled(PFFormGroup)`
|
const FormGroup = styled(PFFormGroup)`
|
||||||
@@ -176,9 +182,11 @@ const InputField = withI18n()(
|
|||||||
max_value = Number.MAX_SAFE_INTEGER,
|
max_value = Number.MAX_SAFE_INTEGER,
|
||||||
} = config;
|
} = config;
|
||||||
const validators = [
|
const validators = [
|
||||||
isRequired ? required(null, i18n) : null,
|
...(isRequired ? [required(null, i18n)] : []),
|
||||||
type === 'url' ? url(i18n) : null,
|
...(type === 'url' ? [url(i18n)] : []),
|
||||||
type === 'number' ? minMaxValue(min_value, max_value, i18n) : null,
|
...(type === 'number'
|
||||||
|
? [integer(i18n), minMaxValue(min_value, max_value, i18n)]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
const [field, meta] = useField({ name, validate: combine(validators) });
|
const [field, meta] = useField({ name, validate: combine(validators) });
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !(meta.touched && meta.error);
|
||||||
@@ -197,7 +205,6 @@ const InputField = withI18n()(
|
|||||||
id={name}
|
id={name}
|
||||||
isRequired={isRequired}
|
isRequired={isRequired}
|
||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
type={type}
|
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* useModal hook provides a way to read and update modal visibility
|
* useModal hook provides a way to read and update modal visibility
|
||||||
|
* Param: boolean that sets initial modal state
|
||||||
* Returns: {
|
* Returns: {
|
||||||
* isModalOpen: boolean that indicates if modal is open
|
* isModalOpen: boolean that indicates if modal is open
|
||||||
* toggleModal: function that toggles the modal open and close
|
* toggleModal: function that toggles the modal open and close
|
||||||
@@ -9,8 +10,8 @@ import { useState } from 'react';
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function useModal() {
|
export default function useModal(isOpen = false) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(isOpen);
|
||||||
|
|
||||||
function toggleModal() {
|
function toggleModal() {
|
||||||
setIsModalOpen(!isModalOpen);
|
setIsModalOpen(!isModalOpen);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('useModal hook', () => {
|
|||||||
let isModalOpen;
|
let isModalOpen;
|
||||||
let toggleModal;
|
let toggleModal;
|
||||||
|
|
||||||
test('should return expected initial values', () => {
|
test('isModalOpen should return expected default value', () => {
|
||||||
testHook(() => {
|
testHook(() => {
|
||||||
({ isModalOpen, toggleModal, closeModal } = useModal());
|
({ isModalOpen, toggleModal, closeModal } = useModal());
|
||||||
});
|
});
|
||||||
@@ -26,6 +26,15 @@ describe('useModal hook', () => {
|
|||||||
expect(closeModal).toBeInstanceOf(Function);
|
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', () => {
|
test('should return expected isModalOpen value after modal toggle', () => {
|
||||||
testHook(() => {
|
testHook(() => {
|
||||||
({ isModalOpen, toggleModal, closeModal } = useModal());
|
({ isModalOpen, toggleModal, closeModal } = useModal());
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function noWhiteSpace(i18n) {
|
|||||||
export function integer(i18n) {
|
export function integer(i18n) {
|
||||||
return value => {
|
return value => {
|
||||||
const str = String(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 i18n._(t`This field must be an integer`);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user