foreman_openscap 5.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/app/graphql/mutations/oval_contents/delete.rb +9 -0
  3. data/app/graphql/mutations/oval_policies/delete.rb +9 -0
  4. data/app/graphql/mutations/oval_policies/update.rb +15 -0
  5. data/app/graphql/types/oval_check.rb +11 -0
  6. data/app/graphql/types/oval_content.rb +2 -0
  7. data/app/graphql/types/oval_policy.rb +3 -0
  8. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  9. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
  10. data/app/models/foreman_openscap/oval_content.rb +2 -0
  11. data/app/services/foreman_openscap/oval/configure.rb +1 -1
  12. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  13. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  14. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  15. data/lib/foreman_openscap/engine.rb +6 -1
  16. data/lib/foreman_openscap/version.rb +1 -1
  17. data/package.json +3 -6
  18. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  19. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  20. data/test/unit/services/hostgroup_overrider_test.rb +1 -1
  21. data/test/unit/services/oval/setup_check_test.rb +37 -0
  22. data/webpack/components/ConfirmModal.js +63 -0
  23. data/webpack/components/ConfirmModal.scss +3 -0
  24. data/webpack/components/EditableInput.js +157 -0
  25. data/webpack/components/EditableInput.scss +3 -0
  26. data/webpack/components/EmptyState.js +4 -1
  27. data/webpack/components/IndexLayout.js +11 -4
  28. data/webpack/components/IndexTable/index.js +17 -17
  29. data/webpack/components/LinkButton.js +26 -0
  30. data/webpack/components/withDeleteModal.js +51 -0
  31. data/webpack/components/withLoading.js +21 -3
  32. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  33. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  34. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  35. data/webpack/graphql/queries/hostgroups.gql +14 -0
  36. data/webpack/graphql/queries/ovalContent.gql +8 -0
  37. data/webpack/graphql/queries/ovalContents.gql +3 -0
  38. data/webpack/graphql/queries/ovalPolicies.gql +3 -0
  39. data/webpack/helpers/formFieldsHelper.js +63 -0
  40. data/webpack/helpers/mutationHelper.js +68 -0
  41. data/webpack/helpers/pathsHelper.js +5 -0
  42. data/webpack/helpers/toastHelper.js +3 -0
  43. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +25 -0
  44. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +41 -4
  45. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  46. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  47. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +61 -59
  48. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +29 -8
  49. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  50. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  51. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  52. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  53. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  54. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  55. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  56. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  57. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
  58. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  59. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +17 -2
  60. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
  61. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  62. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  63. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +57 -41
  64. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +14 -2
  65. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  66. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
  67. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  68. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  69. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
  70. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
  71. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  72. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
  73. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +28 -1
  74. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +47 -4
  75. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +3 -0
  76. data/webpack/routes/routes.js +14 -0
  77. data/webpack/testHelper.js +9 -1
  78. metadata +46 -3
@@ -0,0 +1,105 @@
1
+ import { admin } from '../../../../testHelper';
2
+
3
+ import ovalContentsQuery from '../../../../graphql/queries/ovalContents.gql';
4
+ import deleteOvalContent from '../../../../graphql/mutations/deleteOvalContent.gql';
5
+
6
+ export const firstCall = {
7
+ data: {
8
+ ovalContents: {
9
+ totalCount: 5,
10
+ nodes: [
11
+ {
12
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC0z',
13
+ name: 'ansible OVAL content',
14
+ url:
15
+ 'http://oval-content-source/security/data/oval/ansible-2-including-unpatched.oval.xml.bz2',
16
+ originalFilename: '',
17
+ meta: { canDestroy: true },
18
+ },
19
+ {
20
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC00',
21
+ name: 'dotnet OVAL content',
22
+ url:
23
+ 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
24
+ originalFilename: '',
25
+ meta: { canDestroy: true },
26
+ },
27
+ ],
28
+ },
29
+ currentUser: admin,
30
+ },
31
+ };
32
+
33
+ export const secondCall = {
34
+ data: {
35
+ ovalContents: {
36
+ totalCount: 4,
37
+ nodes: [
38
+ {
39
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC00',
40
+ name: 'dotnet OVAL content',
41
+ url:
42
+ 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
43
+ originalFilename: '',
44
+ meta: { canDestroy: true },
45
+ },
46
+ {
47
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC03',
48
+ name: 'jboss OVAL content',
49
+ url: '',
50
+ originalFilename: 'jboss.oval.xml.bz2',
51
+ meta: { canDestroy: true },
52
+ },
53
+ ],
54
+ },
55
+ currentUser: admin,
56
+ },
57
+ };
58
+
59
+ export const deleteMockFactory = (first, second, errors = null) => {
60
+ let called = false;
61
+
62
+ const deleteMocks = [
63
+ {
64
+ request: {
65
+ query: deleteOvalContent,
66
+ variables: {
67
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC0z',
68
+ },
69
+ },
70
+ result: {
71
+ data: {
72
+ deleteOvalContent: {
73
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsQ29udGVudC0z',
74
+ errors,
75
+ },
76
+ },
77
+ },
78
+ },
79
+ {
80
+ request: {
81
+ query: ovalContentsQuery,
82
+ variables: {
83
+ first: 2,
84
+ last: 2,
85
+ },
86
+ },
87
+ newData: () => {
88
+ if (called && !errors) {
89
+ return second;
90
+ } else if (called && errors) {
91
+ return first;
92
+ }
93
+ called = true;
94
+ return first;
95
+ },
96
+ },
97
+ ];
98
+ return deleteMocks;
99
+ };
100
+
101
+ export const pageParamsHistoryMock = {
102
+ location: {
103
+ search: '?page=1&perPage=2',
104
+ },
105
+ };
@@ -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
+ });
@@ -9,63 +9,6 @@ import {
9
9
 
10
10
  const ovalContentMockFactory = mockFactory('ovalContents', ovalContentsQuery);
11
11
 
12
- const ovalContents = {
13
- totalCount: 4,
14
- nodes: [
15
- {
16
- __typename: 'ForemanOpenscap::OvalContent',
17
- id: 'abc',
18
- name: 'ansible OVAL content',
19
- url:
20
- 'http://oval-content-source/security/data/oval/ansible-2-including-unpatched.oval.xml.bz2',
21
- originalFilename: '',
22
- },
23
- {
24
- __typename: 'ForemanOpenscap::OvalContent',
25
- id: 'bcd',
26
- name: 'dotnet OVAL content',
27
- url:
28
- 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
29
- originalFilename: '',
30
- },
31
- {
32
- __typename: 'ForemanOpenscap::OvalContent',
33
- id: 'cde',
34
- name: 'jboss OVAL content',
35
- url: '',
36
- originalFilename: 'jboss.oval.xml.bz2',
37
- },
38
- {
39
- __typename: 'ForemanOpenscap::OvalContent',
40
- id: 'def',
41
- name: 'openshift OVAL content',
42
- url: '',
43
- originalFilename: 'openshift.oval.xml.bz2',
44
- },
45
- ],
46
- };
47
-
48
- const paginatedOvalContents = {
49
- totalCount: 7,
50
- nodes: [
51
- {
52
- __typename: 'ForemanOpenscap::OvalContent',
53
- id: 'bcd',
54
- name: 'dotnet OVAL content',
55
- url:
56
- 'http://oval-content-source/security/data/oval/dotnet-2.2.oval.xml.bz2',
57
- originalFilename: '',
58
- },
59
- {
60
- __typename: 'ForemanOpenscap::OvalContent',
61
- id: 'def',
62
- name: 'openshift OVAL content',
63
- url: '',
64
- originalFilename: 'openshift.oval.xml.bz2',
65
- },
66
- ],
67
- };
68
-
69
12
  const viewer = userFactory('viewer', [
70
13
  {
71
14
  __typename: 'Permission',
@@ -74,15 +17,62 @@ const viewer = userFactory('viewer', [
74
17
  },
75
18
  ]);
76
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(),
58
+ ];
59
+ const ovalContents = {
60
+ totalCount: ovalContentNodes.length,
61
+ nodes: ovalContentNodes,
62
+ };
63
+
77
64
  export const mocks = ovalContentMockFactory(
78
65
  { first: 20, last: 20 },
79
- ovalContents,
66
+ {
67
+ totalCount: 4,
68
+ nodes: [firstContent(), secondContent(), thirdContent(), fourthContent()],
69
+ },
80
70
  { currentUser: admin }
81
71
  );
82
72
 
83
73
  export const paginatedMocks = ovalContentMockFactory(
84
74
  { first: 10, last: 5 },
85
- paginatedOvalContents,
75
+ { totalCount: 7, nodes: [secondContent(), fourthContent()] },
86
76
  { currentUser: admin }
87
77
  );
88
78
 
@@ -109,6 +99,18 @@ export const unauthorizedMocks = ovalContentMockFactory(
109
99
  { currentUser: intruder }
110
100
  );
111
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 }
112
+ );
113
+
112
114
  export const pushMock = jest.fn();
113
115
 
114
116
  export const pagePaginationHistoryMock = {
@@ -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 {
@@ -20,12 +26,14 @@ import {
20
26
  unauthorizedMocks,
21
27
  } from './OvalContentsIndex.fixtures';
22
28
 
23
- const TestComponent = withMockedProvider(OvalContentsIndex);
29
+ const TestComponent = withRedux(
30
+ withRouter(withMockedProvider(OvalContentsIndex))
31
+ );
24
32
 
25
33
  describe('OvalContentsIndex', () => {
26
34
  it('should load page', async () => {
27
35
  const { container } = render(
28
- <TestComponent history={historyMock} mocks={mocks} />
36
+ <TestComponent history={historyMock} mocks={mocks} location={{}} />
29
37
  );
30
38
  expect(screen.getByText('Loading')).toBeInTheDocument();
31
39
  await waitFor(tick);
@@ -47,6 +55,7 @@ describe('OvalContentsIndex', () => {
47
55
  const { container } = render(
48
56
  <TestComponent
49
57
  history={pagePaginationHistoryMock}
58
+ location={{}}
50
59
  mocks={paginatedMocks}
51
60
  />
52
61
  );
@@ -64,14 +73,18 @@ describe('OvalContentsIndex', () => {
64
73
  );
65
74
  });
66
75
  it('should show empty state', async () => {
67
- render(<TestComponent history={historyMock} mocks={emptyMocks} />);
76
+ render(
77
+ <TestComponent history={historyMock} mocks={emptyMocks} location={{}} />
78
+ );
68
79
  expect(screen.getByText('Loading')).toBeInTheDocument();
69
80
  await waitFor(tick);
70
81
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
71
82
  expect(screen.getByText('No OVAL Contents found.')).toBeInTheDocument();
72
83
  });
73
84
  it('should show errors', async () => {
74
- render(<TestComponent history={historyMock} mocks={errorMocks} />);
85
+ render(
86
+ <TestComponent history={historyMock} mocks={errorMocks} location={{}} />
87
+ );
75
88
  expect(screen.getByText('Loading')).toBeInTheDocument();
76
89
  await waitFor(tick);
77
90
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
@@ -81,13 +94,21 @@ describe('OvalContentsIndex', () => {
81
94
  expect(screen.getByText('Error!')).toBeInTheDocument();
82
95
  });
83
96
  it('should load page for user with permissions', async () => {
84
- render(<TestComponent history={historyMock} mocks={viewerMocks} />);
97
+ render(
98
+ <TestComponent history={historyMock} mocks={viewerMocks} location={{}} />
99
+ );
85
100
  await waitFor(tick);
86
101
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
87
102
  expect(screen.getByText('ansible OVAL content')).toBeInTheDocument();
88
103
  });
89
104
  it('should not load page for user without permissions', async () => {
90
- render(<TestComponent history={historyMock} mocks={unauthorizedMocks} />);
105
+ render(
106
+ <TestComponent
107
+ history={historyMock}
108
+ mocks={unauthorizedMocks}
109
+ location={{}}
110
+ />
111
+ );
91
112
  await waitFor(tick);
92
113
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
93
114
  expect(screen.queryByText('ansible OVAL content')).not.toBeInTheDocument();
@@ -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
+ });