foreman_openscap 4.3.2 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,12 +1,19 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { Button } from '@patternfly/react-core';
4
+
3
5
  import { translate as __ } from 'foremanReact/common/I18n';
4
6
 
5
7
  import IndexTable from '../../../components/IndexTable';
6
8
  import withLoading from '../../../components/withLoading';
9
+ import withDeleteModal from '../../../components/withDeleteModal';
7
10
 
8
11
  import { linkCell } from '../../../helpers/tableHelper';
9
- import { ovalPoliciesPath, modelPath } from '../../../helpers/pathsHelper';
12
+ import {
13
+ ovalPoliciesPath,
14
+ modelPath,
15
+ ovalPoliciesNewPath,
16
+ } from '../../../helpers/pathsHelper';
10
17
 
11
18
  const OvalPoliciesTable = props => {
12
19
  const columns = [{ title: __('Name') }, { title: __('OVAL Content') }];
@@ -19,17 +26,39 @@ const OvalPoliciesTable = props => {
19
26
  policy,
20
27
  }));
21
28
 
22
- const actions = [];
29
+ const actionResolver = (rowData, rest) => {
30
+ const actions = [];
31
+ if (rowData.policy.meta.canDestroy) {
32
+ actions.push({
33
+ title: __('Delete OVAL Policy'),
34
+ onClick: (event, rowId, rData, extra) => {
35
+ props.toggleModal(rData.policy);
36
+ },
37
+ });
38
+ }
39
+ return actions;
40
+ };
41
+
42
+ const createBtn = (
43
+ <Button
44
+ onClick={() => props.history.push(ovalPoliciesNewPath)}
45
+ variant="primary"
46
+ aria-label="create_oval_policy"
47
+ >
48
+ {__('Create OVAL Policy')}
49
+ </Button>
50
+ );
23
51
 
24
52
  return (
25
53
  <IndexTable
26
54
  columns={columns}
27
55
  rows={rows}
28
- actions={actions}
56
+ actionResolver={actionResolver}
29
57
  pagination={props.pagination}
30
58
  totalCount={props.totalCount}
31
59
  history={props.history}
32
60
  ariaTableLabel={__('OVAL Policies Table')}
61
+ toolbarBtns={createBtn}
33
62
  />
34
63
  );
35
64
  };
@@ -39,6 +68,7 @@ OvalPoliciesTable.propTypes = {
39
68
  pagination: PropTypes.object.isRequired,
40
69
  totalCount: PropTypes.number.isRequired,
41
70
  history: PropTypes.object.isRequired,
71
+ toggleModal: PropTypes.func.isRequired,
42
72
  };
43
73
 
44
- export default withLoading(OvalPoliciesTable);
74
+ export default withLoading(withDeleteModal(OvalPoliciesTable));
@@ -0,0 +1,101 @@
1
+ import policiesQuery from '../../../../graphql/queries/ovalPolicies.gql';
2
+ import deleteOvalPolicy from '../../../../graphql/mutations/deleteOvalPolicy.gql';
3
+
4
+ import { admin } from '../../../../testHelper';
5
+
6
+ export const firstCall = {
7
+ data: {
8
+ ovalPolicies: {
9
+ totalCount: 5,
10
+ nodes: [
11
+ {
12
+ __typename: 'ForemanOpenscap::OvalPolicy',
13
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQz',
14
+ name: 'first policy',
15
+ meta: { canDestroy: true },
16
+ ovalContent: { name: 'foo' },
17
+ },
18
+ {
19
+ __typename: 'ForemanOpenscap::OvalPolicy',
20
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQ0',
21
+ name: 'second policy',
22
+ meta: { canDestroy: true },
23
+ ovalContent: { name: 'foo' },
24
+ },
25
+ ],
26
+ },
27
+ currentUser: admin,
28
+ },
29
+ };
30
+
31
+ export const secondCall = {
32
+ data: {
33
+ ovalPolicies: {
34
+ totalCount: 4,
35
+ nodes: [
36
+ {
37
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQ0',
38
+ name: 'second policy',
39
+ meta: { canDestroy: true },
40
+ ovalContent: { name: 'foo' },
41
+ },
42
+ {
43
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQ1',
44
+ name: 'third policy',
45
+ meta: { canDestroy: true },
46
+ ovalContent: { name: 'foo' },
47
+ },
48
+ ],
49
+ },
50
+ currentUser: admin,
51
+ },
52
+ };
53
+
54
+ export const deleteMockFactory = (first, second, errors = null) => {
55
+ let called = false;
56
+
57
+ const deleteMocks = [
58
+ {
59
+ request: {
60
+ query: deleteOvalPolicy,
61
+ variables: {
62
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQz',
63
+ },
64
+ },
65
+ result: {
66
+ data: {
67
+ deleteOvalPolicy: {
68
+ __typename: 'ForemanOpenscap::OvalPolicy',
69
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQz',
70
+ errors,
71
+ },
72
+ },
73
+ },
74
+ },
75
+ {
76
+ request: {
77
+ query: policiesQuery,
78
+ variables: {
79
+ first: 2,
80
+ last: 2,
81
+ },
82
+ },
83
+ newData: () => {
84
+ if (called && !errors) {
85
+ return second;
86
+ } else if (called && errors) {
87
+ return first;
88
+ }
89
+ called = true;
90
+ return first;
91
+ },
92
+ },
93
+ ];
94
+ return deleteMocks;
95
+ };
96
+
97
+ export const pageParamsHistoryMock = {
98
+ location: {
99
+ search: '?page=1&perPage=2',
100
+ },
101
+ };
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import OvalPoliciesIndex from '../OvalPoliciesIndex';
7
+ import {
8
+ withRouter,
9
+ withRedux,
10
+ withMockedProvider,
11
+ tick,
12
+ historyMock,
13
+ } from '../../../../testHelper';
14
+ import { mocks, noDeleteMocks } from './OvalPoliciesIndex.fixtures';
15
+ import {
16
+ firstCall,
17
+ secondCall,
18
+ deleteMockFactory,
19
+ pageParamsHistoryMock,
20
+ } from './OvalPoliciesDestroy.fixtures';
21
+
22
+ const TestComponent = withRouter(
23
+ withRedux(withMockedProvider(OvalPoliciesIndex))
24
+ );
25
+
26
+ describe('OvalPoliciesIndex', () => {
27
+ it('should open and close delete modal', async () => {
28
+ render(
29
+ <TestComponent
30
+ history={historyMock}
31
+ mocks={mocks}
32
+ showToast={jest.fn()}
33
+ />
34
+ );
35
+ await waitFor(tick);
36
+ expect(screen.getByText('first policy')).toBeInTheDocument();
37
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
38
+ userEvent.click(screen.getByText('Delete OVAL Policy'));
39
+ await waitFor(tick);
40
+ expect(
41
+ screen.getByText('Are you sure you want to delete first policy?')
42
+ ).toBeInTheDocument();
43
+ userEvent.click(screen.getByText('Cancel'));
44
+ await waitFor(tick);
45
+ expect(
46
+ screen.queryByText('Are you sure you want to delete first policy?')
47
+ ).not.toBeInTheDocument();
48
+ expect(screen.getByText('first policy')).toBeInTheDocument();
49
+ });
50
+ it('should delete OVAL policy', async () => {
51
+ const showToast = jest.fn();
52
+ render(
53
+ <TestComponent
54
+ history={pageParamsHistoryMock}
55
+ mocks={deleteMockFactory(firstCall, secondCall)}
56
+ showToast={showToast}
57
+ />
58
+ );
59
+ await waitFor(tick);
60
+ expect(screen.getByText('first policy')).toBeInTheDocument();
61
+ expect(screen.queryByText('third policy')).not.toBeInTheDocument();
62
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
63
+ userEvent.click(screen.getByText('Delete OVAL Policy'));
64
+ await waitFor(tick);
65
+ userEvent.click(screen.getByText('Confirm'));
66
+ await waitFor(tick);
67
+ expect(showToast).toHaveBeenCalledWith({
68
+ type: 'success',
69
+ message: 'OVAL policy was successfully deleted.',
70
+ });
71
+ await waitFor(tick);
72
+ expect(screen.queryByText('first policy')).not.toBeInTheDocument();
73
+ expect(screen.getByText('third policy')).toBeInTheDocument();
74
+ });
75
+ it('should show error when deleting OVAL policy fails', async () => {
76
+ const showToast = jest.fn();
77
+ render(
78
+ <TestComponent
79
+ history={pageParamsHistoryMock}
80
+ mocks={deleteMockFactory(firstCall, secondCall, [
81
+ { message: 'cannot do it', path: 'base' },
82
+ { message: 'will not do it', path: 'base' },
83
+ ])}
84
+ showToast={showToast}
85
+ />
86
+ );
87
+ await waitFor(tick);
88
+ expect(screen.getByText('first policy')).toBeInTheDocument();
89
+ expect(screen.queryByText('third policy')).not.toBeInTheDocument();
90
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
91
+ userEvent.click(screen.getByText('Delete OVAL Policy'));
92
+ await waitFor(tick);
93
+ userEvent.click(screen.getByText('Confirm'));
94
+ await waitFor(tick);
95
+ expect(showToast).toHaveBeenCalledWith({
96
+ type: 'error',
97
+ message:
98
+ 'There was a following error when deleting OVAL policy: cannot do it, will not do it',
99
+ });
100
+ expect(screen.getByText('first policy')).toBeInTheDocument();
101
+ expect(screen.queryByText('third policy')).not.toBeInTheDocument();
102
+ });
103
+ it('should not show delete button when user does not have delete permissions', async () => {
104
+ render(
105
+ <TestComponent
106
+ history={historyMock}
107
+ mocks={noDeleteMocks}
108
+ showToast={jest.fn()}
109
+ />
110
+ );
111
+ await waitFor(tick);
112
+ expect(screen.getByText('first policy')).toBeInTheDocument();
113
+ expect(
114
+ screen.queryByRole('button', { name: 'Actions' })
115
+ ).not.toBeInTheDocument();
116
+ });
117
+ });
@@ -1,6 +1,11 @@
1
1
  import policiesQuery from '../../../../graphql/queries/ovalPolicies.gql';
2
2
  import { ovalPoliciesPath } from '../../../../helpers/pathsHelper';
3
- import { mockFactory } from '../../../../testHelper';
3
+ import {
4
+ mockFactory,
5
+ admin,
6
+ intruder,
7
+ userFactory,
8
+ } from '../../../../testHelper';
4
9
 
5
10
  const policiesMockFactory = mockFactory('ovalPolicies', policiesQuery);
6
11
 
@@ -14,23 +19,37 @@ export const pageParamsHistoryMock = {
14
19
  push: pushMock,
15
20
  };
16
21
 
22
+ const viewer = userFactory('viewer', [
23
+ {
24
+ __typename: 'Permission',
25
+ id: 'MDE6UGVybWlzc2lvbi0yOTY=',
26
+ name: 'view_oval_policies',
27
+ },
28
+ ]);
29
+
30
+ const firstPolicy = (meta = { canDestroy: true }) => ({
31
+ __typename: 'ForemanOpenscap::OvalPolicy',
32
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTE=',
33
+ name: 'first policy',
34
+ meta,
35
+ ovalContent: { name: 'first content' },
36
+ });
37
+ const secondPolicy = (meta = { canDestroy: true }) => ({
38
+ __typename: 'ForemanOpenscap::OvalPolicy',
39
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQw',
40
+ name: 'second policy',
41
+ meta,
42
+ ovalContent: { name: 'second content' },
43
+ });
44
+ const policiesData = {
45
+ totalCount: 2,
46
+ nodes: [firstPolicy(), secondPolicy()],
47
+ };
48
+
17
49
  export const mocks = policiesMockFactory(
18
50
  { first: 20, last: 20 },
19
- {
20
- totalCount: 2,
21
- nodes: [
22
- {
23
- id: 'abc',
24
- name: 'first policy',
25
- ovalContent: { name: 'first content' },
26
- },
27
- {
28
- id: 'xyz',
29
- name: 'second policy',
30
- ovalContent: { name: 'second content' },
31
- },
32
- ],
33
- }
51
+ policiesData,
52
+ { currentUser: admin }
34
53
  );
35
54
  export const pageParamsMocks = policiesMockFactory(
36
55
  { first: 10, last: 5 },
@@ -38,24 +57,55 @@ export const pageParamsMocks = policiesMockFactory(
38
57
  totalCount: 7,
39
58
  nodes: [
40
59
  {
41
- id: 'xyz',
60
+ __typename: 'ForemanOpenscap::OvalPolicy',
61
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQx',
42
62
  name: 'sixth policy',
63
+ meta: { canDestroy: true },
43
64
  ovalContent: { name: 'sixth content' },
44
65
  },
45
66
  {
46
- id: 'abc',
67
+ __typename: 'ForemanOpenscap::OvalPolicy',
68
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTQy',
47
69
  name: 'seventh policy',
70
+ meta: { canDestroy: true },
48
71
  ovalContent: { name: 'seventh content' },
49
72
  },
50
73
  ],
51
- }
74
+ },
75
+ { currentUser: admin }
52
76
  );
77
+
53
78
  export const emptyMocks = policiesMockFactory(
54
79
  { first: 20, last: 20 },
55
- { totalCount: 0, nodes: [] }
80
+ { totalCount: 0, nodes: [] },
81
+ { currentUser: admin }
56
82
  );
57
83
  export const errorMocks = policiesMockFactory(
58
84
  { first: 20, last: 20 },
59
85
  { totalCount: 0, nodes: [] },
60
- [{ message: 'Something very bad happened.' }]
86
+ {
87
+ errors: [{ message: 'Something very bad happened.', path: 'base' }],
88
+ currentUser: admin,
89
+ }
90
+ );
91
+ export const viewerMocks = policiesMockFactory(
92
+ { first: 20, last: 20 },
93
+ policiesData,
94
+ { currentUser: viewer }
95
+ );
96
+ export const unauthorizedMocks = policiesMockFactory(
97
+ { first: 20, last: 20 },
98
+ policiesData,
99
+ { currentUser: intruder }
100
+ );
101
+ export const noDeleteMocks = policiesMockFactory(
102
+ { first: 20, last: 20 },
103
+ {
104
+ totalCount: 2,
105
+ nodes: [
106
+ firstPolicy({ canDestroy: false }),
107
+ secondPolicy({ canDestroy: false }),
108
+ ],
109
+ },
110
+ { currentUser: admin }
61
111
  );
@@ -7,6 +7,7 @@ import '@testing-library/jest-dom';
7
7
  import {
8
8
  withMockedProvider,
9
9
  withRouter,
10
+ withRedux,
10
11
  tick,
11
12
  historyMock,
12
13
  } from '../../../../testHelper';
@@ -18,12 +19,16 @@ import {
18
19
  pageParamsHistoryMock,
19
20
  emptyMocks,
20
21
  errorMocks,
22
+ viewerMocks,
23
+ unauthorizedMocks,
21
24
  } from './OvalPoliciesIndex.fixtures';
22
25
 
23
- import OvalPoliciesIndex from '../OvalPoliciesIndex';
26
+ import OvalPoliciesIndex from '../index';
24
27
  import { ovalPoliciesPath } from '../../../../helpers/pathsHelper';
25
28
 
26
- const TestComponent = withRouter(withMockedProvider(OvalPoliciesIndex));
29
+ const TestComponent = withRouter(
30
+ withRedux(withMockedProvider(OvalPoliciesIndex))
31
+ );
27
32
 
28
33
  describe('OvalPoliciesIndex', () => {
29
34
  it('should load page', async () => {
@@ -40,6 +45,15 @@ describe('OvalPoliciesIndex', () => {
40
45
  expect(within(pageItems).getByText(/1 - 2/)).toBeInTheDocument();
41
46
  expect(within(pageItems).getByText('of')).toBeInTheDocument();
42
47
  expect(within(pageItems).getByText('2')).toBeInTheDocument();
48
+
49
+ expect(screen.getByText('first policy').closest('a')).toHaveAttribute(
50
+ 'href',
51
+ '/experimental/compliance/oval_policies/1'
52
+ );
53
+ expect(screen.getByText('second policy').closest('a')).toHaveAttribute(
54
+ 'href',
55
+ '/experimental/compliance/oval_policies/40'
56
+ );
43
57
  });
44
58
  it('should load page with page params', async () => {
45
59
  const { container } = render(
@@ -75,4 +89,22 @@ describe('OvalPoliciesIndex', () => {
75
89
  ).toBeInTheDocument();
76
90
  expect(screen.getByText('Error!')).toBeInTheDocument();
77
91
  });
92
+ it('should load page for user with permissions', async () => {
93
+ render(<TestComponent history={historyMock} mocks={viewerMocks} />);
94
+ await waitFor(tick);
95
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
96
+ expect(screen.getByText('first policy')).toBeInTheDocument();
97
+ });
98
+ it('should not load page for user without permissions', async () => {
99
+ render(<TestComponent history={historyMock} mocks={unauthorizedMocks} />);
100
+ await waitFor(tick);
101
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
102
+ expect(screen.queryByText('first policy')).not.toBeInTheDocument();
103
+ expect(
104
+ screen.getByText(
105
+ 'You are not authorized to view the page. Request the following permissions from administrator: view_oval_policies.'
106
+ )
107
+ ).toBeInTheDocument();
108
+ expect(screen.getByText('Permission denied')).toBeInTheDocument();
109
+ });
78
110
  });
@@ -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 OvalPoliciesIndex from './OvalPoliciesIndex';
4
6
 
5
- const WrappedOvalPoliciesIndex = props => <OvalPoliciesIndex {...props} />;
7
+ const WrappedOvalPoliciesIndex = props => {
8
+ const dispatch = useDispatch();
9
+
10
+ return <OvalPoliciesIndex {...props} showToast={showToast(dispatch)} />;
11
+ };
6
12
 
7
13
  export default WrappedOvalPoliciesIndex;
@@ -0,0 +1,135 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useLazyQuery } from '@apollo/client';
4
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
5
+ import {
6
+ Select,
7
+ SelectOption,
8
+ SelectVariant,
9
+ FormGroup,
10
+ } from '@patternfly/react-core';
11
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
12
+ import hostgroupsQuery from '../../../graphql/queries/hostgroups.gql';
13
+
14
+ const HostgroupSelect = ({
15
+ selected,
16
+ setSelected,
17
+ hgsError,
18
+ showError,
19
+ setShowError,
20
+ }) => {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+
23
+ const [typingTimeout, setTypingTimeout] = useState(null);
24
+
25
+ const [fetchHostgroups, { loading, data, error }] = useLazyQuery(
26
+ hostgroupsQuery
27
+ );
28
+ const results = data?.hostgroups?.nodes ? data.hostgroups.nodes : [];
29
+
30
+ const onSelect = (event, selection) => {
31
+ if (selected.find(item => item.name === selection)) {
32
+ setSelected(selected.filter(item => item.name !== selection));
33
+ } else {
34
+ const hg = results.find(item => item.name === selection);
35
+ setSelected([...selected, hg]);
36
+ }
37
+ };
38
+
39
+ const onClear = () => {
40
+ if (showError) {
41
+ setShowError(false);
42
+ }
43
+ setSelected([]);
44
+ };
45
+
46
+ const onInputChange = value => {
47
+ if (showError) {
48
+ setShowError(false);
49
+ }
50
+ if (typingTimeout) {
51
+ clearTimeout(typingTimeout);
52
+ }
53
+ const variables = { search: `name ~ ${value}` };
54
+ setTypingTimeout(setTimeout(() => fetchHostgroups({ variables }), 500));
55
+ };
56
+
57
+ const shouldValidate = (err, shouldShowError) => {
58
+ if (shouldShowError) {
59
+ return err ? 'error' : 'success';
60
+ }
61
+ return 'noval';
62
+ };
63
+
64
+ const prepareOptions = fetchedResults => {
65
+ if (loading) {
66
+ return [
67
+ <SelectOption isDisabled key={0}>
68
+ {__('Loading...')}
69
+ </SelectOption>,
70
+ ];
71
+ }
72
+
73
+ if (error) {
74
+ return [
75
+ <SelectOption isDisabled key={0}>
76
+ {sprintf('Failed to fetch hostgroups, cause: %s', error.message)}
77
+ </SelectOption>,
78
+ ];
79
+ }
80
+
81
+ if (fetchedResults.length > 20) {
82
+ return [
83
+ <SelectOption isDisabled key={0}>
84
+ {sprintf(
85
+ 'You have %s hostgroups to display. Please refine your search.',
86
+ fetchedResults.length
87
+ )}
88
+ </SelectOption>,
89
+ ];
90
+ }
91
+
92
+ return fetchedResults.map((hg, idx) => (
93
+ <SelectOption key={hg.id} value={hg.name} />
94
+ ));
95
+ };
96
+
97
+ return (
98
+ <FormGroup
99
+ label={__('Hostgroups')}
100
+ helperTextInvalidIcon={<ExclamationCircleIcon />}
101
+ helperTextInvalid={showError && hgsError}
102
+ validated={shouldValidate(hgsError, showError)}
103
+ >
104
+ <Select
105
+ variant={SelectVariant.typeaheadMulti}
106
+ typeAheadAriaLabel="Select a hostgroup"
107
+ placeholderText="Type a hostroup name..."
108
+ onToggle={() => setIsOpen(!isOpen)}
109
+ onSelect={onSelect}
110
+ onClear={onClear}
111
+ selections={selected.map(item => item.name)}
112
+ isOpen={isOpen}
113
+ onTypeaheadInputChanged={onInputChange}
114
+ validated={shouldValidate(hgsError, showError)}
115
+ >
116
+ {prepareOptions(results)}
117
+ </Select>
118
+ </FormGroup>
119
+ );
120
+ };
121
+
122
+ HostgroupSelect.propTypes = {
123
+ selected: PropTypes.array,
124
+ setSelected: PropTypes.func.isRequired,
125
+ hgsError: PropTypes.string,
126
+ showError: PropTypes.bool.isRequired,
127
+ setShowError: PropTypes.func.isRequired,
128
+ };
129
+
130
+ HostgroupSelect.defaultProps = {
131
+ selected: [],
132
+ hgsError: '',
133
+ };
134
+
135
+ export default HostgroupSelect;