foreman_openscap 4.3.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) 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/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 +1 -1
  13. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  14. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
  15. data/app/models/foreman_openscap/oval_content.rb +2 -0
  16. data/app/services/foreman_openscap/client_config/base.rb +1 -0
  17. data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
  18. data/app/services/foreman_openscap/oval/configure.rb +1 -1
  19. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  20. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  21. data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
  22. data/app/views/arf_reports/_metrics.html.erb +4 -4
  23. data/app/views/compliance_hosts/show.html.erb +4 -6
  24. data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
  25. data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
  26. data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
  27. data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
  28. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  29. data/lib/foreman_openscap/engine.rb +5 -7
  30. data/lib/foreman_openscap/version.rb +1 -1
  31. data/package.json +3 -6
  32. data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
  33. data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
  34. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  35. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  36. data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
  37. data/test/helpers/policy_dashboard_helper_test.rb +1 -1
  38. data/test/test_plugin_helper.rb +9 -4
  39. data/test/unit/policy_test.rb +1 -1
  40. data/test/unit/services/config_name_service_test.rb +1 -0
  41. data/test/unit/services/hostgroup_overrider_test.rb +2 -1
  42. data/test/unit/services/lookup_key_overrider_test.rb +4 -1
  43. data/test/unit/services/oval/setup_check_test.rb +37 -0
  44. data/webpack/components/ConfirmModal.js +63 -0
  45. data/webpack/components/ConfirmModal.scss +3 -0
  46. data/webpack/components/EditableInput.js +157 -0
  47. data/webpack/components/EditableInput.scss +3 -0
  48. data/webpack/components/EmptyState.js +12 -3
  49. data/webpack/components/IndexLayout.js +11 -4
  50. data/webpack/components/IndexTable/index.js +17 -18
  51. data/webpack/components/LinkButton.js +26 -0
  52. data/webpack/components/withDeleteModal.js +51 -0
  53. data/webpack/components/withLoading.js +41 -4
  54. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  55. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  56. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  57. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  58. data/webpack/graphql/queries/cves.gql +5 -0
  59. data/webpack/graphql/queries/hostgroups.gql +14 -0
  60. data/webpack/graphql/queries/ovalContent.gql +8 -0
  61. data/webpack/graphql/queries/ovalContents.gql +8 -0
  62. data/webpack/graphql/queries/ovalPolicies.gql +8 -0
  63. data/webpack/graphql/queries/ovalPolicy.gql +5 -0
  64. data/webpack/helpers/formFieldsHelper.js +63 -0
  65. data/webpack/helpers/mutationHelper.js +68 -0
  66. data/webpack/helpers/pathsHelper.js +5 -0
  67. data/webpack/helpers/permissionsHelper.js +42 -0
  68. data/webpack/helpers/toastHelper.js +3 -0
  69. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  70. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  71. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  72. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  73. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +93 -77
  74. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  75. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  76. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  77. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  78. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  79. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  80. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  81. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  82. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  83. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
  84. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  85. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  86. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
  87. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  88. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  89. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  90. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  92. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  93. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
  94. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  95. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  96. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -12
  97. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
  98. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +40 -4
  101. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  102. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  103. data/webpack/routes/routes.js +14 -0
  104. data/webpack/testHelper.js +42 -2
  105. metadata +53 -7
@@ -0,0 +1,124 @@
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 OvalContentsIndex from '../OvalContentsIndex';
7
+ import {
8
+ withRouter,
9
+ withRedux,
10
+ withMockedProvider,
11
+ tick,
12
+ historyMock,
13
+ } from '../../../../testHelper';
14
+ import { mocks, noDeleteMocks } from './OvalContentsIndex.fixtures';
15
+ import {
16
+ firstCall,
17
+ secondCall,
18
+ deleteMockFactory,
19
+ pageParamsHistoryMock,
20
+ } from './OvalContentsDestroy.fixtures';
21
+
22
+ const TestComponent = withRouter(
23
+ withRedux(withMockedProvider(OvalContentsIndex))
24
+ );
25
+
26
+ describe('OvalContentsIndex', () => {
27
+ it('should open and close delete modal', async () => {
28
+ render(
29
+ <TestComponent
30
+ history={historyMock}
31
+ location={{}}
32
+ mocks={mocks}
33
+ showToast={jest.fn()}
34
+ />
35
+ );
36
+ await waitFor(tick);
37
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
38
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
39
+ userEvent.click(screen.getByText('Delete OVAL Content'));
40
+ await waitFor(tick);
41
+ expect(
42
+ screen.getByText('Are you sure you want to delete ansible OVAL content?')
43
+ ).toBeInTheDocument();
44
+ userEvent.click(screen.getByText('Cancel'));
45
+ await waitFor(tick);
46
+ expect(
47
+ screen.queryByText(
48
+ 'Are you sure you want to delete ansible OVAL content?'
49
+ )
50
+ ).not.toBeInTheDocument();
51
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
52
+ });
53
+ it('should delete OVAL content', async () => {
54
+ const mocked = deleteMockFactory(firstCall, secondCall);
55
+ const showToast = jest.fn();
56
+ render(
57
+ <TestComponent
58
+ history={pageParamsHistoryMock}
59
+ location={{}}
60
+ mocks={mocked}
61
+ showToast={showToast}
62
+ />
63
+ );
64
+ await waitFor(tick);
65
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
66
+ expect(screen.queryByText('jboss OVAL content')).not.toBeInTheDocument();
67
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
68
+ userEvent.click(screen.getByText('Delete OVAL Content'));
69
+ await waitFor(tick);
70
+ userEvent.click(screen.getByText('Confirm'));
71
+ await waitFor(tick);
72
+ expect(showToast).toHaveBeenCalledWith({
73
+ type: 'success',
74
+ message: 'OVAL Content successfully deleted.',
75
+ });
76
+ await waitFor(tick);
77
+ expect(screen.queryByText('ansible OVAL content')).not.toBeInTheDocument();
78
+ expect(screen.getByText('jboss OVAL content')).toBeInTheDocument();
79
+ });
80
+ it('should show error when deleting OVAL content fails', async () => {
81
+ const showToast = jest.fn();
82
+ render(
83
+ <TestComponent
84
+ history={pageParamsHistoryMock}
85
+ location={{}}
86
+ mocks={deleteMockFactory(firstCall, secondCall, [
87
+ { message: 'is used by first policy', path: ['base'] },
88
+ { message: 'is used by second policy', path: ['base'] },
89
+ ])}
90
+ showToast={showToast}
91
+ />
92
+ );
93
+ await waitFor(tick);
94
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
95
+ expect(screen.queryByText('jboss OVAL content')).not.toBeInTheDocument();
96
+ userEvent.click(screen.getAllByRole('button', { name: 'Actions' })[0]);
97
+ userEvent.click(screen.getByText('Delete OVAL Content'));
98
+ await waitFor(tick);
99
+ userEvent.click(screen.getByText('Confirm'));
100
+ await waitFor(tick);
101
+ expect(showToast).toHaveBeenCalledWith({
102
+ type: 'error',
103
+ message:
104
+ 'There was a following error when deleting OVAL Content: is used by first policy, is used by second policy',
105
+ });
106
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
107
+ expect(screen.queryByText('jboss OVAL content')).not.toBeInTheDocument();
108
+ });
109
+ it('should not show delete button when user does not have delete permissions', async () => {
110
+ render(
111
+ <TestComponent
112
+ history={historyMock}
113
+ location={{}}
114
+ mocks={noDeleteMocks}
115
+ showToast={jest.fn()}
116
+ />
117
+ );
118
+ await waitFor(tick);
119
+ expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
120
+ expect(
121
+ screen.queryByRole('button', { name: 'Actions' })
122
+ ).not.toBeInTheDocument();
123
+ });
124
+ });
@@ -1,98 +1,114 @@
1
1
  import ovalContentsQuery from '../../../../graphql/queries/ovalContents.gql';
2
2
  import { ovalContentsPath } 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 ovalContentMockFactory = mockFactory('ovalContents', ovalContentsQuery);
6
11
 
7
- export const mocks = [
12
+ const viewer = userFactory('viewer', [
8
13
  {
9
- request: {
10
- query: ovalContentsQuery,
11
- variables: {
12
- first: 20,
13
- last: 20,
14
- },
15
- },
16
- result: {
17
- data: {
18
- ovalContents: {
19
- totalCount: 4,
20
- nodes: [
21
- {
22
- id: 'abc',
23
- name: 'ansible OVAL content',
24
- url:
25
- 'http://oval-content-source/security/data/oval/ansible-2-including-unpatched.oval.xml.bz2',
26
- originalFilename: '',
27
- },
28
- {
29
- id: 'bcd',
30
- name: 'dotnet OVAL content',
31
- url:
32
- 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
33
- originalFilename: '',
34
- },
35
- {
36
- id: 'cde',
37
- name: 'jboss OVAL content',
38
- url: '',
39
- originalFilename: 'jboss.oval.xml.bz2',
40
- },
41
- {
42
- id: 'def',
43
- name: 'openshift OVAL content',
44
- url: '',
45
- originalFilename: 'openshift.oval.xml.bz2',
46
- },
47
- ],
48
- },
49
- },
50
- },
14
+ __typename: 'Permission',
15
+ id: 'MDE6UGVybWlzc2lvbi0yOTY=',
16
+ name: 'view_oval_contents',
51
17
  },
18
+ ]);
19
+
20
+ const firstContent = (meta = { canDestroy: true }) => ({
21
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC0z',
22
+ name: 'ansible OVAL content',
23
+ url:
24
+ 'http://oval-content-source/security/data/oval/ansible-2-including-unpatched.oval.xml.bz2',
25
+ originalFilename: '',
26
+ meta,
27
+ });
28
+
29
+ const secondContent = (meta = { canDestroy: true }) => ({
30
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC00',
31
+ name: 'dotnet OVAL content',
32
+ url: 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
33
+ originalFilename: '',
34
+ meta,
35
+ });
36
+
37
+ const thirdContent = (meta = { canDestroy: true }) => ({
38
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC03',
39
+ name: 'jboss OVAL content',
40
+ url: '',
41
+ originalFilename: 'jboss.oval.xml.bz2',
42
+ meta,
43
+ });
44
+
45
+ const fourthContent = (meta = { canDestroy: true }) => ({
46
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC0zMw==',
47
+ name: 'openshift OVAL content',
48
+ url: '',
49
+ originalFilename: 'openshift.oval.xml.bz2',
50
+ meta,
51
+ });
52
+
53
+ const ovalContentNodes = [
54
+ firstContent(),
55
+ secondContent(),
56
+ thirdContent(),
57
+ fourthContent(),
52
58
  ];
59
+ const ovalContents = {
60
+ totalCount: ovalContentNodes.length,
61
+ nodes: ovalContentNodes,
62
+ };
53
63
 
54
- export const paginatedMocks = [
64
+ export const mocks = ovalContentMockFactory(
65
+ { first: 20, last: 20 },
55
66
  {
56
- request: {
57
- query: ovalContentsQuery,
58
- variables: {
59
- first: 10,
60
- last: 5,
61
- },
62
- },
63
- result: {
64
- data: {
65
- ovalContents: {
66
- totalCount: 7,
67
- nodes: [
68
- {
69
- id: 'bcd',
70
- name: 'dotnet OVAL content',
71
- url:
72
- 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
73
- originalFilename: '',
74
- },
75
- {
76
- id: 'def',
77
- name: 'openshift OVAL content',
78
- url: '',
79
- originalFilename: 'openshift.oval.xml.bz2',
80
- },
81
- ],
82
- },
83
- },
84
- },
67
+ totalCount: 4,
68
+ nodes: [firstContent(), secondContent(), thirdContent(), fourthContent()],
85
69
  },
86
- ];
70
+ { currentUser: admin }
71
+ );
72
+
73
+ export const paginatedMocks = ovalContentMockFactory(
74
+ { first: 10, last: 5 },
75
+ { totalCount: 7, nodes: [secondContent(), fourthContent()] },
76
+ { currentUser: admin }
77
+ );
87
78
 
88
79
  export const emptyMocks = ovalContentMockFactory(
89
80
  { first: 20, last: 20 },
90
- { totalCount: 0, nodes: [] }
81
+ { totalCount: 0, nodes: [] },
82
+ { currentUser: admin }
91
83
  );
92
84
  export const errorMocks = ovalContentMockFactory(
93
85
  { first: 20, last: 20 },
94
86
  { totalCount: 0, nodes: [] },
95
- [{ message: 'Something very bad happened.' }]
87
+ { errors: [{ message: 'Something very bad happened.' }], currentUser: admin }
88
+ );
89
+
90
+ export const viewerMocks = ovalContentMockFactory(
91
+ { first: 20, last: 20 },
92
+ ovalContents,
93
+ { currentUser: viewer }
94
+ );
95
+
96
+ export const unauthorizedMocks = ovalContentMockFactory(
97
+ { first: 20, last: 20 },
98
+ ovalContents,
99
+ { currentUser: intruder }
100
+ );
101
+
102
+ export const noDeleteMocks = ovalContentMockFactory(
103
+ { first: 20, last: 20 },
104
+ {
105
+ totalCount: 2,
106
+ nodes: [
107
+ firstContent({ canDestroy: false }),
108
+ secondContent({ canDestroy: false }),
109
+ ],
110
+ },
111
+ { currentUser: admin }
96
112
  );
97
113
 
98
114
  export const pushMock = jest.fn();
@@ -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
+ });