foreman_openscap 4.3.3 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
  3. data/app/graphql/mutations/oval_contents/delete.rb +9 -0
  4. data/app/graphql/mutations/oval_policies/create.rb +33 -0
  5. data/app/graphql/mutations/oval_policies/delete.rb +9 -0
  6. data/app/graphql/mutations/oval_policies/update.rb +15 -0
  7. data/app/graphql/types/oval_check.rb +11 -0
  8. data/app/graphql/types/oval_content.rb +2 -0
  9. data/app/graphql/types/oval_policy.rb +3 -0
  10. data/app/helpers/arf_report_dashboard_helper.rb +2 -4
  11. data/app/helpers/compliance_hosts_helper.rb +1 -1
  12. data/app/helpers/policies_helper.rb +2 -2
  13. data/app/models/concerns/foreman_openscap/data_stream_content.rb +1 -1
  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/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/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 +8 -9
  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/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  74. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  75. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  76. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  77. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +98 -77
  78. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  79. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  80. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  81. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  82. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  83. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  84. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  85. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  86. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  87. data/{locale/de/foreman_openscap.edit.po → webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js} +0 -0
  88. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  89. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  90. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +34 -4
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  92. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  93. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  94. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  95. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  96. data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
  97. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
  98. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
  101. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
  102. data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
  103. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  104. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
  105. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +87 -0
  106. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  107. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  108. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
  109. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +80 -2
  110. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  111. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +202 -0
  112. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +50 -4
  113. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  114. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  115. data/webpack/routes/routes.js +21 -0
  116. data/webpack/testHelper.js +64 -2
  117. metadata +76 -35
  118. data/locale/en_GB/foreman_openscap.edit.po +0 -0
  119. data/locale/es/foreman_openscap.edit.po +0 -0
  120. data/locale/fr/foreman_openscap.edit.po +0 -0
  121. data/locale/gl/foreman_openscap.edit.po +0 -0
  122. data/locale/it/foreman_openscap.edit.po +0 -0
  123. data/locale/ja/foreman_openscap.edit.po +0 -0
  124. data/locale/ko/foreman_openscap.edit.po +0 -0
  125. data/locale/pt_BR/foreman_openscap.edit.po +0 -0
  126. data/locale/ru/foreman_openscap.edit.po +0 -0
  127. data/locale/sv_SE/foreman_openscap.edit.po +0 -0
  128. data/locale/zh_CN/foreman_openscap.edit.po +0 -0
  129. data/locale/zh_TW/foreman_openscap.edit.po +0 -0
@@ -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
  );