foreman_openscap 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/graphql/mutations/oval_contents/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/update.rb +15 -0
- data/app/graphql/types/oval_check.rb +11 -0
- data/app/graphql/types/oval_content.rb +2 -0
- data/app/graphql/types/oval_policy.rb +3 -0
- data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
- data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
- data/app/models/foreman_openscap/oval_content.rb +2 -0
- data/app/services/foreman_openscap/oval/configure.rb +1 -1
- data/app/services/foreman_openscap/oval/setup.rb +5 -5
- data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
- data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
- data/lib/foreman_openscap/engine.rb +6 -1
- data/lib/foreman_openscap/version.rb +1 -1
- data/package.json +3 -6
- data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
- data/test/graphql/queries/oval_content_query_test.rb +29 -0
- data/test/unit/services/hostgroup_overrider_test.rb +1 -1
- data/test/unit/services/oval/setup_check_test.rb +37 -0
- data/webpack/components/ConfirmModal.js +63 -0
- data/webpack/components/ConfirmModal.scss +3 -0
- data/webpack/components/EditableInput.js +157 -0
- data/webpack/components/EditableInput.scss +3 -0
- data/webpack/components/EmptyState.js +4 -1
- data/webpack/components/IndexLayout.js +11 -4
- data/webpack/components/IndexTable/index.js +17 -17
- data/webpack/components/LinkButton.js +26 -0
- data/webpack/components/withDeleteModal.js +51 -0
- data/webpack/components/withLoading.js +21 -3
- data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
- data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
- data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
- data/webpack/graphql/queries/hostgroups.gql +14 -0
- data/webpack/graphql/queries/ovalContent.gql +8 -0
- data/webpack/graphql/queries/ovalContents.gql +3 -0
- data/webpack/graphql/queries/ovalPolicies.gql +3 -0
- data/webpack/helpers/formFieldsHelper.js +63 -0
- data/webpack/helpers/mutationHelper.js +68 -0
- data/webpack/helpers/pathsHelper.js +5 -0
- data/webpack/helpers/toastHelper.js +3 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +25 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +41 -4
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +61 -59
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +29 -8
- data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
- data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
- data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
- data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +17 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +57 -41
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +14 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -11
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +28 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +47 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +3 -0
- data/webpack/routes/routes.js +14 -0
- data/webpack/testHelper.js +9 -1
- 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
|
-
|
|
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
|
-
|
|
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 '../
|
|
7
|
+
import OvalContentsIndex from '../';
|
|
8
8
|
|
|
9
|
-
import {
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =>
|
|
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,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
|
+
});
|