foreman_openscap 4.3.2 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
  3. data/app/controllers/api/v2/compliance/oval_policies_controller.rb +1 -1
  4. data/app/graphql/mutations/oval_contents/delete.rb +9 -0
  5. data/app/graphql/mutations/oval_policies/create.rb +33 -0
  6. data/app/graphql/mutations/oval_policies/delete.rb +9 -0
  7. data/app/graphql/mutations/oval_policies/update.rb +15 -0
  8. data/app/graphql/types/oval_check.rb +11 -0
  9. data/app/graphql/types/oval_content.rb +2 -0
  10. data/app/graphql/types/oval_policy.rb +3 -0
  11. data/app/helpers/arf_report_dashboard_helper.rb +2 -4
  12. data/app/helpers/compliance_hosts_helper.rb +1 -1
  13. data/app/helpers/policies_helper.rb +2 -2
  14. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  15. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +16 -0
  16. data/app/models/concerns/foreman_openscap/policy_common.rb +1 -1
  17. data/app/models/foreman_openscap/oval_content.rb +2 -0
  18. data/app/services/foreman_openscap/client_config/base.rb +1 -0
  19. data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
  20. data/app/services/foreman_openscap/oval/configure.rb +16 -13
  21. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  22. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  23. data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
  24. data/app/views/arf_reports/_metrics.html.erb +4 -4
  25. data/app/views/compliance_hosts/show.html.erb +4 -6
  26. data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
  27. data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
  28. data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
  29. data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
  30. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  31. data/lib/foreman_openscap/engine.rb +6 -7
  32. data/lib/foreman_openscap/version.rb +1 -1
  33. data/package.json +3 -6
  34. data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
  35. data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
  36. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  37. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  38. data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
  39. data/test/helpers/policy_dashboard_helper_test.rb +1 -1
  40. data/test/test_plugin_helper.rb +9 -4
  41. data/test/unit/policy_test.rb +1 -1
  42. data/test/unit/services/config_name_service_test.rb +1 -0
  43. data/test/unit/services/hostgroup_overrider_test.rb +2 -1
  44. data/test/unit/services/lookup_key_overrider_test.rb +4 -1
  45. data/test/unit/services/oval/setup_check_test.rb +37 -0
  46. data/webpack/components/ConfirmModal.js +63 -0
  47. data/webpack/components/ConfirmModal.scss +3 -0
  48. data/webpack/components/EditableInput.js +163 -0
  49. data/webpack/components/EditableInput.scss +3 -0
  50. data/webpack/components/EmptyState.js +12 -3
  51. data/webpack/components/IndexLayout.js +11 -4
  52. data/webpack/components/IndexTable/index.js +21 -16
  53. data/webpack/components/LinkButton.js +38 -0
  54. data/webpack/components/withDeleteModal.js +51 -0
  55. data/webpack/components/withLoading.js +44 -5
  56. data/webpack/graphql/mutations/createOvalPolicy.gql +22 -0
  57. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  58. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  59. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  60. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  61. data/webpack/graphql/queries/cves.gql +5 -0
  62. data/webpack/graphql/queries/hostgroups.gql +14 -0
  63. data/webpack/graphql/queries/ovalContent.gql +8 -0
  64. data/webpack/graphql/queries/ovalContents.gql +8 -0
  65. data/webpack/graphql/queries/ovalPolicies.gql +8 -0
  66. data/webpack/graphql/queries/ovalPolicy.gql +8 -0
  67. data/webpack/helpers/formFieldsHelper.js +113 -0
  68. data/webpack/helpers/globalIdHelper.js +4 -2
  69. data/webpack/helpers/mutationHelper.js +68 -0
  70. data/webpack/helpers/pathsHelper.js +10 -3
  71. data/webpack/helpers/permissionsHelper.js +42 -0
  72. data/webpack/helpers/toastHelper.js +3 -0
  73. data/webpack/helpers/toastsHelper.js +3 -0
  74. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  75. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  76. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  77. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  78. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +98 -77
  79. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  80. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  81. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  82. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  83. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  84. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  85. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  86. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  87. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  88. data/{locale/de/foreman_openscap.edit.po → webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js} +0 -0
  89. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  90. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +34 -4
  92. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  93. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  94. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  95. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  96. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  97. data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
  98. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
  101. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
  102. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
  103. data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
  104. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  105. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
  106. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +87 -0
  107. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  108. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  109. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
  110. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +80 -2
  111. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  112. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +202 -0
  113. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +50 -4
  114. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  115. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  116. data/webpack/routes/routes.js +21 -0
  117. data/webpack/testHelper.js +64 -2
  118. metadata +80 -37
  119. data/locale/en_GB/foreman_openscap.edit.po +0 -0
  120. data/locale/es/foreman_openscap.edit.po +0 -0
  121. data/locale/fr/foreman_openscap.edit.po +0 -0
  122. data/locale/gl/foreman_openscap.edit.po +0 -0
  123. data/locale/it/foreman_openscap.edit.po +0 -0
  124. data/locale/ja/foreman_openscap.edit.po +0 -0
  125. data/locale/ko/foreman_openscap.edit.po +0 -0
  126. data/locale/pt_BR/foreman_openscap.edit.po +0 -0
  127. data/locale/ru/foreman_openscap.edit.po +0 -0
  128. data/locale/sv_SE/foreman_openscap.edit.po +0 -0
  129. data/locale/zh_CN/foreman_openscap.edit.po +0 -0
  130. data/locale/zh_TW/foreman_openscap.edit.po +0 -0
@@ -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 '../OvalContentsIndex';
7
+ import OvalContentsIndex from '../';
8
8
 
9
- import { withMockedProvider, tick, historyMock } from '../../../../testHelper';
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 = withMockedProvider(OvalContentsIndex);
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(<TestComponent history={historyMock} mocks={emptyMocks} />);
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(<TestComponent history={historyMock} mocks={errorMocks} />);
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 => <OvalContentsIndex {...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,3 @@
1
+ #scap-file-source-url, #scap-file-source-file {
2
+ margin: 0;
3
+ }
@@ -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
+ });
@@ -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;