Changes settings revert all to send DELETE on individual endpoint rather than PATCHing

This commit is contained in:
mabashian 2021-06-03 14:47:35 -04:00
parent d3b20e6585
commit 12504c9bc3
51 changed files with 7174 additions and 7765 deletions

View File

@ -29,6 +29,10 @@ class Settings extends Base {
createTest(category, data) {
return this.http.post(`${this.baseUrl}${category}/test/`, data);
}
revertCategory(category) {
return this.http.delete(`${this.baseUrl}${category}/`);
}
}
export default Settings;

View File

@ -1,88 +0,0 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
mountWithContexts,
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import { SettingsProvider } from '../../../../contexts/Settings';
import { SettingsAPI } from '../../../../api';
import { assertDetail } from '../../shared/settingTestUtils';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import ActivityStreamDetail from './ActivityStreamDetail';
jest.mock('../../../../api');
describe('<ActivityStreamDetail />', () => {
let wrapper;
beforeAll(async () => {
SettingsAPI.readCategory.mockResolvedValue({
data: {
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
},
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<ActivityStreamDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
afterAll(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('initially renders without crashing', () => {
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
});
test('should render expected tabs', () => {
const expectedTabs = ['Back to Settings', 'Details'];
wrapper.find('RoutedTabs li').forEach((tab, index) => {
expect(tab.text()).toEqual(expectedTabs[index]);
});
});
test('should render expected details', () => {
assertDetail(wrapper, 'Enable Activity Stream', 'On');
assertDetail(wrapper, 'Enable Activity Stream for Inventory Sync', 'Off');
});
test('should hide edit button from non-superusers', async () => {
const config = {
me: {
is_superuser: false,
},
};
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<ActivityStreamDetail />
</SettingsProvider>,
{
context: { config },
}
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
});
test('should display content error when api throws error on initial render', async () => {
SettingsAPI.readCategory.mockRejectedValue(new Error());
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<ActivityStreamDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@ -1 +0,0 @@
export { default } from './ActivityStreamDetail';

View File

@ -1,130 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Formik } from 'formik';
import { Form } from '@patternfly/react-core';
import { CardBody } from '../../../../components/Card';
import ContentError from '../../../../components/ContentError';
import ContentLoading from '../../../../components/ContentLoading';
import { FormSubmitError } from '../../../../components/FormField';
import { FormColumnLayout } from '../../../../components/FormLayout';
import { useSettings } from '../../../../contexts/Settings';
import {
BooleanField,
RevertAllAlert,
RevertFormActionGroup,
} from '../../shared';
import useModal from '../../../../util/useModal';
import useRequest from '../../../../util/useRequest';
import { SettingsAPI } from '../../../../api';
function ActivityStreamEdit() {
const history = useHistory();
const { isModalOpen, toggleModal, closeModal } = useModal();
const { PUT: options } = useSettings();
const {
isLoading,
error,
request,
result: {
ACTIVITY_STREAM_ENABLED,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
},
} = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('system');
return {
ACTIVITY_STREAM_ENABLED: {
...options.ACTIVITY_STREAM_ENABLED,
value: data.ACTIVITY_STREAM_ENABLED,
},
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: {
...options.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
value: data.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
},
};
}, [options]),
{}
);
useEffect(() => {
request();
}, [request]);
const { error: submitError, request: submitForm } = useRequest(
useCallback(
async values => {
await SettingsAPI.updateAll(values);
history.push('/settings/activity_stream/details');
},
[history]
),
null
);
const handleSubmit = async form => {
await submitForm(form);
};
const handleCancel = () => {
history.push('/settings/activity_stream/details');
};
const handleRevertAll = async () => {
const defaultValues = {
ACTIVITY_STREAM_ENABLED: ACTIVITY_STREAM_ENABLED.default,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC.default,
};
await submitForm(defaultValues);
closeModal();
};
return (
<CardBody>
{isLoading && <ContentLoading />}
{!isLoading && error && <ContentError error={error} />}
{!isLoading && ACTIVITY_STREAM_ENABLED && (
<Formik
initialValues={{
ACTIVITY_STREAM_ENABLED: ACTIVITY_STREAM_ENABLED.value,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC.value,
}}
onSubmit={handleSubmit}
>
{formik => {
return (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<BooleanField
name="ACTIVITY_STREAM_ENABLED"
config={ACTIVITY_STREAM_ENABLED}
/>
<BooleanField
name="ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC"
config={ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC}
/>
{submitError && <FormSubmitError error={submitError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
onRevert={toggleModal}
/>
{isModalOpen && (
<RevertAllAlert
onClose={closeModal}
onRevertAll={handleRevertAll}
/>
)}
</Form>
);
}}
</Formik>
)}
</CardBody>
);
}
export default ActivityStreamEdit;

View File

@ -1 +0,0 @@
export { default } from './ActivityStreamEdit';

View File

@ -1 +0,0 @@
export { default } from './ActivityStream';

View File

@ -55,6 +55,13 @@ function AzureADEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('azuread-oauth2');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function AzureADEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(azure).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/azure/details');
};
const handleCancel = () => {
@ -120,6 +125,7 @@ function AzureADEdit() {
config={azure.SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<AzureADEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -60,7 +61,7 @@ describe('<AzureADEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -75,13 +76,8 @@ describe('<AzureADEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY: '',
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: '',
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: null,
SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('azuread-oauth2');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -66,13 +73,11 @@ function GitHubEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/details');
};
const handleCancel = () => {
@ -118,6 +123,7 @@ function GitHubEdit() {
config={github.SOCIAL_AUTH_GITHUB_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -72,7 +73,7 @@ describe('<GitHubEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -87,13 +88,8 @@ describe('<GitHubEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_KEY: '',
SOCIAL_AUTH_GITHUB_SECRET: '',
SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('github');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubEnterpriseEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github-enterprise');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function GitHubEnterpriseEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/enterprise/details');
};
const handleCancel = () => {
@ -128,6 +133,7 @@ function GitHubEnterpriseEdit() {
config={github.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubEnterpriseEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -82,7 +83,7 @@ describe('<GitHubEnterpriseEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -97,15 +98,10 @@ describe('<GitHubEnterpriseEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith(
'github-enterprise'
);
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubEnterpriseOrgEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github-enterprise-org');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function GitHubEnterpriseOrgEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/enterprise_organization/details');
};
const handleCancel = () => {
@ -134,6 +139,7 @@ function GitHubEnterpriseOrgEdit() {
config={github.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubEnterpriseOrgEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -94,7 +95,7 @@ describe('<GitHubEnterpriseOrgEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -109,16 +110,10 @@ describe('<GitHubEnterpriseOrgEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith(
'github-enterprise-org'
);
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubEnterpriseTeamEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github-enterprise-team');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function GitHubEnterpriseTeamEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/enterprise_team/details');
};
const handleCancel = () => {
@ -134,6 +139,7 @@ function GitHubEnterpriseTeamEdit() {
config={github.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubEnterpriseTeamEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -88,7 +89,7 @@ describe('<GitHubEnterpriseTeamEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -103,16 +104,10 @@ describe('<GitHubEnterpriseTeamEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '',
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith(
'github-enterprise-team'
);
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubOrgEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github-org');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function GitHubOrgEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/organization/details');
};
const handleCancel = () => {
@ -124,6 +129,7 @@ function GitHubOrgEdit() {
config={github.SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubOrgEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -79,7 +80,7 @@ describe('<GitHubOrgEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -94,14 +95,8 @@ describe('<GitHubOrgEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_ORG_KEY: '',
SOCIAL_AUTH_GITHUB_ORG_SECRET: '',
SOCIAL_AUTH_GITHUB_ORG_NAME: '',
SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('github-org');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -55,6 +55,13 @@ function GitHubTeamEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('github-team');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -68,13 +75,11 @@ function GitHubTeamEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(github).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/github/team/details');
};
const handleCancel = () => {
@ -124,6 +129,7 @@ function GitHubTeamEdit() {
config={github.SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GitHubTeamEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -74,7 +75,7 @@ describe('<GitHubTeamEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -89,14 +90,8 @@ describe('<GitHubTeamEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GITHUB_TEAM_KEY: '',
SOCIAL_AUTH_GITHUB_TEAM_SECRET: '',
SOCIAL_AUTH_GITHUB_TEAM_ID: '',
SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('github-team');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -60,6 +60,13 @@ function GoogleOAuth2Edit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('google-oauth2');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -79,13 +86,11 @@ function GoogleOAuth2Edit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(googleOAuth2).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/google_oauth2/details');
};
const handleCancel = () => {
@ -148,6 +153,7 @@ function GoogleOAuth2Edit() {
config={googleOAuth2.SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<GoogleOAuth2Edit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -82,7 +83,7 @@ describe('<GoogleOAuth2Edit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -97,15 +98,8 @@ describe('<GoogleOAuth2Edit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: '',
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '',
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [],
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {},
SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: null,
SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: null,
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('google-oauth2');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -63,6 +63,13 @@ function JobsEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('jobs');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -76,12 +83,11 @@ function JobsEdit() {
};
const handleRevertAll = async () => {
const defaultValues = {};
Object.entries(jobs).forEach(([key, value]) => {
defaultValues[key] = value.default;
});
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/jobs/details');
};
const handleCancel = () => {
@ -181,6 +187,7 @@ function JobsEdit() {
/>
<ObjectField name="AWX_TASK_ENV" config={jobs.AWX_TASK_ENV} />
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -7,7 +7,6 @@ import {
} from '../../../../../testUtils/enzymeHelpers';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import mockJobSettings from '../../shared/data.jobSettings.json';
import mockDefaultJobSettings from './data.defaultJobSettings.json';
import { SettingsProvider } from '../../../../contexts/Settings';
import { SettingsAPI } from '../../../../api';
import JobsEdit from './JobsEdit';
@ -19,6 +18,7 @@ describe('<JobsEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: mockJobSettings,
@ -51,7 +51,7 @@ describe('<JobsEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -66,8 +66,8 @@ describe('<JobsEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith(mockDefaultJobSettings);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('jobs');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -75,14 +75,19 @@ function LoggingEdit() {
});
};
const handleRevertAll = async () => {
const defaultValues = {};
Object.entries(logging).forEach(([key, value]) => {
defaultValues[key] = value.default;
});
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('logging');
}, []),
null
);
const handleRevertAll = async () => {
await revertAll();
await submitForm(defaultValues);
closeModal();
history.push('/settings/logging/details');
};
const {
@ -221,6 +226,7 @@ function LoggingEdit() {
config={logging.LOG_AGGREGATOR_LOGGERS}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
<RevertFormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}

View File

@ -35,29 +35,6 @@ const mockSettings = {
LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx',
LOG_AGGREGATOR_RSYSLOGD_DEBUG: false,
};
const mockDefaultSettings = {
LOG_AGGREGATOR_HOST: null,
LOG_AGGREGATOR_PORT: null,
LOG_AGGREGATOR_TYPE: null,
LOG_AGGREGATOR_USERNAME: '',
LOG_AGGREGATOR_PASSWORD: '',
LOG_AGGREGATOR_LOGGERS: [
'awx',
'activity_stream',
'job_events',
'system_tracking',
],
LOG_AGGREGATOR_INDIVIDUAL_FACTS: false,
LOG_AGGREGATOR_ENABLED: false,
LOG_AGGREGATOR_TOWER_UUID: '',
LOG_AGGREGATOR_PROTOCOL: 'https',
LOG_AGGREGATOR_TCP_TIMEOUT: 5,
LOG_AGGREGATOR_VERIFY_CERT: true,
LOG_AGGREGATOR_LEVEL: 'INFO',
LOG_AGGREGATOR_MAX_DISK_USAGE_GB: 1,
LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx',
LOG_AGGREGATOR_RSYSLOGD_DEBUG: false,
};
describe('<LoggingEdit />', () => {
let wrapper;
@ -68,6 +45,7 @@ describe('<LoggingEdit />', () => {
});
beforeEach(async () => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: mockSettings,
@ -227,7 +205,7 @@ describe('<LoggingEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -242,8 +220,8 @@ describe('<LoggingEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith(mockDefaultSettings);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('logging');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -1,15 +1,14 @@
import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
import ContentError from '../../../components/ContentError';
import { useConfig } from '../../../contexts/Config';
import ActivityStreamDetail from './ActivityStreamDetail';
import ActivityStreamEdit from './ActivityStreamEdit';
import MiscAuthenticationDetail from './MiscAuthenticationDetail';
import MiscAuthenticationEdit from './MiscAuthenticationEdit';
function ActivityStream() {
const baseURL = '/settings/activity_stream';
function MiscAuthentication() {
const baseURL = '/settings/miscellaneous_authentication';
const { me } = useConfig();
return (
@ -18,11 +17,11 @@ function ActivityStream() {
<Switch>
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
<Route path={`${baseURL}/details`}>
<ActivityStreamDetail />
<MiscAuthenticationDetail />
</Route>
<Route path={`${baseURL}/edit`}>
{me?.is_superuser ? (
<ActivityStreamEdit />
<MiscAuthenticationEdit />
) : (
<Redirect to={`${baseURL}/details`} />
)}
@ -30,7 +29,7 @@ function ActivityStream() {
<Route key="not-found" path={`${baseURL}/*`}>
<ContentError isNotFound>
<Link to={`${baseURL}/details`}>
{t`View Activity Stream settings`}
{t`View Miscellaneous Authentication settings`}
</Link>
</ContentError>
</Route>
@ -40,4 +39,4 @@ function ActivityStream() {
);
}
export default ActivityStream;
export default MiscAuthentication;

View File

@ -5,55 +5,57 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import ActivityStream from './ActivityStream';
import { SettingsAPI } from '../../../api';
import MiscAuthentication from './MiscAuthentication';
jest.mock('../../../api/models/Settings');
SettingsAPI.readCategory.mockResolvedValue({
data: {
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
},
});
jest.mock('../../../api');
describe('<ActivityStream />', () => {
describe('<MiscAuthentication />', () => {
let wrapper;
beforeEach(() => {
SettingsAPI.readCategory.mockResolvedValue({
data: {},
});
});
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('should render activity stream details', async () => {
test('should render miscellaneous authentication details', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/activity_stream/details'],
initialEntries: ['/settings/miscellaneous_authentication/details'],
});
await act(async () => {
wrapper = mountWithContexts(<ActivityStream />, {
wrapper = mountWithContexts(<MiscAuthentication />, {
context: { router: { history } },
});
});
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('MiscAuthenticationDetail').length).toBe(1);
});
test('should render activity stream edit', async () => {
test('should render miscellaneous authentication edit', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/activity_stream/edit'],
initialEntries: ['/settings/miscellaneous_authentication/edit'],
});
await act(async () => {
wrapper = mountWithContexts(<ActivityStream />, {
wrapper = mountWithContexts(<MiscAuthentication />, {
context: { router: { history } },
});
});
expect(wrapper.find('ActivityStreamEdit').length).toBe(1);
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('MiscAuthenticationEdit').length).toBe(1);
});
test('should show content error when user navigates to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/activity_stream/foo'],
initialEntries: ['/settings/miscellaneous_authentication/foo'],
});
await act(async () => {
wrapper = mountWithContexts(<ActivityStream />, {
wrapper = mountWithContexts(<MiscAuthentication />, {
context: { router: { history } },
});
});
@ -62,10 +64,10 @@ describe('<ActivityStream />', () => {
test('should redirect to details for users without system admin permissions', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/activity_stream/edit'],
initialEntries: ['/settings/miscellaneous_authentication/edit'],
});
await act(async () => {
wrapper = mountWithContexts(<ActivityStream />, {
wrapper = mountWithContexts(<MiscAuthentication />, {
context: {
router: {
history,
@ -79,7 +81,7 @@ describe('<ActivityStream />', () => {
});
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('ActivityStreamDetail').length).toBe(1);
expect(wrapper.find('ActivityStreamEdit').length).toBe(0);
expect(wrapper.find('MiscAuthenticationDetail').length).toBe(1);
expect(wrapper.find('MiscAuthenticationEdit').length).toBe(0);
});
});

View File

@ -1,36 +1,27 @@
import React, { useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { t } from '@lingui/macro';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { Button } from '@patternfly/react-core';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { CardBody, CardActionsRow } from '../../../../components/Card';
import ContentLoading from '../../../../components/ContentLoading';
import ContentError from '../../../../components/ContentError';
import { DetailList } from '../../../../components/DetailList';
import RoutedTabs from '../../../../components/RoutedTabs';
import useRequest from '../../../../util/useRequest';
import { useConfig } from '../../../../contexts/Config';
import { useSettings } from '../../../../contexts/Settings';
import useRequest from '../../../../util/useRequest';
import { SettingsAPI } from '../../../../api';
import { SettingDetail } from '../../shared';
function ActivityStreamDetail() {
function MiscAuthenticationDetail() {
const { me } = useConfig();
const { GET: options } = useSettings();
const { isLoading, error, request, result: activityStream } = useRequest(
const { isLoading, error, request, result: authentication } = useRequest(
useCallback(async () => {
const {
data: {
ACTIVITY_STREAM_ENABLED,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
},
} = await SettingsAPI.readCategory('system');
return {
ACTIVITY_STREAM_ENABLED,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC,
};
const { data } = await SettingsAPI.readCategory('authentication');
return data;
}, []),
null
);
@ -52,7 +43,7 @@ function ActivityStreamDetail() {
},
{
name: t`Details`,
link: `/settings/activity_stream/details`,
link: `/settings/miscellaneous_authentication/details`,
id: 0,
},
];
@ -63,9 +54,9 @@ function ActivityStreamDetail() {
<CardBody>
{isLoading && <ContentLoading />}
{!isLoading && error && <ContentError error={error} />}
{!isLoading && activityStream && (
{!isLoading && authentication && (
<DetailList>
{Object.keys(activityStream).map(key => {
{Object.keys(authentication).map(key => {
const record = options?.[key];
return (
<SettingDetail
@ -75,7 +66,7 @@ function ActivityStreamDetail() {
label={record?.label}
type={record?.type}
unit={record?.unit}
value={activityStream?.[key]}
value={authentication?.[key]}
/>
);
})}
@ -84,10 +75,10 @@ function ActivityStreamDetail() {
{me?.is_superuser && (
<CardActionsRow>
<Button
ouiaId="activity-stream-detail-edit-button"
ouiaId="authentication-detail-edit-button"
aria-label={t`Edit`}
component={Link}
to="/settings/activity_stream/edit"
to="/settings/miscellaneous_authentication/edit"
>
{t`Edit`}
</Button>
@ -98,4 +89,4 @@ function ActivityStreamDetail() {
);
}
export default ActivityStreamDetail;
export default MiscAuthenticationDetail;

View File

@ -0,0 +1,128 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
mountWithContexts,
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import { SettingsProvider } from '../../../../contexts/Settings';
import { SettingsAPI } from '../../../../api';
import {
assertDetail,
assertVariableDetail,
} from '../../shared/settingTestUtils';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import MiscAuthenticationDetail from './MiscAuthenticationDetail';
jest.mock('../../../../api');
describe('<MiscAuthenticationDetail />', () => {
let wrapper;
beforeEach(async () => {
SettingsAPI.readCategory = jest.fn();
SettingsAPI.readCategory.mockResolvedValue({
data: {
SESSION_COOKIE_AGE: 1800,
SESSIONS_PER_USER: -1,
DISABLE_LOCAL_AUTH: false,
AUTH_BASIC_ENABLED: true,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000,
REFRESH_TOKEN_EXPIRE_SECONDS: 2628000,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 600,
},
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
LOGIN_REDIRECT_OVERRIDE: 'https://foohost',
AUTHENTICATION_BACKENDS: [
'awx.sso.backends.TACACSPlusBackend',
'awx.main.backends.AWXModelBackend',
],
SOCIAL_AUTH_ORGANIZATION_MAP: {},
SOCIAL_AUTH_TEAM_MAP: {},
SOCIAL_AUTH_USER_FIELDS: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<MiscAuthenticationDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
test('initially renders without crashing', () => {
expect(wrapper.find('MiscAuthenticationDetail').length).toBe(1);
});
test('should render expected tabs', () => {
const expectedTabs = ['Back to Settings', 'Details'];
wrapper.find('RoutedTabs li').forEach((tab, index) => {
expect(tab.text()).toEqual(expectedTabs[index]);
});
});
test('should render expected details', () => {
assertDetail(wrapper, 'Disable the built-in authentication system', 'Off');
assertVariableDetail(
wrapper,
'OAuth 2 Timeout Settings',
'{\n "ACCESS_TOKEN_EXPIRE_SECONDS": 31536000000,\n "REFRESH_TOKEN_EXPIRE_SECONDS": 2628000,\n "AUTHORIZATION_CODE_EXPIRE_SECONDS": 600\n}'
);
assertDetail(wrapper, 'Login redirect override URL', 'https://foohost');
assertVariableDetail(
wrapper,
'Authentication Backends',
'[\n "awx.sso.backends.TACACSPlusBackend",\n "awx.main.backends.AWXModelBackend"\n]'
);
assertVariableDetail(wrapper, 'Social Auth Organization Map', '{}');
assertVariableDetail(wrapper, 'Social Auth Team Map', '{}');
assertVariableDetail(wrapper, 'Social Auth User Fields', '[]');
assertDetail(
wrapper,
'Allow External Users to Create OAuth2 Tokens',
'Off'
);
assertDetail(wrapper, 'Enable HTTP Basic Auth', 'On');
assertDetail(wrapper, 'Idle Time Force Log Out', '1800 seconds');
assertDetail(
wrapper,
'Maximum number of simultaneous logged in sessions',
'-1'
);
});
test('should hide edit button from non-superusers', async () => {
const config = {
me: {
is_superuser: false,
},
};
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<MiscAuthenticationDetail />
</SettingsProvider>,
{
context: { config },
}
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
});
test('should display content error when api throws error on initial render', async () => {
SettingsAPI.readCategory.mockRejectedValue(new Error());
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<MiscAuthenticationDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@ -0,0 +1 @@
export { default } from './MiscAuthenticationDetail';

View File

@ -0,0 +1,270 @@
import React, { useCallback, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
import { Form } from '@patternfly/react-core';
import { CardBody } from '../../../../components/Card';
import ContentError from '../../../../components/ContentError';
import ContentLoading from '../../../../components/ContentLoading';
import { FormSubmitError } from '../../../../components/FormField';
import { FormColumnLayout } from '../../../../components/FormLayout';
import { useSettings } from '../../../../contexts/Settings';
import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
import {
BooleanField,
InputField,
ObjectField,
} from '../../shared/SharedFields';
import useModal from '../../../../util/useModal';
import useRequest from '../../../../util/useRequest';
import { SettingsAPI } from '../../../../api';
import { formatJson, pluck } from '../../shared/settingUtils';
function MiscAuthenticationEdit() {
const history = useHistory();
const { isModalOpen, toggleModal, closeModal } = useModal();
const { PUT: options } = useSettings();
const {
isLoading,
error,
request: fetchAuthentication,
result: authentication,
} = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('authentication');
const {
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
...pluckedAuthenticationData
} = pluck(
data,
'ALLOW_OAUTH2_FOR_EXTERNAL_USERS',
'AUTH_BASIC_ENABLED',
'LOGIN_REDIRECT_OVERRIDE',
'DISABLE_LOCAL_AUTH',
'OAUTH2_PROVIDER',
'SESSIONS_PER_USER',
'SESSION_COOKIE_AGE',
'SOCIAL_AUTH_ORGANIZATION_MAP',
'SOCIAL_AUTH_TEAM_MAP',
'SOCIAL_AUTH_USER_FIELDS'
);
const authenticationData = {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
...pluckedAuthenticationData,
};
const {
OAUTH2_PROVIDER: OAUTH2_PROVIDER_OPTIONS,
...restOptions
} = options;
const authenticationOptions = {
...restOptions,
ACCESS_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Access Token Expiration`,
},
REFRESH_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Refresh Token Expiration`,
},
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default:
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Authorization Code Expiration`,
},
};
const mergedData = {};
Object.keys(authenticationData).forEach(key => {
if (!authenticationOptions[key]) {
return;
}
mergedData[key] = authenticationOptions[key];
mergedData[key].value = authenticationData[key];
});
return mergedData;
}, [options]),
null
);
useEffect(() => {
fetchAuthentication();
}, [fetchAuthentication]);
const { error: submitError, request: submitForm } = useRequest(
useCallback(
async values => {
await SettingsAPI.updateAll(values);
history.push('/settings/miscellaneous_authentication/details');
},
[history]
),
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('authentication');
}, []),
null
);
const handleSubmit = async form => {
const {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
...formData
} = form;
await submitForm({
...formData,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
SOCIAL_AUTH_ORGANIZATION_MAP: formatJson(
formData.SOCIAL_AUTH_ORGANIZATION_MAP
),
SOCIAL_AUTH_TEAM_MAP: formatJson(formData.SOCIAL_AUTH_TEAM_MAP),
SOCIAL_AUTH_USER_FIELDS: formatJson(formData.SOCIAL_AUTH_USER_FIELDS),
});
};
const handleRevertAll = async () => {
await revertAll();
closeModal();
history.push('/settings/miscellaneous_authentication/details');
};
const handleCancel = () => {
history.push('/settings/miscellaneous_authentication/details');
};
const initialValues = fields =>
Object.keys(fields).reduce((acc, key) => {
if (fields[key].type === 'list' || fields[key].type === 'nested object') {
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 ?? '';
}
return acc;
}, {});
return (
<CardBody>
{isLoading && <ContentLoading />}
{!isLoading && error && <ContentError error={error} />}
{!isLoading && authentication && (
<Formik
initialValues={initialValues(authentication)}
onSubmit={handleSubmit}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<BooleanField
name="DISABLE_LOCAL_AUTH"
needsConfirmationModal
modalTitle={t`Confirm Disable Local Authorization`}
config={authentication.DISABLE_LOCAL_AUTH}
/>
<InputField
name="SESSION_COOKIE_AGE"
config={authentication.SESSION_COOKIE_AGE}
type="number"
isRequired
/>
<InputField
name="SESSIONS_PER_USER"
config={authentication.SESSIONS_PER_USER}
type="number"
isRequired
/>
<BooleanField
name="AUTH_BASIC_ENABLED"
config={authentication.AUTH_BASIC_ENABLED}
/>
<BooleanField
name="ALLOW_OAUTH2_FOR_EXTERNAL_USERS"
config={authentication.ALLOW_OAUTH2_FOR_EXTERNAL_USERS}
/>
<InputField
name="LOGIN_REDIRECT_OVERRIDE"
config={authentication.LOGIN_REDIRECT_OVERRIDE}
type="url"
/>
<InputField
name="ACCESS_TOKEN_EXPIRE_SECONDS"
config={authentication.ACCESS_TOKEN_EXPIRE_SECONDS}
type="number"
/>
<InputField
name="REFRESH_TOKEN_EXPIRE_SECONDS"
config={authentication.REFRESH_TOKEN_EXPIRE_SECONDS}
type="number"
/>
<InputField
name="AUTHORIZATION_CODE_EXPIRE_SECONDS"
config={authentication.AUTHORIZATION_CODE_EXPIRE_SECONDS}
type="number"
/>
<ObjectField
name="SOCIAL_AUTH_ORGANIZATION_MAP"
config={authentication.SOCIAL_AUTH_ORGANIZATION_MAP}
/>
<ObjectField
name="SOCIAL_AUTH_TEAM_MAP"
config={authentication.SOCIAL_AUTH_TEAM_MAP}
/>
<ObjectField
name="SOCIAL_AUTH_USER_FIELDS"
config={authentication.SOCIAL_AUTH_USER_FIELDS}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
onRevert={toggleModal}
/>
{isModalOpen && (
<RevertAllAlert
onClose={closeModal}
onRevertAll={handleRevertAll}
/>
)}
</Form>
)}
</Formik>
)}
</CardBody>
);
}
export default MiscAuthenticationEdit;

View File

@ -6,36 +6,55 @@ import {
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import mockAllSettings from '../../shared/data.allSettings.json';
import { SettingsProvider } from '../../../../contexts/Settings';
import { SettingsAPI } from '../../../../api';
import ActivityStreamEdit from './ActivityStreamEdit';
import MiscAuthenticationEdit from './MiscAuthenticationEdit';
jest.mock('../../../../api');
describe('<ActivityStreamEdit />', () => {
const authenticationData = {
SESSION_COOKIE_AGE: 1800,
SESSIONS_PER_USER: -1,
DISABLE_LOCAL_AUTH: false,
AUTH_BASIC_ENABLED: true,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000,
REFRESH_TOKEN_EXPIRE_SECONDS: 2628000,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 600,
},
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
LOGIN_REDIRECT_OVERRIDE: '',
AUTHENTICATION_BACKENDS: [
'awx.sso.backends.TACACSPlusBackend',
'awx.main.backends.AWXModelBackend',
],
SOCIAL_AUTH_ORGANIZATION_MAP: {},
SOCIAL_AUTH_TEAM_MAP: {},
SOCIAL_AUTH_USER_FIELDS: [],
};
describe('<MiscAuthenticationEdit />', () => {
let wrapper;
let history;
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/settings/activity_stream/edit'],
});
SettingsAPI.readCategory.mockResolvedValue({
data: {
ACTIVITY_STREAM_ENABLED: false,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: true,
},
});
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: mockAllSettings,
});
history = createMemoryHistory({
initialEntries: ['/settings/miscellaneous_authentication/edit'],
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<ActivityStreamEdit />
<MiscAuthenticationEdit />
</SettingsProvider>,
{
context: { router: { history } },
@ -45,54 +64,23 @@ describe('<ActivityStreamEdit />', () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
test('initially renders without crashing', () => {
expect(wrapper.find('ActivityStreamEdit').length).toBe(1);
test('initially renders without crashing', async () => {
expect(wrapper.find('MiscAuthenticationEdit').length).toBe(1);
});
test('should navigate to activity stream detail when cancel is clicked', async () => {
test('save button should call updateAll', async () => {
expect(wrapper.find('MiscAuthenticationEdit').length).toBe(1);
wrapper.update();
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
});
expect(history.location.pathname).toEqual(
'/settings/activity_stream/details'
);
});
test('should navigate to activity stream detail on successful submission', async () => {
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(history.location.pathname).toEqual(
'/settings/activity_stream/details'
);
});
test('should successfully send request to api on form submission', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(
wrapper.find('Switch#ACTIVITY_STREAM_ENABLED').prop('isChecked')
).toEqual(false);
await act(async () => {
wrapper.find('Switch#ACTIVITY_STREAM_ENABLED').invoke('onChange')(true);
wrapper.find('button[aria-label="Save"]').simulate('click');
});
wrapper.update();
expect(
wrapper.find('Switch#ACTIVITY_STREAM_ENABLED').prop('isChecked')
).toEqual(true);
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: true,
});
const { AUTHENTICATION_BACKENDS, ...rest } = authenticationData;
expect(SettingsAPI.updateAll).toHaveBeenCalledWith(rest);
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -107,11 +95,33 @@ describe('<ActivityStreamEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('authentication');
});
test('should successfully send request to api on form submission', async () => {
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
});
test('should navigate to miscellaneous detail on successful submission', async () => {
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(history.location.pathname).toEqual(
'/settings/miscellaneous_authentication/details'
);
});
test('should navigate to miscellaneous detail when cancel is clicked', async () => {
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
});
expect(history.location.pathname).toEqual(
'/settings/miscellaneous_authentication/details'
);
});
test('should display error message on unsuccessful submission', async () => {
@ -138,7 +148,7 @@ describe('<ActivityStreamEdit />', () => {
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<ActivityStreamEdit />
<MiscAuthenticationEdit />
</SettingsProvider>
);
});

View File

@ -0,0 +1 @@
export { default } from './MiscAuthenticationEdit';

View File

@ -0,0 +1 @@
export { default } from './MiscAuthentication';

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
import ContentError from '../../../components/ContentError';

View File

@ -18,84 +18,49 @@ import { sortNestedDetails, pluck } from '../../shared/settingUtils';
function MiscSystemDetail() {
const { me } = useConfig();
const { GET: allOptions } = useSettings();
const { GET: options } = useSettings();
const { isLoading, error, request, result: system } = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('all');
let DEFAULT_EXECUTION_ENVIRONMENT = '';
const { data } = await SettingsAPI.readCategory('system');
if (data.DEFAULT_EXECUTION_ENVIRONMENT) {
const {
data: { name },
} = await ExecutionEnvironmentsAPI.readDetail(
data.DEFAULT_EXECUTION_ENVIRONMENT
);
DEFAULT_EXECUTION_ENVIRONMENT = name;
data.DEFAULT_EXECUTION_ENVIRONMENT = name;
}
const {
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
...pluckedSystemData
} = pluck(
const systemData = pluck(
data,
'ALLOW_OAUTH2_FOR_EXTERNAL_USERS',
'AUTH_BASIC_ENABLED',
'ACTIVITY_STREAM_ENABLED',
'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC',
'AUTOMATION_ANALYTICS_GATHER_INTERVAL',
'AUTOMATION_ANALYTICS_URL',
'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH',
'DISABLE_LOCAL_AUTH',
'OAUTH2_PROVIDER',
'ORG_ADMINS_CAN_SEE_ALL_USERS',
'REDHAT_PASSWORD',
'REDHAT_USERNAME',
'REMOTE_HOST_HEADERS',
'SESSIONS_PER_USER',
'SESSION_COOKIE_AGE',
'REDHAT_PASSWORD',
'SUBSCRIPTIONS_USERNAME',
'SUBSCRIPTIONS_PASSWORD',
'TOWER_URL_BASE'
'INSTALL_UUID',
'REMOTE_HOST_HEADERS',
'TOWER_URL_BASE',
'DEFAULT_EXECUTION_ENVIRONMENT',
'PROXY_IP_ALLOWED_LIST',
'AUTOMATION_ANALYTICS_LAST_GATHER',
'AUTOMATION_ANALYTICS_LAST_ENTRIES'
);
const systemData = {
...pluckedSystemData,
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
DEFAULT_EXECUTION_ENVIRONMENT,
};
const {
OAUTH2_PROVIDER: OAUTH2_PROVIDER_OPTIONS,
...options
} = allOptions;
const systemOptions = {
...options,
ACCESS_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Access Token Expiration`,
},
REFRESH_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Refresh Token Expiration`,
},
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Authorization Code Expiration`,
},
};
const mergedData = {};
Object.keys(systemData).forEach(key => {
mergedData[key] = systemOptions[key];
mergedData[key] = options[key];
mergedData[key].value = systemData[key];
});
return sortNestedDetails(mergedData);
}, [allOptions]),
}, [options]),
null
);

View File

@ -22,28 +22,26 @@ describe('<MiscSystemDetail />', () => {
SettingsAPI.readCategory = jest.fn();
SettingsAPI.readCategory.mockResolvedValue({
data: {
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
AUTH_BASIC_ENABLED: true,
AUTOMATION_ANALYTICS_GATHER_INTERVAL: 14400,
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
ORG_ADMINS_CAN_SEE_ALL_USERS: true,
MANAGE_ORGANIZATION_AUTH: true,
TOWER_URL_BASE: 'https://towerhost',
REMOTE_HOST_HEADERS: [],
PROXY_IP_ALLOWED_LIST: [],
LICENSE: null,
REDHAT_USERNAME: 'name1',
REDHAT_PASSWORD: '$encrypted$',
SUBSCRIPTIONS_USERNAME: 'name2',
SUBSCRIPTIONS_PASSWORD: '$encrypted$',
AUTOMATION_ANALYTICS_URL: 'https://example.com',
INSTALL_UUID: 'db39b9ec-0c6e-4554-987d-42aw9c732ed8',
DEFAULT_EXECUTION_ENVIRONMENT: 1,
CUSTOM_VENV_PATHS: [],
INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com',
MANAGE_ORGANIZATION_AUTH: true,
DISABLE_LOCAL_AUTH: false,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 1,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 2,
REFRESH_TOKEN_EXPIRE_SECONDS: 3,
},
ORG_ADMINS_CAN_SEE_ALL_USERS: true,
REDHAT_PASSWORD: '$encrypted$',
REDHAT_USERNAME: 'mock name',
REMOTE_HOST_HEADERS: [],
SESSIONS_PER_USER: -1,
SESSION_COOKIE_AGE: 30000000000,
TOWER_URL_BASE: 'https://towerhost',
DEFAULT_EXECUTION_ENVIRONMENT: 1,
AUTOMATION_ANALYTICS_LAST_GATHER: null,
AUTOMATION_ANALYTICS_LAST_ENTRIES: 'foo',
AUTOMATION_ANALYTICS_GATHER_INTERVAL: 14400,
},
});
ExecutionEnvironmentsAPI.readDetail = jest.fn();
@ -77,14 +75,17 @@ describe('<MiscSystemDetail />', () => {
});
test('should render expected details', () => {
assertDetail(wrapper, 'Access Token Expiration', '1 seconds');
assertDetail(wrapper, 'All Users Visible to Organization Admins', 'On');
assertDetail(
wrapper,
'Allow External Users to Create OAuth2 Tokens',
'Off'
'Unique identifier for an installation',
'db39b9ec-0c6e-4554-987d-42aw9c732ed8'
);
assertDetail(wrapper, 'Authorization Code Expiration', '2 seconds');
assertDetail(
wrapper,
'Last gathered entries for expensive collectors for Insights for Ansible Automation Platform.',
'foo'
);
assertDetail(wrapper, 'All Users Visible to Organization Admins', 'On');
assertDetail(
wrapper,
'Insights for Ansible Automation Platform Gather Interval',
@ -96,32 +97,24 @@ describe('<MiscSystemDetail />', () => {
'https://example.com'
);
assertDetail(wrapper, 'Base URL of the service', 'https://towerhost');
assertDetail(wrapper, 'Enable HTTP Basic Auth', 'On');
assertDetail(
wrapper,
'Gather data for Insights for Ansible Automation Platform',
'Off'
);
assertDetail(wrapper, 'Idle Time Force Log Out', '30000000000 seconds');
assertDetail(
wrapper,
'Login redirect override URL',
'https://redirect.com'
);
assertDetail(
wrapper,
'Maximum number of simultaneous logged in sessions',
'-1'
);
assertDetail(
wrapper,
'Organization Admins Can Manage Users and Teams',
'On'
);
assertDetail(wrapper, 'Enable Activity Stream', 'On');
assertDetail(wrapper, 'Enable Activity Stream for Inventory Sync', 'Off');
assertDetail(wrapper, 'Red Hat customer password', 'Encrypted');
assertDetail(wrapper, 'Red Hat customer username', 'mock name');
assertDetail(wrapper, 'Refresh Token Expiration', '3 seconds');
assertDetail(wrapper, 'Red Hat customer username', 'name1');
assertDetail(wrapper, 'Red Hat or Satellite password', 'Encrypted');
assertDetail(wrapper, 'Red Hat or Satellite username', 'name2');
assertVariableDetail(wrapper, 'Remote Host Headers', '[]');
assertVariableDetail(wrapper, 'Proxy IP Allowed List', '[]');
assertDetail(wrapper, 'Global default execution environment', 'Foo');
});

View File

@ -30,76 +30,33 @@ function MiscSystemEdit() {
const { isLoading, error, request: fetchSystem, result: system } = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('all');
const {
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
...pluckedSystemData
} = pluck(
const { data } = await SettingsAPI.readCategory('system');
const systemData = pluck(
data,
'ALLOW_OAUTH2_FOR_EXTERNAL_USERS',
'AUTH_BASIC_ENABLED',
'ACTIVITY_STREAM_ENABLED',
'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC',
'AUTOMATION_ANALYTICS_GATHER_INTERVAL',
'AUTOMATION_ANALYTICS_URL',
'AUTOMATION_ANALYTICS_LAST_ENTRIES',
'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH',
'DISABLE_LOCAL_AUTH',
'OAUTH2_PROVIDER',
'ORG_ADMINS_CAN_SEE_ALL_USERS',
'REDHAT_PASSWORD',
'REDHAT_USERNAME',
'REDHAT_PASSWORD',
'SUBSCRIPTIONS_USERNAME',
'SUBSCRIPTIONS_PASSWORD',
'REMOTE_HOST_HEADERS',
'SESSIONS_PER_USER',
'SESSION_COOKIE_AGE',
'TOWER_URL_BASE',
'DEFAULT_EXECUTION_ENVIRONMENT'
'DEFAULT_EXECUTION_ENVIRONMENT',
'PROXY_IP_ALLOWED_LIST'
);
const systemData = {
...pluckedSystemData,
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
};
const {
OAUTH2_PROVIDER: OAUTH2_PROVIDER_OPTIONS,
...restOptions
} = options;
const systemOptions = {
...restOptions,
ACCESS_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Access Token Expiration`,
},
REFRESH_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Refresh Token Expiration`,
},
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS,
default:
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Authorization Code Expiration`,
},
};
const mergedData = {};
Object.keys(systemData).forEach(key => {
if (!systemOptions[key]) {
if (!options[key]) {
return;
}
mergedData[key] = systemOptions[key];
mergedData[key] = options[key];
mergedData[key].value = systemData[key];
});
return mergedData;
@ -122,50 +79,29 @@ function MiscSystemEdit() {
null
);
const handleSubmit = async form => {
const {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
...formData
} = form;
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('system');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...formData,
REMOTE_HOST_HEADERS: formatJson(formData.REMOTE_HOST_HEADERS),
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
...form,
PROXY_IP_ALLOWED_LIST: formatJson(form.PROXY_IP_ALLOWED_LIST),
REMOTE_HOST_HEADERS: formatJson(form.REMOTE_HOST_HEADERS),
DEFAULT_EXECUTION_ENVIRONMENT:
formData.DEFAULT_EXECUTION_ENVIRONMENT?.id || null,
form.DEFAULT_EXECUTION_ENVIRONMENT?.id || null,
});
};
const handleRevertAll = async () => {
const {
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
...systemData
} = system;
await revertAll();
const defaultValues = {};
Object.entries(systemData).forEach(([key, value]) => {
defaultValues[key] = value.default;
});
await submitForm({
...defaultValues,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: ACCESS_TOKEN_EXPIRE_SECONDS.default,
REFRESH_TOKEN_EXPIRE_SECONDS: REFRESH_TOKEN_EXPIRE_SECONDS.default,
AUTHORIZATION_CODE_EXPIRE_SECONDS:
AUTHORIZATION_CODE_EXPIRE_SECONDS.default,
},
});
closeModal();
history.push('/settings/miscellaneous_system/details');
};
const handleCancel = () => {
@ -226,6 +162,14 @@ function MiscSystemEdit() {
return (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<BooleanField
name="ACTIVITY_STREAM_ENABLED"
config={system.ACTIVITY_STREAM_ENABLED}
/>
<BooleanField
name="ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC"
config={system.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC}
/>
<ExecutionEnvironmentLookup
helperTextInvalid={
formik.errors.DEFAULT_EXECUTION_ENVIRONMENT
@ -267,52 +211,6 @@ function MiscSystemEdit() {
name="MANAGE_ORGANIZATION_AUTH"
config={system.MANAGE_ORGANIZATION_AUTH}
/>
<BooleanField
name="DISABLE_LOCAL_AUTH"
needsConfirmationModal
modalTitle={t`Confirm Disable Local Authorization`}
config={system.DISABLE_LOCAL_AUTH}
/>
<InputField
name="SESSION_COOKIE_AGE"
config={system.SESSION_COOKIE_AGE}
type="number"
isRequired
/>
<InputField
name="SESSIONS_PER_USER"
config={system.SESSIONS_PER_USER}
type="number"
isRequired
/>
<BooleanField
name="AUTH_BASIC_ENABLED"
config={system.AUTH_BASIC_ENABLED}
/>
<BooleanField
name="ALLOW_OAUTH2_FOR_EXTERNAL_USERS"
config={system.ALLOW_OAUTH2_FOR_EXTERNAL_USERS}
/>
<InputField
name="LOGIN_REDIRECT_OVERRIDE"
config={system.LOGIN_REDIRECT_OVERRIDE}
type="url"
/>
<InputField
name="ACCESS_TOKEN_EXPIRE_SECONDS"
config={system.ACCESS_TOKEN_EXPIRE_SECONDS}
type="number"
/>
<InputField
name="REFRESH_TOKEN_EXPIRE_SECONDS"
config={system.REFRESH_TOKEN_EXPIRE_SECONDS}
type="number"
/>
<InputField
name="AUTHORIZATION_CODE_EXPIRE_SECONDS"
config={system.AUTHORIZATION_CODE_EXPIRE_SECONDS}
type="number"
/>
<BooleanField
name="INSIGHTS_TRACKING_STATE"
config={system.INSIGHTS_TRACKING_STATE}
@ -325,6 +223,14 @@ function MiscSystemEdit() {
name="REDHAT_PASSWORD"
config={system.REDHAT_PASSWORD}
/>
<InputField
name="SUBSCRIPTIONS_USERNAME"
config={system.SUBSCRIPTIONS_USERNAME}
/>
<EncryptedField
name="SUBSCRIPTIONS_PASSWORD"
config={system.SUBSCRIPTIONS_PASSWORD}
/>
<InputField
name="AUTOMATION_ANALYTICS_URL"
config={system.AUTOMATION_ANALYTICS_URL}
@ -336,12 +242,22 @@ function MiscSystemEdit() {
type="number"
isRequired
/>
<InputField
name="AUTOMATION_ANALYTICS_LAST_ENTRIES"
config={system.AUTOMATION_ANALYTICS_LAST_ENTRIES}
/>
<ObjectField
name="REMOTE_HOST_HEADERS"
config={system.REMOTE_HOST_HEADERS}
isRequired
/>
<ObjectField
name="PROXY_IP_ALLOWED_LIST"
config={system.PROXY_IP_ALLOWED_LIST}
isRequired
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -23,27 +23,22 @@ const mockExecutionEnvironment = [
];
const systemData = {
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
AUTH_BASIC_ENABLED: true,
ACTIVITY_STREAM_ENABLED: true,
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: false,
AUTOMATION_ANALYTICS_GATHER_INTERVAL: 14400,
AUTOMATION_ANALYTICS_LAST_ENTRIES: '',
AUTOMATION_ANALYTICS_URL: 'https://example.com',
DEFAULT_EXECUTION_ENVIRONMENT: 1,
INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: '',
MANAGE_ORGANIZATION_AUTH: true,
DISABLE_LOCAL_AUTH: false,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 600,
REFRESH_TOKEN_EXPIRE_SECONDS: 2628000,
},
ORG_ADMINS_CAN_SEE_ALL_USERS: true,
REDHAT_PASSWORD: '',
REDHAT_USERNAME: '',
REDHAT_PASSWORD: '',
SUBSCRIPTIONS_USERNAME: '',
SUBSCRIPTIONS_PASSWORD: '',
REMOTE_HOST_HEADERS: ['REMOTE_ADDR', 'REMOTE_HOST'],
SESSIONS_PER_USER: -1,
SESSION_COOKIE_AGE: 1800,
TOWER_URL_BASE: 'https://localhost:3000',
PROXY_IP_ALLOWED_LIST: [],
};
describe('<MiscSystemEdit />', () => {
@ -55,6 +50,7 @@ describe('<MiscSystemEdit />', () => {
});
beforeEach(async () => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: mockAllSettings,
@ -116,7 +112,7 @@ describe('<MiscSystemEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -131,7 +127,8 @@ describe('<MiscSystemEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('system');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -47,18 +47,23 @@ function RADIUSEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('radius');
}, []),
null
);
const handleSubmit = async form => {
await submitForm(form);
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(radius).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/radius/details');
};
const handleCancel = () => {
@ -90,6 +95,7 @@ function RADIUSEdit() {
config={radius.RADIUS_SECRET}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<RADIUSEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -60,7 +61,7 @@ describe('<RADIUSEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -75,12 +76,8 @@ describe('<RADIUSEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
RADIUS_SERVER: '',
RADIUS_PORT: 1812,
RADIUS_SECRET: '',
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('radius');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -56,6 +56,13 @@ function SAMLEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('saml');
}, []),
null
);
const handleSubmit = async form => {
await submitForm({
...form,
@ -86,13 +93,11 @@ function SAMLEdit() {
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(saml).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/saml/details');
};
const handleCancel = () => {
@ -185,6 +190,7 @@ function SAMLEdit() {
config={saml.SOCIAL_AUTH_SAML_EXTRA_DATA}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<SAMLEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -131,7 +132,7 @@ describe('<SAMLEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -146,26 +147,8 @@ describe('<SAMLEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SAML_AUTO_CREATE_OBJECTS: true,
SOCIAL_AUTH_SAML_ENABLED_IDPS: {},
SOCIAL_AUTH_SAML_EXTRA_DATA: null,
SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {},
SOCIAL_AUTH_SAML_ORGANIZATION_MAP: null,
SOCIAL_AUTH_SAML_ORG_INFO: {},
SOCIAL_AUTH_SAML_SP_ENTITY_ID: '',
SOCIAL_AUTH_SAML_SP_EXTRA: null,
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '',
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: '',
SOCIAL_AUTH_SAML_SUPPORT_CONTACT: {},
SOCIAL_AUTH_SAML_TEAM_ATTR: {},
SOCIAL_AUTH_SAML_TEAM_MAP: null,
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: {},
SOCIAL_AUTH_SAML_SECURITY_CONFIG: {
requestedAuthnContext: false,
},
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('saml');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -107,8 +107,8 @@ function SettingList() {
path: '/settings/miscellaneous_system',
},
{
title: t`Activity Stream settings`,
path: '/settings/activity_stream',
title: t`Miscellaneous Authentication settings`,
path: '/settings/miscellaneous_authentication',
},
{
title: t`Logging settings`,

View File

@ -1,12 +1,10 @@
import React, { useCallback, useEffect } from 'react';
import { Link, Route, Switch, Redirect } from 'react-router-dom';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
import ContentError from '../../components/ContentError';
import ContentLoading from '../../components/ContentLoading';
import ScreenHeader from '../../components/ScreenHeader';
import ActivityStream from './ActivityStream';
import AzureAD from './AzureAD';
import GitHub from './GitHub';
import GoogleOAuth2 from './GoogleOAuth2';
@ -14,6 +12,7 @@ import Jobs from './Jobs';
import LDAP from './LDAP';
import Subscription from './Subscription';
import Logging from './Logging';
import MiscAuthentication from './MiscAuthentication';
import MiscSystem from './MiscSystem';
import RADIUS from './RADIUS';
import SAML from './SAML';
@ -94,6 +93,9 @@ function Settings() {
'/settings/logging': t`Logging`,
'/settings/logging/details': t`Details`,
'/settings/logging/edit': t`Edit Details`,
'/settings/miscellaneous_authentication': t`Miscellaneous Authentication`,
'/settings/miscellaneous_authentication/details': t`Details`,
'/settings/miscellaneous_authentication/edit': t`Edit Details`,
'/settings/miscellaneous_system': t`Miscellaneous System`,
'/settings/miscellaneous_system/details': t`Details`,
'/settings/miscellaneous_system/edit': t`Edit Details`,
@ -142,9 +144,6 @@ function Settings() {
<SettingsProvider value={result}>
<ScreenHeader streamType="setting" breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path="/settings/activity_stream">
<ActivityStream />
</Route>
<Route path="/settings/azure">
<AzureAD />
</Route>
@ -170,6 +169,9 @@ function Settings() {
<Route path="/settings/logging">
<Logging />
</Route>
<Route path="/settings/miscellaneous_authentication">
<MiscAuthentication />
</Route>
<Route path="/settings/miscellaneous_system">
<MiscSystem />
</Route>

View File

@ -51,18 +51,23 @@ function TACACSEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('tacacsplus');
}, []),
null
);
const handleSubmit = async form => {
await submitForm(form);
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(tacacs).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push('/settings/tacacs/details');
};
const handleCancel = () => {
@ -107,6 +112,7 @@ function TACACSEdit() {
config={tacacs.TACACSPLUS_AUTH_PROTOCOL}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<TACACSEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -68,7 +69,7 @@ describe('<TACACSEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -83,14 +84,8 @@ describe('<TACACSEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
TACACSPLUS_HOST: '',
TACACSPLUS_PORT: 49,
TACACSPLUS_SECRET: '',
TACACSPLUS_SESSION_TIMEOUT: 5,
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('tacacsplus');
});
test('should successfully send request to api on form submission', async () => {

View File

@ -65,18 +65,26 @@ function UIEdit() {
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('ui');
}, []),
null
);
const handleSubmit = async form => {
await submitForm(form);
};
const handleRevertAll = async () => {
const defaultValues = Object.assign(
...Object.entries(uiData).map(([key, value]) => ({
[key]: value.default,
}))
);
await submitForm(defaultValues);
await revertAll();
closeModal();
history.push({
pathname: '/settings/ui/details',
hardReload: true,
});
};
const handleCancel = () => {
@ -115,6 +123,7 @@ function UIEdit() {
type="dataURL"
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}

View File

@ -17,6 +17,7 @@ describe('<UIEdit />', () => {
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
@ -62,7 +63,7 @@ describe('<UIEdit />', () => {
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
@ -77,12 +78,8 @@ describe('<UIEdit />', () => {
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
CUSTOM_LOGIN_INFO: '',
CUSTOM_LOGO: '',
PENDO_TRACKING_STATE: 'off',
});
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('ui');
});
test('should successfully send request to api on form submission', async () => {