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.
Files changed (119) 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/data_stream_content.rb +1 -1
  15. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  16. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +16 -0
  17. data/app/models/foreman_openscap/arf_report.rb +1 -1
  18. data/app/models/foreman_openscap/oval_content.rb +2 -0
  19. data/app/services/foreman_openscap/client_config/base.rb +1 -0
  20. data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
  21. data/app/services/foreman_openscap/oval/configure.rb +16 -13
  22. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  23. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  24. data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
  25. data/app/views/arf_reports/_metrics.html.erb +4 -4
  26. data/app/views/compliance_hosts/show.html.erb +4 -6
  27. data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
  28. data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
  29. data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
  30. data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
  31. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  32. data/lib/foreman_openscap/engine.rb +8 -9
  33. data/lib/foreman_openscap/version.rb +1 -1
  34. data/package.json +3 -6
  35. data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
  36. data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
  37. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  38. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  39. data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
  40. data/test/helpers/policy_dashboard_helper_test.rb +1 -1
  41. data/test/test_plugin_helper.rb +9 -4
  42. data/test/unit/policy_test.rb +1 -1
  43. data/test/unit/services/config_name_service_test.rb +1 -0
  44. data/test/unit/services/hostgroup_overrider_test.rb +2 -1
  45. data/test/unit/services/lookup_key_overrider_test.rb +4 -1
  46. data/test/unit/services/oval/setup_check_test.rb +37 -0
  47. data/webpack/components/ConfirmModal.js +63 -0
  48. data/webpack/components/ConfirmModal.scss +3 -0
  49. data/webpack/components/EditableInput.js +163 -0
  50. data/webpack/components/EditableInput.scss +3 -0
  51. data/webpack/components/EmptyState.js +12 -3
  52. data/webpack/components/IndexLayout.js +11 -4
  53. data/webpack/components/IndexTable/index.js +21 -16
  54. data/webpack/components/LinkButton.js +38 -0
  55. data/webpack/components/withDeleteModal.js +51 -0
  56. data/webpack/components/withLoading.js +44 -5
  57. data/webpack/graphql/mutations/createOvalPolicy.gql +22 -0
  58. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  59. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  60. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  61. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  62. data/webpack/graphql/queries/cves.gql +5 -0
  63. data/webpack/graphql/queries/hostgroups.gql +14 -0
  64. data/webpack/graphql/queries/ovalContent.gql +8 -0
  65. data/webpack/graphql/queries/ovalContents.gql +8 -0
  66. data/webpack/graphql/queries/ovalPolicies.gql +8 -0
  67. data/webpack/graphql/queries/ovalPolicy.gql +8 -0
  68. data/webpack/helpers/formFieldsHelper.js +113 -0
  69. data/webpack/helpers/globalIdHelper.js +4 -2
  70. data/webpack/helpers/mutationHelper.js +68 -0
  71. data/webpack/helpers/pathsHelper.js +10 -3
  72. data/webpack/helpers/permissionsHelper.js +42 -0
  73. data/webpack/helpers/toastHelper.js +3 -0
  74. data/webpack/helpers/toastsHelper.js +3 -0
  75. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  76. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  77. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  78. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  79. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +98 -77
  80. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  81. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  82. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  83. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  84. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  85. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  86. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  87. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  88. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  89. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
  90. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  92. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +34 -4
  93. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  94. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  95. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  96. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  97. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  98. data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
  101. data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
  102. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
  103. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
  104. data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
  105. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  106. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
  107. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +87 -0
  108. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  109. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  110. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
  111. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +80 -2
  112. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  113. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +202 -0
  114. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +50 -4
  115. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  116. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  117. data/webpack/routes/routes.js +21 -0
  118. data/webpack/testHelper.js +64 -2
  119. metadata +63 -7
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useMutation } from '@apollo/client';
4
+
5
+ import {
6
+ TextList,
7
+ TextContent,
8
+ TextArea,
9
+ TextListItem,
10
+ TextListVariants,
11
+ TextListItemVariants,
12
+ TextInput,
13
+ } from '@patternfly/react-core';
14
+
15
+ import { translate as __ } from 'foremanReact/common/I18n';
16
+
17
+ import EditableInput from '../../../components/EditableInput';
18
+
19
+ import { onAttrUpdate, policySchedule } from './OvalPoliciesShowHelper';
20
+ import updateOvalPolicyMutation from '../../../graphql/mutations/updateOvalPolicy.gql';
21
+
22
+ const DetailsTab = props => {
23
+ const { policy, showToast } = props;
24
+
25
+ const [callMutation] = useMutation(updateOvalPolicyMutation);
26
+
27
+ return (
28
+ <TextContent className="pf-u-pt-md">
29
+ <TextList component={TextListVariants.dl}>
30
+ <TextListItem component={TextListItemVariants.dt}>
31
+ {__('Name')}
32
+ </TextListItem>
33
+ <TextListItem
34
+ aria-label="label text value"
35
+ component={TextListItemVariants.dd}
36
+ className="foreman-spaced-list"
37
+ >
38
+ <EditableInput
39
+ value={policy.name}
40
+ onConfirm={onAttrUpdate('name', policy, callMutation, showToast)}
41
+ component={TextInput}
42
+ attrName="name"
43
+ allowed={policy.meta.canEdit}
44
+ />
45
+ </TextListItem>
46
+ <TextListItem component={TextListItemVariants.dt}>
47
+ {__('Period')}
48
+ </TextListItem>
49
+ <TextListItem
50
+ aria-label="label text value"
51
+ component={TextListItemVariants.dd}
52
+ className="foreman-spaced-list"
53
+ >
54
+ {policySchedule(policy)}
55
+ </TextListItem>
56
+ <TextListItem component={TextListItemVariants.dt}>
57
+ {__('Description')}
58
+ </TextListItem>
59
+ <TextListItem
60
+ aria-label="label text value"
61
+ component={TextListItemVariants.dd}
62
+ className="foreman-spaced-list"
63
+ >
64
+ <EditableInput
65
+ value={policy.description}
66
+ onConfirm={onAttrUpdate(
67
+ 'description',
68
+ policy,
69
+ callMutation,
70
+ showToast
71
+ )}
72
+ component={TextArea}
73
+ attrName="description"
74
+ allowed={policy.meta.canEdit}
75
+ />
76
+ </TextListItem>
77
+ </TextList>
78
+ </TextContent>
79
+ );
80
+ };
81
+
82
+ DetailsTab.propTypes = {
83
+ policy: PropTypes.object.isRequired,
84
+ showToast: PropTypes.func.isRequired,
85
+ };
86
+
87
+ export default DetailsTab;
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import { useQuery } from '@apollo/client';
6
+
7
+ import HostgroupsTable from './HostgroupsTable';
8
+
9
+ import hostgroups from '../../../graphql/queries/hostgroups.gql';
10
+ import {
11
+ useParamsToVars,
12
+ useCurrentPagination,
13
+ } from '../../../helpers/pageParamsHelper';
14
+
15
+ const HostgroupsTab = props => {
16
+ const useFetchFn = componentProps =>
17
+ useQuery(hostgroups, {
18
+ variables: {
19
+ search: `oval_policy_id = ${componentProps.match.params.id}`,
20
+ ...useParamsToVars(componentProps.history),
21
+ },
22
+ });
23
+
24
+ const renameData = data => ({
25
+ hostgroups: data.hostgroups.nodes,
26
+ totalCount: data.hostgroups.totalCount,
27
+ });
28
+
29
+ const pagination = useCurrentPagination(props.history);
30
+
31
+ return (
32
+ <HostgroupsTable
33
+ {...props}
34
+ fetchFn={useFetchFn}
35
+ renameData={renameData}
36
+ resultPath="hostgroups.nodes"
37
+ pagination={pagination}
38
+ emptyStateTitle={__('No Hostgroups found.')}
39
+ permissions={['view_hostgroups']}
40
+ />
41
+ );
42
+ };
43
+
44
+ HostgroupsTab.propTypes = {
45
+ match: PropTypes.object.isRequired,
46
+ history: PropTypes.object.isRequired,
47
+ };
48
+
49
+ export default HostgroupsTab;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import withLoading from '../../../components/withLoading';
6
+ import IndexTable from '../../../components/IndexTable';
7
+
8
+ const CvesTable = props => {
9
+ const columns = [{ title: __('Name') }];
10
+
11
+ const rows = props.hostgroups.map(hostgroup => ({
12
+ cells: [{ title: hostgroup.name }],
13
+ hostgroup,
14
+ }));
15
+
16
+ const actions = [];
17
+
18
+ return (
19
+ <IndexTable
20
+ columns={columns}
21
+ rows={rows}
22
+ actions={actions}
23
+ pagination={props.pagination}
24
+ totalCount={props.totalCount}
25
+ history={props.history}
26
+ ariaTableLabel={__('Table of hostgroups for OVAL policy')}
27
+ />
28
+ );
29
+ };
30
+
31
+ CvesTable.propTypes = {
32
+ hostgroups: PropTypes.array.isRequired,
33
+ pagination: PropTypes.object.isRequired,
34
+ totalCount: PropTypes.number.isRequired,
35
+ history: PropTypes.object.isRequired,
36
+ };
37
+
38
+ export default withLoading(CvesTable);
@@ -7,7 +7,6 @@ import {
7
7
  Button,
8
8
  Grid,
9
9
  GridItem,
10
- TextContent,
11
10
  Text,
12
11
  TextVariants,
13
12
  Tabs,
@@ -16,10 +15,11 @@ import {
16
15
  } from '@patternfly/react-core';
17
16
 
18
17
  import withLoading from '../../../components/withLoading';
19
-
20
18
  import CvesTab from './CvesTab';
19
+ import HostgroupsTab from './HostgroupsTab';
20
+ import DetailsTab from './DetailsTab';
21
21
 
22
- import { policySchedule, newJobFormPath } from './OvalPoliciesShowHelper';
22
+ import { newJobFormPath } from './OvalPoliciesShowHelper';
23
23
  import { resolvePath } from '../../../helpers/pathsHelper';
24
24
 
25
25
  const OvalPoliciesShow = props => {
@@ -50,18 +50,22 @@ const OvalPoliciesShow = props => {
50
50
  <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabSelect}>
51
51
  <Tab
52
52
  eventKey="details"
53
- title={<TabTitleText>Details</TabTitleText>}
53
+ title={<TabTitleText>{__('Details')}</TabTitleText>}
54
54
  >
55
- <TextContent className="pf-u-pt-md">
56
- <Text component={TextVariants.h3}>Period</Text>
57
- <Text component={TextVariants.p}>{policySchedule(policy)}</Text>
58
- <Text component={TextVariants.h3}>Description</Text>
59
- <Text component={TextVariants.p}>{policy.description}</Text>
60
- </TextContent>
55
+ <DetailsTab {...props} />
61
56
  </Tab>
62
- <Tab eventKey="cves" title={<TabTitleText>CVEs</TabTitleText>}>
57
+ <Tab
58
+ eventKey="cves"
59
+ title={<TabTitleText>{__('CVEs')}</TabTitleText>}
60
+ >
63
61
  <CvesTab {...props} />
64
62
  </Tab>
63
+ <Tab
64
+ eventKey="hostgroups"
65
+ title={<TabTitleText>{__('Hostgroups')}</TabTitleText>}
66
+ >
67
+ <HostgroupsTab {...props} />
68
+ </Tab>
65
69
  </Tabs>
66
70
  </GridItem>
67
71
  </Grid>
@@ -1,4 +1,5 @@
1
- import { decodeId } from '../../../helpers/globalIdHelper';
1
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
+ import { decodeModelId } from '../../../helpers/globalIdHelper';
2
3
  import { addSearch } from '../../../helpers/pageParamsHelper';
3
4
  import { newJobPath } from '../../../helpers/pathsHelper';
4
5
 
@@ -17,7 +18,9 @@ export const policySchedule = policy => {
17
18
 
18
19
  const targetingScopedSearchQuery = policy => {
19
20
  const hgIds = policy.hostgroups.nodes.reduce((memo, hg) => {
20
- const ids = [decodeId(hg)].concat(hg.descendants.nodes.map(decodeId));
21
+ const ids = [decodeModelId(hg)].concat(
22
+ hg.descendants.nodes.map(decodeModelId)
23
+ );
21
24
  return ids.reduce(
22
25
  (acc, id) => (acc.includes(id) ? acc : [...acc, id]),
23
26
  memo
@@ -37,3 +40,78 @@ export const newJobFormPath = (policy, policyId) =>
37
40
  host_ids: targetingScopedSearchQuery(policy),
38
41
  'inputs[oval_policies]': policyId,
39
42
  });
43
+
44
+ const policyToAttrs = (policy, attrs) =>
45
+ Object.entries(policy).reduce((memo, [key, value]) => {
46
+ if (attrs.includes(key)) {
47
+ memo[key] = value;
48
+ }
49
+ return memo;
50
+ }, {});
51
+
52
+ const onUpdateSuccess = (
53
+ closeEditable,
54
+ stopSubmitting,
55
+ showToast,
56
+ attr,
57
+ onValidationError
58
+ ) => result => {
59
+ const { errors } = result.data.updateOvalPolicy;
60
+ if (Array.isArray(errors) && errors.length > 0) {
61
+ stopSubmitting();
62
+ if (
63
+ errors.length === 1 &&
64
+ errors[0].path.join(' ') === `attributes ${attr}`
65
+ ) {
66
+ onValidationError(errors[0].message);
67
+ } else {
68
+ showToast({
69
+ type: 'error',
70
+ message: formatError(joinErrors(errors)),
71
+ });
72
+ }
73
+ } else {
74
+ closeEditable();
75
+ showToast({
76
+ type: 'success',
77
+ message: __('OVAL policy was successfully updated.'),
78
+ });
79
+ }
80
+ };
81
+
82
+ const formatError = error =>
83
+ sprintf(
84
+ __('There was a following error when updating OVAL policy: %s'),
85
+ error
86
+ );
87
+
88
+ const joinErrors = errors => errors.map(err => err.message).join(', ');
89
+
90
+ const onUpdateError = (showToast, stopSubmitting) => error => {
91
+ stopSubmitting();
92
+ showToast({ type: 'error', message: formatError(error.message) });
93
+ };
94
+
95
+ export const onAttrUpdate = (attr, policy, callMutation, showToast) => (
96
+ newValue,
97
+ closeEditable,
98
+ stopSubmitting,
99
+ onValidationError
100
+ ) => {
101
+ const vars = policyToAttrs(policy, ['id', 'name', 'description', 'cronLine']);
102
+ vars[attr] = newValue;
103
+ return (
104
+ callMutation({ variables: vars })
105
+ // eslint-disable-next-line promise/prefer-await-to-then
106
+ .then(
107
+ onUpdateSuccess(
108
+ closeEditable,
109
+ stopSubmitting,
110
+ showToast,
111
+ attr,
112
+ onValidationError
113
+ )
114
+ )
115
+ .catch(onUpdateError(showToast, stopSubmitting))
116
+ );
117
+ };
@@ -0,0 +1,48 @@
1
+ import { mockFactory } from '../../../../testHelper';
2
+ import updateOvalPolicyMutation from '../../../../graphql/mutations/updateOvalPolicy.gql';
3
+ import { ovalPolicy } from './OvalPoliciesShow.fixtures';
4
+
5
+ const updateOvalPolicyMockFactory = mockFactory(
6
+ 'updateOvalPolicy',
7
+ updateOvalPolicyMutation
8
+ );
9
+
10
+ export const updatedName = 'updated policy name';
11
+
12
+ const variables = {
13
+ id: ovalPolicy.id,
14
+ name: updatedName,
15
+ cronLine: ovalPolicy.cronLine,
16
+ description: ovalPolicy.description,
17
+ };
18
+ const responsePolicy = {
19
+ ovalPolicy: {
20
+ __typename: 'ForemanOpenscap::OvalPolicy',
21
+ id: ovalPolicy.id,
22
+ name: updatedName,
23
+ description: ovalPolicy.description,
24
+ cronLine: ovalPolicy.cronLine,
25
+ },
26
+ errors: [],
27
+ };
28
+
29
+ export const policyUpdateMock = updateOvalPolicyMockFactory(
30
+ variables,
31
+ responsePolicy
32
+ );
33
+
34
+ export const policyUpdateErrorMock = updateOvalPolicyMockFactory(
35
+ variables,
36
+ responsePolicy,
37
+ { errors: [{ message: 'This is an unexpected failure.' }] }
38
+ );
39
+
40
+ export const policyUpdateValidationMock = updateOvalPolicyMockFactory(
41
+ variables,
42
+ {
43
+ ovalPolicy,
44
+ errors: [
45
+ { path: ['attributes', 'name'], message: 'has already been taken' },
46
+ ],
47
+ }
48
+ );
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ import OvalPoliciesShow from '../';
8
+ import {
9
+ historyMock,
10
+ ovalPolicyId,
11
+ policyDetailMock,
12
+ policyEditPermissionsMock,
13
+ ovalPolicy,
14
+ } from './OvalPoliciesShow.fixtures';
15
+ import {
16
+ policyUpdateMock,
17
+ policyUpdateErrorMock,
18
+ policyUpdateValidationMock,
19
+ updatedName,
20
+ } from './OvalPoliciesEdit.fixtures';
21
+ import { ovalPoliciesShowPath } from '../../../../helpers/pathsHelper';
22
+
23
+ import {
24
+ withMockedProvider,
25
+ tick,
26
+ withRouter,
27
+ withRedux,
28
+ } from '../../../../testHelper';
29
+
30
+ import * as toasts from '../../../../helpers/toastHelper';
31
+
32
+ const TestComponent = withRouter(
33
+ withRedux(withMockedProvider(OvalPoliciesShow))
34
+ );
35
+
36
+ describe('OvalPoliciesShow', () => {
37
+ it('should open and close inline edit for name', async () => {
38
+ render(
39
+ <TestComponent
40
+ history={historyMock}
41
+ match={{
42
+ params: { id: ovalPolicyId, tab: 'details' },
43
+ path: ovalPoliciesShowPath,
44
+ }}
45
+ mocks={policyDetailMock}
46
+ />
47
+ );
48
+ await waitFor(tick);
49
+ userEvent.click(screen.getByRole('button', { name: 'edit name' }));
50
+ userEvent.clear(screen.getByLabelText(/name text input/));
51
+ userEvent.type(screen.getByLabelText(/name text input/), 'foo');
52
+ expect(screen.getByLabelText(/name text input/)).toHaveAttribute(
53
+ 'value',
54
+ 'foo'
55
+ );
56
+ userEvent.click(
57
+ screen.getByRole('button', { name: 'cancel editing name' })
58
+ );
59
+ expect(screen.queryByText('foo')).not.toBeInTheDocument();
60
+ });
61
+ it('should update policy name', async () => {
62
+ const showToast = jest.fn();
63
+ jest.spyOn(toasts, 'showToast').mockImplementation(() => showToast);
64
+
65
+ const { container } = render(
66
+ <TestComponent
67
+ history={historyMock}
68
+ match={{
69
+ params: { id: ovalPolicyId, tab: 'details' },
70
+ path: ovalPoliciesShowPath,
71
+ }}
72
+ mocks={policyDetailMock.concat(policyUpdateMock)}
73
+ />
74
+ );
75
+ await waitFor(tick);
76
+ const editBtn = screen.getByRole('button', { name: 'edit name' });
77
+ expect(editBtn).toBeInTheDocument();
78
+ expect(
79
+ screen.queryByRole('button', { name: 'submit name' })
80
+ ).not.toBeInTheDocument();
81
+
82
+ userEvent.click(editBtn);
83
+ expect(
84
+ screen.queryByRole('button', { name: 'edit name' })
85
+ ).not.toBeInTheDocument();
86
+ const inputField = screen.getByLabelText(/name text input/);
87
+ const submitBtn = screen.getByRole('button', { name: 'submit name' });
88
+ const cancelBtn = screen.getByRole('button', {
89
+ name: 'cancel editing name',
90
+ });
91
+
92
+ userEvent.clear(inputField);
93
+ userEvent.type(inputField, updatedName);
94
+ userEvent.click(submitBtn);
95
+ expect(inputField).toBeDisabled();
96
+ expect(submitBtn).toBeDisabled();
97
+ expect(cancelBtn).toBeDisabled();
98
+ const spinner = container.querySelector('#edit-name-spinner');
99
+ expect(spinner).toBeInTheDocument();
100
+ await waitFor(tick);
101
+ expect(showToast).toHaveBeenCalledWith({
102
+ type: 'success',
103
+ message: 'OVAL policy was successfully updated.',
104
+ });
105
+
106
+ expect(inputField).not.toBeInTheDocument();
107
+ expect(editBtn).toBeInTheDocument();
108
+ expect(cancelBtn).not.toBeInTheDocument();
109
+ expect(
110
+ screen.queryByRole('button', { name: 'submit name' })
111
+ ).not.toBeInTheDocument();
112
+ await waitFor(tick);
113
+ expect(screen.getAllByText(updatedName).pop()).toBeInTheDocument();
114
+ });
115
+ it('should show unexpected errors', async () => {
116
+ const showToast = jest.fn();
117
+ jest.spyOn(toasts, 'showToast').mockImplementation(() => showToast);
118
+
119
+ render(
120
+ <TestComponent
121
+ history={historyMock}
122
+ match={{
123
+ params: { id: ovalPolicyId, tab: 'details' },
124
+ path: ovalPoliciesShowPath,
125
+ }}
126
+ mocks={policyDetailMock.concat(policyUpdateErrorMock)}
127
+ />
128
+ );
129
+ await waitFor(tick);
130
+ const editBtn = screen.getByRole('button', { name: 'edit name' });
131
+ userEvent.click(editBtn);
132
+ const inputField = screen.getByLabelText(/name text input/);
133
+ userEvent.clear(inputField);
134
+ userEvent.type(inputField, updatedName);
135
+ userEvent.click(screen.getByRole('button', { name: 'submit name' }));
136
+ await waitFor(tick);
137
+ expect(showToast).toHaveBeenCalledWith({
138
+ type: 'error',
139
+ message:
140
+ 'There was a following error when updating OVAL policy: This is an unexpected failure.',
141
+ });
142
+ expect(inputField).toBeInTheDocument();
143
+ expect(inputField).not.toBeDisabled();
144
+ expect(screen.getByText(ovalPolicy.name)).toBeInTheDocument();
145
+ });
146
+ it('should show validation errors', async () => {
147
+ const showToast = jest.fn();
148
+ jest.spyOn(toasts, 'showToast').mockImplementation(() => showToast);
149
+
150
+ const { container } = render(
151
+ <TestComponent
152
+ history={historyMock}
153
+ match={{
154
+ params: { id: ovalPolicyId, tab: 'details' },
155
+ path: ovalPoliciesShowPath,
156
+ }}
157
+ mocks={policyDetailMock.concat(policyUpdateValidationMock)}
158
+ />
159
+ );
160
+ await waitFor(tick);
161
+ const editBtn = screen.getByRole('button', { name: 'edit name' });
162
+ userEvent.click(editBtn);
163
+ const inputField = screen.getByLabelText(/name text input/);
164
+ userEvent.clear(inputField);
165
+ userEvent.type(inputField, updatedName);
166
+ userEvent.click(screen.getByRole('button', { name: 'submit name' }));
167
+ await waitFor(tick);
168
+ expect(inputField).toBeInTheDocument();
169
+ expect(inputField).not.toBeDisabled();
170
+ expect(
171
+ container.querySelector('#edit-name-spinner')
172
+ ).not.toBeInTheDocument();
173
+ expect(screen.getByText(ovalPolicy.name)).toBeInTheDocument();
174
+ expect(screen.getByText('has already been taken')).toBeInTheDocument();
175
+ userEvent.click(
176
+ screen.getByRole('button', { name: 'cancel editing name' })
177
+ );
178
+ userEvent.click(editBtn);
179
+ expect(
180
+ screen.queryByText('has already been taken')
181
+ ).not.toBeInTheDocument();
182
+ });
183
+ it('should not show edit btns when user is not allowed to edit', async () => {
184
+ render(
185
+ <TestComponent
186
+ history={historyMock}
187
+ match={{
188
+ params: { id: ovalPolicyId, tab: 'details' },
189
+ path: ovalPoliciesShowPath,
190
+ }}
191
+ mocks={policyEditPermissionsMock}
192
+ />
193
+ );
194
+ await waitFor(tick);
195
+ expect(
196
+ screen.queryByRole('button', { name: 'edit name' })
197
+ ).not.toBeInTheDocument();
198
+ expect(
199
+ screen.queryByRole('button', { name: 'edit description' })
200
+ ).not.toBeInTheDocument();
201
+ });
202
+ });
@@ -1,11 +1,14 @@
1
- import { mockFactory } from '../../../../testHelper';
1
+ import { mockFactory, admin, intruder, viewer } from '../../../../testHelper';
2
2
  import ovalPolicyQuery from '../../../../graphql/queries/ovalPolicy.gql';
3
3
  import cvesQuery from '../../../../graphql/queries/cves.gql';
4
+ import hostgroupsQuery from '../../../../graphql/queries/hostgroups.gql';
4
5
 
5
6
  const policyDetailMockFactory = mockFactory('ovalPolicy', ovalPolicyQuery);
6
7
  const cvesMockFactory = mockFactory('cves', cvesQuery);
8
+ const hostgroupsMockFactory = mockFactory('hostgroups', hostgroupsQuery);
7
9
 
8
- const ovalPolicy = {
10
+ export const ovalPolicy = {
11
+ __typename: 'ForemanOpenscap::OvalPolicy',
9
12
  id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTM=',
10
13
  name: 'Third policy',
11
14
  period: 'weekly',
@@ -13,6 +16,9 @@ const ovalPolicy = {
13
16
  weekday: 'tuesday',
14
17
  dayOfMonth: null,
15
18
  description: 'A very strict policy',
19
+ meta: {
20
+ canEdit: true,
21
+ },
16
22
  hostgroups: {
17
23
  nodes: [
18
24
  {
@@ -30,6 +36,8 @@ const ovalPolicy = {
30
36
  },
31
37
  };
32
38
 
39
+ const noEditPolicy = { ...ovalPolicy, meta: { canEdit: false } };
40
+
33
41
  const cvesResult = {
34
42
  totalCount: 1,
35
43
  nodes: [
@@ -51,6 +59,20 @@ const cvesResult = {
51
59
  ],
52
60
  };
53
61
 
62
+ const hostgroupsResult = {
63
+ totalCount: 2,
64
+ nodes: [
65
+ {
66
+ id: 'MDE6SG9zdGdyb3VwLTQ=',
67
+ name: 'first hostgroup',
68
+ },
69
+ {
70
+ id: 'MDE6SG9zdGdyb3VwLTEy',
71
+ name: 'second hostgroup',
72
+ },
73
+ ],
74
+ };
75
+
54
76
  export const ovalPolicyId = 3;
55
77
 
56
78
  export const pushMock = jest.fn();
@@ -70,9 +92,33 @@ export const historyWithSearch = {
70
92
 
71
93
  export const policyDetailMock = policyDetailMockFactory(
72
94
  { id: ovalPolicy.id },
73
- ovalPolicy
95
+ ovalPolicy,
96
+ { currentUser: admin }
74
97
  );
98
+
99
+ export const policyUnauthorizedMock = policyDetailMockFactory(
100
+ { id: ovalPolicy.id },
101
+ ovalPolicy,
102
+ { currentUser: intruder }
103
+ );
104
+
75
105
  export const policyCvesMock = cvesMockFactory(
76
106
  { search: `oval_policy_id = ${ovalPolicyId}`, first: 5, last: 5 },
77
- cvesResult
107
+ cvesResult,
108
+ { currentUser: admin }
109
+ );
110
+ export const policyHostgroupsMock = hostgroupsMockFactory(
111
+ { search: `oval_policy_id = ${ovalPolicyId}`, first: 5, last: 5 },
112
+ hostgroupsResult,
113
+ { currentUser: admin }
114
+ );
115
+ export const policyHostgroupsDeniedMock = hostgroupsMockFactory(
116
+ { search: `oval_policy_id = ${ovalPolicyId}`, first: 5, last: 5 },
117
+ { totalCount: 0, nodes: [] },
118
+ { currentUser: intruder }
119
+ );
120
+ export const policyEditPermissionsMock = policyDetailMockFactory(
121
+ { id: ovalPolicy.id },
122
+ noEditPolicy,
123
+ { currentUser: viewer }
78
124
  );