foreman_openscap 4.3.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
- data/app/controllers/api/v2/compliance/oval_policies_controller.rb +1 -1
- data/app/graphql/mutations/oval_contents/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/create.rb +33 -0
- data/app/graphql/mutations/oval_policies/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/update.rb +15 -0
- data/app/graphql/types/oval_check.rb +11 -0
- data/app/graphql/types/oval_content.rb +2 -0
- data/app/graphql/types/oval_policy.rb +3 -0
- data/app/helpers/arf_report_dashboard_helper.rb +2 -4
- data/app/helpers/compliance_hosts_helper.rb +1 -1
- data/app/helpers/policies_helper.rb +2 -2
- data/app/models/concerns/foreman_openscap/data_stream_content.rb +1 -1
- data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
- data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +16 -0
- data/app/models/foreman_openscap/arf_report.rb +1 -1
- data/app/models/foreman_openscap/oval_content.rb +2 -0
- data/app/services/foreman_openscap/client_config/base.rb +1 -0
- data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
- data/app/services/foreman_openscap/oval/configure.rb +16 -13
- data/app/services/foreman_openscap/oval/setup.rb +5 -5
- data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
- data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
- data/app/views/arf_reports/_metrics.html.erb +4 -4
- data/app/views/compliance_hosts/show.html.erb +4 -6
- data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
- data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
- data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
- data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
- data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
- data/lib/foreman_openscap/engine.rb +8 -9
- data/lib/foreman_openscap/version.rb +1 -1
- data/package.json +3 -6
- data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
- data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
- data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
- data/test/graphql/queries/oval_content_query_test.rb +29 -0
- data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
- data/test/helpers/policy_dashboard_helper_test.rb +1 -1
- data/test/test_plugin_helper.rb +9 -4
- data/test/unit/policy_test.rb +1 -1
- data/test/unit/services/config_name_service_test.rb +1 -0
- data/test/unit/services/hostgroup_overrider_test.rb +2 -1
- data/test/unit/services/lookup_key_overrider_test.rb +4 -1
- data/test/unit/services/oval/setup_check_test.rb +37 -0
- data/webpack/components/ConfirmModal.js +63 -0
- data/webpack/components/ConfirmModal.scss +3 -0
- data/webpack/components/EditableInput.js +163 -0
- data/webpack/components/EditableInput.scss +3 -0
- data/webpack/components/EmptyState.js +12 -3
- data/webpack/components/IndexLayout.js +11 -4
- data/webpack/components/IndexTable/index.js +21 -16
- data/webpack/components/LinkButton.js +38 -0
- data/webpack/components/withDeleteModal.js +51 -0
- data/webpack/components/withLoading.js +44 -5
- data/webpack/graphql/mutations/createOvalPolicy.gql +22 -0
- data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
- data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
- data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
- data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
- data/webpack/graphql/queries/cves.gql +5 -0
- data/webpack/graphql/queries/hostgroups.gql +14 -0
- data/webpack/graphql/queries/ovalContent.gql +8 -0
- data/webpack/graphql/queries/ovalContents.gql +8 -0
- data/webpack/graphql/queries/ovalPolicies.gql +8 -0
- data/webpack/graphql/queries/ovalPolicy.gql +8 -0
- data/webpack/helpers/formFieldsHelper.js +113 -0
- data/webpack/helpers/globalIdHelper.js +4 -2
- data/webpack/helpers/mutationHelper.js +68 -0
- data/webpack/helpers/pathsHelper.js +10 -3
- data/webpack/helpers/permissionsHelper.js +42 -0
- data/webpack/helpers/toastHelper.js +3 -0
- data/webpack/helpers/toastsHelper.js +3 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +98 -77
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
- data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
- data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
- data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
- data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +34 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +87 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +80 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +202 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +50 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
- data/webpack/routes/routes.js +21 -0
- data/webpack/testHelper.js +64 -2
- metadata +63 -7
|
@@ -4,9 +4,15 @@ import { within } from '@testing-library/dom';
|
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
|
|
7
|
-
import OvalContentsIndex from '../
|
|
7
|
+
import OvalContentsIndex from '../';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
withRouter,
|
|
11
|
+
withRedux,
|
|
12
|
+
withMockedProvider,
|
|
13
|
+
tick,
|
|
14
|
+
historyMock,
|
|
15
|
+
} from '../../../../testHelper';
|
|
10
16
|
import { ovalContentsPath } from '../../../../helpers/pathsHelper';
|
|
11
17
|
|
|
12
18
|
import {
|
|
@@ -16,20 +22,30 @@ import {
|
|
|
16
22
|
pagePaginationHistoryMock,
|
|
17
23
|
emptyMocks,
|
|
18
24
|
errorMocks,
|
|
25
|
+
viewerMocks,
|
|
26
|
+
unauthorizedMocks,
|
|
19
27
|
} from './OvalContentsIndex.fixtures';
|
|
20
28
|
|
|
21
|
-
const TestComponent =
|
|
29
|
+
const TestComponent = withRedux(
|
|
30
|
+
withRouter(withMockedProvider(OvalContentsIndex))
|
|
31
|
+
);
|
|
22
32
|
|
|
23
33
|
describe('OvalContentsIndex', () => {
|
|
24
34
|
it('should load page', async () => {
|
|
25
35
|
const { container } = render(
|
|
26
|
-
<TestComponent history={historyMock} mocks={mocks} />
|
|
36
|
+
<TestComponent history={historyMock} mocks={mocks} location={{}} />
|
|
27
37
|
);
|
|
28
38
|
expect(screen.getByText('Loading')).toBeInTheDocument();
|
|
29
39
|
await waitFor(tick);
|
|
30
40
|
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
31
41
|
expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByText(
|
|
44
|
+
'http://oval-content-source/security/data/oval/ansible-2-including-unpatched.oval.xml.bz2'
|
|
45
|
+
)
|
|
46
|
+
).toBeInTheDocument();
|
|
32
47
|
expect(screen.getByText('openshift OVAL content')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('openshift.oval.xml.bz2')).toBeInTheDocument();
|
|
33
49
|
const pageItems = container.querySelector('.pf-c-pagination__total-items');
|
|
34
50
|
expect(within(pageItems).getByText(/1 - 4/)).toBeInTheDocument();
|
|
35
51
|
expect(within(pageItems).getByText('of')).toBeInTheDocument();
|
|
@@ -39,6 +55,7 @@ describe('OvalContentsIndex', () => {
|
|
|
39
55
|
const { container } = render(
|
|
40
56
|
<TestComponent
|
|
41
57
|
history={pagePaginationHistoryMock}
|
|
58
|
+
location={{}}
|
|
42
59
|
mocks={paginatedMocks}
|
|
43
60
|
/>
|
|
44
61
|
);
|
|
@@ -56,14 +73,18 @@ describe('OvalContentsIndex', () => {
|
|
|
56
73
|
);
|
|
57
74
|
});
|
|
58
75
|
it('should show empty state', async () => {
|
|
59
|
-
render(
|
|
76
|
+
render(
|
|
77
|
+
<TestComponent history={historyMock} mocks={emptyMocks} location={{}} />
|
|
78
|
+
);
|
|
60
79
|
expect(screen.getByText('Loading')).toBeInTheDocument();
|
|
61
80
|
await waitFor(tick);
|
|
62
81
|
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
63
82
|
expect(screen.getByText('No OVAL Contents found.')).toBeInTheDocument();
|
|
64
83
|
});
|
|
65
84
|
it('should show errors', async () => {
|
|
66
|
-
render(
|
|
85
|
+
render(
|
|
86
|
+
<TestComponent history={historyMock} mocks={errorMocks} location={{}} />
|
|
87
|
+
);
|
|
67
88
|
expect(screen.getByText('Loading')).toBeInTheDocument();
|
|
68
89
|
await waitFor(tick);
|
|
69
90
|
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
@@ -72,4 +93,30 @@ describe('OvalContentsIndex', () => {
|
|
|
72
93
|
).toBeInTheDocument();
|
|
73
94
|
expect(screen.getByText('Error!')).toBeInTheDocument();
|
|
74
95
|
});
|
|
96
|
+
it('should load page for user with permissions', async () => {
|
|
97
|
+
render(
|
|
98
|
+
<TestComponent history={historyMock} mocks={viewerMocks} location={{}} />
|
|
99
|
+
);
|
|
100
|
+
await waitFor(tick);
|
|
101
|
+
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
102
|
+
expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
it('should not load page for user without permissions', async () => {
|
|
105
|
+
render(
|
|
106
|
+
<TestComponent
|
|
107
|
+
history={historyMock}
|
|
108
|
+
mocks={unauthorizedMocks}
|
|
109
|
+
location={{}}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
await waitFor(tick);
|
|
113
|
+
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
114
|
+
expect(screen.queryByText('ansible OVAL content')).not.toBeInTheDocument();
|
|
115
|
+
expect(
|
|
116
|
+
screen.getByText(
|
|
117
|
+
'You are not authorized to view the page. Request the following permissions from administrator: view_oval_contents.'
|
|
118
|
+
)
|
|
119
|
+
).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText('Permission denied')).toBeInTheDocument();
|
|
121
|
+
});
|
|
75
122
|
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { useDispatch } from 'react-redux';
|
|
3
|
+
import { showToast } from '../../../helpers/toastHelper';
|
|
2
4
|
|
|
3
5
|
import OvalContentsIndex from './OvalContentsIndex';
|
|
4
6
|
|
|
5
|
-
const WrappedOvalContentsIndex = props =>
|
|
7
|
+
const WrappedOvalContentsIndex = props => {
|
|
8
|
+
const dispatch = useDispatch();
|
|
9
|
+
|
|
10
|
+
return <OvalContentsIndex {...props} showToast={showToast(dispatch)} />;
|
|
11
|
+
};
|
|
6
12
|
|
|
7
13
|
export default WrappedOvalContentsIndex;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import { Formik, Field as FormikField } from 'formik';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Form as PfForm,
|
|
8
|
+
ActionGroup,
|
|
9
|
+
Button,
|
|
10
|
+
FileUpload,
|
|
11
|
+
FormGroup,
|
|
12
|
+
Radio,
|
|
13
|
+
Spinner,
|
|
14
|
+
} from '@patternfly/react-core';
|
|
15
|
+
import {
|
|
16
|
+
onSubmit,
|
|
17
|
+
createValidationSchema,
|
|
18
|
+
validateFile,
|
|
19
|
+
submitDisabled,
|
|
20
|
+
} from './OvalContentsNewHelper';
|
|
21
|
+
import LinkButton from '../../../components/LinkButton';
|
|
22
|
+
import IndexLayout from '../../../components/IndexLayout';
|
|
23
|
+
import { TextField } from '../../../helpers/formFieldsHelper';
|
|
24
|
+
import { ovalContentsPath } from '../../../helpers/pathsHelper';
|
|
25
|
+
|
|
26
|
+
import './OvalContentsNew.scss';
|
|
27
|
+
|
|
28
|
+
const OvalContentsNew = props => {
|
|
29
|
+
const [file, setFile] = useState(null);
|
|
30
|
+
const [fileTouched, setFileTouched] = useState(false);
|
|
31
|
+
const [fileFromUrl, setFileFromUrl] = useState(true);
|
|
32
|
+
|
|
33
|
+
const handleFileChange = (value, filename, event) => {
|
|
34
|
+
setFile(value);
|
|
35
|
+
setFileTouched(true);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<IndexLayout pageTitle={__('New OVAL Content')} contentWidthSpan={6}>
|
|
40
|
+
<Formik
|
|
41
|
+
onSubmit={(values, actions) =>
|
|
42
|
+
onSubmit(
|
|
43
|
+
values,
|
|
44
|
+
actions,
|
|
45
|
+
props.showToast,
|
|
46
|
+
props.history,
|
|
47
|
+
fileFromUrl,
|
|
48
|
+
file
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
initialValues={{ name: '', url: '' }}
|
|
52
|
+
validationSchema={createValidationSchema(fileFromUrl)}
|
|
53
|
+
>
|
|
54
|
+
{formProps => (
|
|
55
|
+
<PfForm>
|
|
56
|
+
<FormikField
|
|
57
|
+
label="Name"
|
|
58
|
+
name="name"
|
|
59
|
+
component={TextField}
|
|
60
|
+
isRequired
|
|
61
|
+
/>
|
|
62
|
+
<FormGroup label={__('OVAL Content Source')}>
|
|
63
|
+
<Radio
|
|
64
|
+
id="scap-file-source-url"
|
|
65
|
+
isChecked={fileFromUrl}
|
|
66
|
+
isDisabled={formProps.isSubmitting}
|
|
67
|
+
name="fileSource"
|
|
68
|
+
onChange={() => {
|
|
69
|
+
setFileFromUrl(true);
|
|
70
|
+
// Force validations to run by setting the same value.
|
|
71
|
+
// Workaround for https://github.com/formium/formik/issues/1755
|
|
72
|
+
formProps.setFieldValue(formProps.values.url);
|
|
73
|
+
}}
|
|
74
|
+
label={__('OVAL Content from URL')}
|
|
75
|
+
/>
|
|
76
|
+
<Radio
|
|
77
|
+
id="scap-file-source-file"
|
|
78
|
+
isChecked={!fileFromUrl}
|
|
79
|
+
isDisabled={formProps.isSubmitting}
|
|
80
|
+
name="fileSource"
|
|
81
|
+
onChange={() => {
|
|
82
|
+
setFileFromUrl(false);
|
|
83
|
+
const filtered = Object.entries(formProps.errors).filter(
|
|
84
|
+
([key, value]) => key !== 'url'
|
|
85
|
+
);
|
|
86
|
+
formProps.setErrors(Object.fromEntries(filtered));
|
|
87
|
+
}}
|
|
88
|
+
label={__('OVAL Content from file')}
|
|
89
|
+
/>
|
|
90
|
+
</FormGroup>
|
|
91
|
+
{!fileFromUrl ? (
|
|
92
|
+
<FormGroup label="File" isRequired>
|
|
93
|
+
<FileUpload
|
|
94
|
+
value={file}
|
|
95
|
+
filename={file ? file.name : ''}
|
|
96
|
+
onChange={handleFileChange}
|
|
97
|
+
isDisabled={formProps.isSubmitting}
|
|
98
|
+
validated={validateFile(file, fileTouched)}
|
|
99
|
+
/>
|
|
100
|
+
</FormGroup>
|
|
101
|
+
) : (
|
|
102
|
+
<FormikField
|
|
103
|
+
label={__('URL')}
|
|
104
|
+
name="url"
|
|
105
|
+
component={TextField}
|
|
106
|
+
placeholder="https://www.redhat.com/security/data/oval/v2/RHEL8/rhel-8.oval.xml.bz2"
|
|
107
|
+
isRequired
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
<ActionGroup>
|
|
111
|
+
<Button
|
|
112
|
+
variant="primary"
|
|
113
|
+
onClick={formProps.handleSubmit}
|
|
114
|
+
isDisabled={submitDisabled(formProps, file, fileFromUrl)}
|
|
115
|
+
>
|
|
116
|
+
{__('Submit')}
|
|
117
|
+
</Button>
|
|
118
|
+
<LinkButton
|
|
119
|
+
btnVariant="link"
|
|
120
|
+
isDisabled={formProps.isSubmitting}
|
|
121
|
+
btnText={__('Cancel')}
|
|
122
|
+
path={ovalContentsPath}
|
|
123
|
+
/>
|
|
124
|
+
{formProps.isSubmitting ? <Spinner size="lg" /> : null}
|
|
125
|
+
</ActionGroup>
|
|
126
|
+
</PfForm>
|
|
127
|
+
)}
|
|
128
|
+
</Formik>
|
|
129
|
+
</IndexLayout>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
OvalContentsNew.propTypes = {
|
|
134
|
+
showToast: PropTypes.func.isRequired,
|
|
135
|
+
history: PropTypes.object.isRequired,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default OvalContentsNew;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as Yup from 'yup';
|
|
2
|
+
|
|
3
|
+
import api from 'foremanReact/redux/API/API';
|
|
4
|
+
import { prepareErrors } from 'foremanReact/redux/actions/common/forms';
|
|
5
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
import {
|
|
7
|
+
ovalContentsPath,
|
|
8
|
+
ovalContentsApiPath,
|
|
9
|
+
} from '../../../helpers/pathsHelper';
|
|
10
|
+
|
|
11
|
+
export const submitForm = (params, actions) => {
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'multipart/form-data',
|
|
14
|
+
};
|
|
15
|
+
return api.post(ovalContentsApiPath, params, headers);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const onSubmit = async (
|
|
19
|
+
values,
|
|
20
|
+
actions,
|
|
21
|
+
showToast,
|
|
22
|
+
history,
|
|
23
|
+
fileFromUrl,
|
|
24
|
+
file
|
|
25
|
+
) => {
|
|
26
|
+
const formData = new FormData();
|
|
27
|
+
if (fileFromUrl) {
|
|
28
|
+
formData.append('oval_content[url]', values.url);
|
|
29
|
+
} else {
|
|
30
|
+
formData.append('oval_content[scap_file]', file);
|
|
31
|
+
}
|
|
32
|
+
formData.append('oval_content[name]', values.name);
|
|
33
|
+
try {
|
|
34
|
+
await submitForm(formData, actions);
|
|
35
|
+
history.push(ovalContentsPath, { refreshOvalContents: true });
|
|
36
|
+
showToast({
|
|
37
|
+
type: 'success',
|
|
38
|
+
message: sprintf(__('OVAL Content %s successfully created'), values.name),
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
onError(error, actions, showToast);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const onError = (error, actions, showToast) => {
|
|
46
|
+
actions.setSubmitting(false);
|
|
47
|
+
if (error.response?.status === 422) {
|
|
48
|
+
actions.setErrors(prepareErrors(error?.response?.data?.error?.errors, {}));
|
|
49
|
+
} else {
|
|
50
|
+
showToast({
|
|
51
|
+
type: 'error',
|
|
52
|
+
message: __(
|
|
53
|
+
'Unknown error when submitting data, please try again later.'
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const validateFile = (file, touched) => {
|
|
60
|
+
if (!touched) {
|
|
61
|
+
return 'default';
|
|
62
|
+
}
|
|
63
|
+
return file ? 'success' : 'error';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const submitDisabled = (formProps, file, fileFromUrl) =>
|
|
67
|
+
formProps.isSubmitting || !formProps.isValid || (!fileFromUrl && !file);
|
|
68
|
+
|
|
69
|
+
export const createValidationSchema = contentFromUrl =>
|
|
70
|
+
Yup.object().shape({
|
|
71
|
+
name: Yup.string().required("can't be blank"),
|
|
72
|
+
...(contentFromUrl && { url: Yup.string().required("can't be blank") }),
|
|
73
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import api from 'foremanReact/redux/API/API';
|
|
7
|
+
|
|
8
|
+
import OvalContentsNew from '../OvalContentsNew';
|
|
9
|
+
import { withRouter, withRedux, tick } from '../../../../testHelper';
|
|
10
|
+
import { ovalContentsPath } from '../../../../helpers/pathsHelper';
|
|
11
|
+
|
|
12
|
+
jest.mock('foremanReact/redux/API/API', () => ({ post: jest.fn() }));
|
|
13
|
+
|
|
14
|
+
const TestComponent = withRouter(withRedux(OvalContentsNew));
|
|
15
|
+
|
|
16
|
+
describe('OvalContentsNew', () => {
|
|
17
|
+
it('should create with content from URL', async () => {
|
|
18
|
+
const pushMock = jest.fn();
|
|
19
|
+
const toastMock = jest.fn();
|
|
20
|
+
|
|
21
|
+
api.post.mockImplementation(() => Promise.resolve());
|
|
22
|
+
|
|
23
|
+
render(
|
|
24
|
+
<TestComponent history={{ push: pushMock }} showToast={toastMock} />
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByText('OVAL Content Source')).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText('URL')).toBeInTheDocument();
|
|
29
|
+
expect(screen.queryByText('File')).not.toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('Submit')).toBeDisabled();
|
|
31
|
+
userEvent.type(screen.getByLabelText('name'), 'test content');
|
|
32
|
+
await waitFor(tick);
|
|
33
|
+
expect(screen.getByText('Submit')).toBeDisabled();
|
|
34
|
+
userEvent.type(
|
|
35
|
+
screen.getByLabelText(/url/),
|
|
36
|
+
'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2'
|
|
37
|
+
);
|
|
38
|
+
await waitFor(tick);
|
|
39
|
+
expect(screen.getByText('Submit')).not.toBeDisabled();
|
|
40
|
+
userEvent.click(screen.getByText('Submit'));
|
|
41
|
+
await waitFor(tick);
|
|
42
|
+
expect(pushMock).toHaveBeenCalledWith(ovalContentsPath, {
|
|
43
|
+
refreshOvalContents: true,
|
|
44
|
+
});
|
|
45
|
+
expect(toastMock).toHaveBeenCalledWith({
|
|
46
|
+
type: 'success',
|
|
47
|
+
message: 'OVAL Content test content successfully created',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it('should show resource errors', async () => {
|
|
51
|
+
const pushMock = jest.fn();
|
|
52
|
+
const toastMock = jest.fn();
|
|
53
|
+
api.post.mockImplementation(() => {
|
|
54
|
+
// eslint-disable-next-line no-throw-literal
|
|
55
|
+
throw {
|
|
56
|
+
response: {
|
|
57
|
+
status: 422,
|
|
58
|
+
data: { error: { errors: { name: ['has already been taken'] } } },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<TestComponent history={{ push: pushMock }} showToast={toastMock} />
|
|
65
|
+
);
|
|
66
|
+
userEvent.type(screen.getByLabelText('name'), 'test content');
|
|
67
|
+
userEvent.type(
|
|
68
|
+
screen.getByLabelText(/url/),
|
|
69
|
+
'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2'
|
|
70
|
+
);
|
|
71
|
+
await waitFor(tick);
|
|
72
|
+
userEvent.click(screen.getByText('Submit'));
|
|
73
|
+
await waitFor(tick);
|
|
74
|
+
expect(pushMock).not.toHaveBeenCalled();
|
|
75
|
+
expect(screen.getByText('has already been taken')).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
it('should show error toast on unexpected error', async () => {
|
|
78
|
+
const pushMock = jest.fn();
|
|
79
|
+
const toastMock = jest.fn();
|
|
80
|
+
|
|
81
|
+
api.post.mockImplementation(() => {
|
|
82
|
+
// eslint-disable-next-line no-throw-literal
|
|
83
|
+
throw { response: { status: 500 } };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<TestComponent history={{ push: pushMock }} showToast={toastMock} />
|
|
88
|
+
);
|
|
89
|
+
userEvent.type(screen.getByLabelText('name'), 'test content');
|
|
90
|
+
userEvent.type(
|
|
91
|
+
screen.getByLabelText(/url/),
|
|
92
|
+
'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2'
|
|
93
|
+
);
|
|
94
|
+
await waitFor(tick);
|
|
95
|
+
userEvent.click(screen.getByText('Submit'));
|
|
96
|
+
await waitFor(tick);
|
|
97
|
+
expect(pushMock).not.toHaveBeenCalled();
|
|
98
|
+
expect(screen.getByText('Submit')).not.toBeDisabled();
|
|
99
|
+
expect(toastMock).toHaveBeenCalledWith({
|
|
100
|
+
type: 'error',
|
|
101
|
+
message: 'Unknown error when submitting data, please try again later.',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useDispatch } from 'react-redux';
|
|
3
|
+
import { showToast } from '../../../helpers/toastHelper';
|
|
4
|
+
|
|
5
|
+
import OvalContentsNew from './OvalContentsNew';
|
|
6
|
+
|
|
7
|
+
const WrappedOvalContentsNew = props => {
|
|
8
|
+
const dispatch = useDispatch();
|
|
9
|
+
|
|
10
|
+
return <OvalContentsNew {...props} showToast={showToast(dispatch)} />;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default WrappedOvalContentsNew;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Helmet } from 'react-helmet';
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Grid,
|
|
8
|
+
GridItem,
|
|
9
|
+
TextContent,
|
|
10
|
+
Text,
|
|
11
|
+
TextVariants,
|
|
12
|
+
} from '@patternfly/react-core';
|
|
13
|
+
|
|
14
|
+
import withLoading from '../../../components/withLoading';
|
|
15
|
+
|
|
16
|
+
const OvalContentsShow = ({ ovalContent }) => {
|
|
17
|
+
let contentSource;
|
|
18
|
+
if (ovalContent.url) {
|
|
19
|
+
contentSource = (
|
|
20
|
+
<React.Fragment>
|
|
21
|
+
<Text component={TextVariants.h3}>{__('URL')}</Text>
|
|
22
|
+
<Text component={TextVariants.p}>{ovalContent.url || ''}</Text>
|
|
23
|
+
</React.Fragment>
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
contentSource = (
|
|
27
|
+
<React.Fragment>
|
|
28
|
+
<Text component={TextVariants.h3}>{__('File')}</Text>
|
|
29
|
+
<Text component={TextVariants.p}>
|
|
30
|
+
{ovalContent.originalFilename || ''}
|
|
31
|
+
</Text>
|
|
32
|
+
</React.Fragment>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<React.Fragment>
|
|
38
|
+
<Helmet>
|
|
39
|
+
<title>{`${ovalContent.name} | ${__('OVAL Content')}`}</title>
|
|
40
|
+
</Helmet>
|
|
41
|
+
<Grid className="scap-page-grid">
|
|
42
|
+
<GridItem span={10}>
|
|
43
|
+
<Text component={TextVariants.h1}>{ovalContent.name}</Text>
|
|
44
|
+
</GridItem>
|
|
45
|
+
<GridItem span={2} />
|
|
46
|
+
<GridItem span={12}>
|
|
47
|
+
<TextContent className="pf-u-pt-md">
|
|
48
|
+
<Text component={TextVariants.h3}>{__('Name')}</Text>
|
|
49
|
+
<Text component={TextVariants.p}>{ovalContent.name}</Text>
|
|
50
|
+
{contentSource}
|
|
51
|
+
</TextContent>
|
|
52
|
+
</GridItem>
|
|
53
|
+
</Grid>
|
|
54
|
+
</React.Fragment>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
OvalContentsShow.propTypes = {
|
|
59
|
+
ovalContent: PropTypes.object.isRequired,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default withLoading(OvalContentsShow);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
|
|
5
|
+
import { withMockedProvider, tick } from '../../../testHelper';
|
|
6
|
+
import ovalContentQuery from '../../../graphql/queries/ovalContent.gql';
|
|
7
|
+
import OvalContentsShow from './';
|
|
8
|
+
|
|
9
|
+
const TestComponent = withMockedProvider(OvalContentsShow);
|
|
10
|
+
|
|
11
|
+
const matchMock = { params: { id: 4 } };
|
|
12
|
+
const name = 'dotnet OVAL content';
|
|
13
|
+
const url =
|
|
14
|
+
'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2';
|
|
15
|
+
const id = 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC00';
|
|
16
|
+
|
|
17
|
+
const mocks = [
|
|
18
|
+
{
|
|
19
|
+
request: {
|
|
20
|
+
query: ovalContentQuery,
|
|
21
|
+
variables: { id },
|
|
22
|
+
},
|
|
23
|
+
result: {
|
|
24
|
+
data: {
|
|
25
|
+
ovalContent: {
|
|
26
|
+
id,
|
|
27
|
+
name,
|
|
28
|
+
url,
|
|
29
|
+
originalFilename: '',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe('OVAL Contents show page', () => {
|
|
37
|
+
it('should show OVAL Content', async () => {
|
|
38
|
+
render(<TestComponent match={matchMock} mocks={mocks} />);
|
|
39
|
+
expect(screen.getByText('Loading')).toBeInTheDocument();
|
|
40
|
+
await waitFor(tick);
|
|
41
|
+
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
|
42
|
+
expect(screen.getAllByText(name).length === 2).toBeTruthy();
|
|
43
|
+
expect(screen.getByText(url)).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useQuery } from '@apollo/client';
|
|
4
|
+
|
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
|
|
7
|
+
import OvalContentsShow from './OvalContentsShow';
|
|
8
|
+
import { encodeId } from '../../../helpers/globalIdHelper';
|
|
9
|
+
|
|
10
|
+
import ovalContent from '../../../graphql/queries/ovalContent.gql';
|
|
11
|
+
|
|
12
|
+
const WrappedOvalContentsShow = props => {
|
|
13
|
+
const id = encodeId('ForemanOpenscap::OvalContent', props.match.params.id);
|
|
14
|
+
|
|
15
|
+
const useFetchFn = componentProps =>
|
|
16
|
+
useQuery(ovalContent, { variables: { id } });
|
|
17
|
+
|
|
18
|
+
const renameData = data => ({ ovalContent: data.ovalContent });
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<OvalContentsShow
|
|
22
|
+
{...props}
|
|
23
|
+
fetchFn={useFetchFn}
|
|
24
|
+
renameData={renameData}
|
|
25
|
+
resultPath="ovalContent"
|
|
26
|
+
emptyStateTitle={__('No OVAL Content found')}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
WrappedOvalContentsShow.propTypes = {
|
|
32
|
+
match: PropTypes.object.isRequired,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default WrappedOvalContentsShow;
|
|
@@ -4,6 +4,8 @@ import { useQuery } from '@apollo/client';
|
|
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
|
|
6
6
|
import OvalPoliciesTable from './OvalPoliciesTable';
|
|
7
|
+
import { submitDelete, prepareMutation } from '../../../helpers/mutationHelper';
|
|
8
|
+
|
|
7
9
|
import IndexLayout from '../../../components/IndexLayout';
|
|
8
10
|
|
|
9
11
|
import {
|
|
@@ -11,10 +13,9 @@ import {
|
|
|
11
13
|
useCurrentPagination,
|
|
12
14
|
} from '../../../helpers/pageParamsHelper';
|
|
13
15
|
import policiesQuery from '../../../graphql/queries/ovalPolicies.gql';
|
|
16
|
+
import deleteOvalPolicyMutation from '../../../graphql/mutations/deleteOvalPolicy.gql';
|
|
14
17
|
|
|
15
18
|
const OvalPoliciesIndex = props => {
|
|
16
|
-
const pagination = useCurrentPagination(props.history);
|
|
17
|
-
|
|
18
19
|
const useFetchFn = componentProps =>
|
|
19
20
|
useQuery(policiesQuery, {
|
|
20
21
|
variables: useParamsToVars(componentProps.history),
|
|
@@ -25,6 +26,8 @@ const OvalPoliciesIndex = props => {
|
|
|
25
26
|
totalCount: data.ovalPolicies.totalCount,
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
const pagination = useCurrentPagination(props.history);
|
|
30
|
+
|
|
28
31
|
return (
|
|
29
32
|
<IndexLayout pageTitle={__('OVAL Policies')}>
|
|
30
33
|
<OvalPoliciesTable
|
|
@@ -34,6 +37,18 @@ const OvalPoliciesIndex = props => {
|
|
|
34
37
|
resultPath="ovalPolicies.nodes"
|
|
35
38
|
pagination={pagination}
|
|
36
39
|
emptyStateTitle={__('No OVAL Policies found')}
|
|
40
|
+
permissions={['view_oval_policies']}
|
|
41
|
+
confirmDeleteTitle={__('Delete OVAL Policy')}
|
|
42
|
+
submitDelete={submitDelete}
|
|
43
|
+
prepareMutation={prepareMutation(
|
|
44
|
+
props.history,
|
|
45
|
+
props.showToast,
|
|
46
|
+
policiesQuery,
|
|
47
|
+
'deleteOvalPolicy',
|
|
48
|
+
__('OVAL policy was successfully deleted.'),
|
|
49
|
+
deleteOvalPolicyMutation,
|
|
50
|
+
__('OVAL policy')
|
|
51
|
+
)}
|
|
37
52
|
/>
|
|
38
53
|
</IndexLayout>
|
|
39
54
|
);
|
|
@@ -41,6 +56,7 @@ const OvalPoliciesIndex = props => {
|
|
|
41
56
|
|
|
42
57
|
OvalPoliciesIndex.propTypes = {
|
|
43
58
|
history: PropTypes.object.isRequired,
|
|
59
|
+
showToast: PropTypes.func.isRequired,
|
|
44
60
|
};
|
|
45
61
|
|
|
46
62
|
export default OvalPoliciesIndex;
|