Skip to content

Commit 84ee4d3

Browse files
jeremylenzclaude
authored andcommitted
Display 'Analysis disabled' in CVE column (theforeman#1141)
* Display 'Analysis disabled' in CVE column * fix tests * Add test coverage for 'Analysis disabled' CVE column behavior Adds two test cases to CVECountCell.test.js: - Renders "Analysis disabled" when opt_out is true - Renders CVE count link when valid count is returned Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent bfa3fdd commit 84ee4d3

File tree

4 files changed

+138
-102
lines changed

4 files changed

+138
-102
lines changed

webpack/ForemanColumnExtensions/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const hostsIndexColumnExtensions = [
7474
columnName: 'cves_count',
7575
title: __('Total CVEs'),
7676
wrapper: hostDetails => <CVECountCell hostDetails={hostDetails} />,
77-
weight: 2600,
77+
weight: 1600,
7878
tableName: 'hosts',
7979
categoryName: insightsCategoryName,
8080
categoryKey: 'insights',

webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { UnknownIcon } from '@patternfly/react-icons';
3+
import { translate as __ } from 'foremanReact/common/I18n';
44
import { Link } from 'react-router-dom';
55
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
6+
import { propsToCamelCase } from 'foremanReact/common/helpers';
67
import { insightsCloudUrl } from '../InsightsCloudSync/InsightsCloudSyncHelpers';
78
import { useIopConfig } from '../common/Hooks/ConfigHooks';
89

@@ -24,24 +25,23 @@ export const CVECountCell = ({ hostDetails }) => {
2425
}
2526
);
2627

27-
if (!isIopEnabled) {
28-
return <UnknownIcon />;
28+
if (!isIopEnabled || uuid === undefined) {
29+
return '—';
2930
}
3031

31-
if (uuid === undefined) {
32-
return <UnknownIcon />;
33-
}
32+
const { cveCount, optOut } = propsToCamelCase(
33+
(response.response?.data || [])[0]?.attributes || {}
34+
);
3435

35-
// eslint-disable-next-line camelcase
36-
const { cve_count } = (response.response?.data || [])[0]?.attributes || {};
36+
if (optOut === true) {
37+
return __('Analysis disabled');
38+
}
3739

3840
const cveLink = (
39-
// eslint-disable-next-line camelcase
40-
<Link to={`hosts/${hostDetails.name}#/Vulnerabilities`}>{cve_count}</Link>
41+
<Link to={`hosts/${hostDetails.name}#/Vulnerabilities`}>{cveCount}</Link>
4142
);
4243

43-
// eslint-disable-next-line camelcase
44-
return cve_count === undefined ? <UnknownIcon /> : cveLink;
44+
return cveCount === undefined ? '—' : cveLink;
4545
};
4646

4747
CVECountCell.propTypes = {
Lines changed: 114 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
import React from 'react';
2-
import { screen } from '@testing-library/react';
3-
import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
4-
import { API } from 'foremanReact/redux/API';
2+
import { render, screen } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { Provider } from 'react-redux';
5+
import { ConnectedRouter } from 'connected-react-router';
6+
import { createMemoryHistory } from 'history';
7+
import configureMockStore from 'redux-mock-store';
8+
import { STATUS } from 'foremanReact/constants';
9+
import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
10+
import * as ConfigHooks from '../../common/Hooks/ConfigHooks';
511
import { CVECountCell } from '../CVECountCell';
612

7-
jest.mock('foremanReact/redux/API');
13+
jest.mock('foremanReact/common/hooks/API/APIHooks');
814
jest.mock('../../common/Hooks/ConfigHooks');
915

10-
const { renderWithStore } = rtlHelpers;
11-
12-
API.get.mockImplementation(async () => ({
13-
data: [
14-
{
15-
attributes: {
16-
cve_count: 1,
17-
},
16+
const mockStore = configureMockStore();
17+
const history = createMemoryHistory();
18+
const store = mockStore({
19+
router: {
20+
location: {
21+
pathname: '/',
22+
search: '',
23+
hash: '',
24+
state: null,
1825
},
19-
],
20-
}));
26+
action: 'POP',
27+
},
28+
});
2129

2230
const hostDetailsMock = {
2331
name: 'test-host.example.com',
@@ -26,104 +34,121 @@ const hostDetailsMock = {
2634
},
2735
};
2836

37+
const renderComponent = (props = {}) => {
38+
const allProps = {
39+
hostDetails: hostDetailsMock,
40+
...props,
41+
};
42+
43+
return render(
44+
<Provider store={store}>
45+
<ConnectedRouter history={history}>
46+
<CVECountCell {...allProps} />
47+
</ConnectedRouter>
48+
</Provider>
49+
);
50+
};
51+
2952
describe('CVECountCell', () => {
30-
afterEach(() => {
53+
beforeEach(() => {
54+
store.clearActions();
3155
jest.clearAllMocks();
3256
});
3357

3458
it('renders an empty cves count column when no subscription UUID', () => {
35-
const hostDetailsMockIoP = {
59+
ConfigHooks.useIopConfig.mockReturnValue(true);
60+
APIHooks.useAPI.mockReturnValue({
61+
status: STATUS.RESOLVED,
62+
response: null,
63+
});
64+
65+
const hostDetailsMockNoUuid = {
3666
name: 'test-host.example.com',
3767
subscription_facet_attributes: {
3868
uuid: null, // no subscription
3969
},
4070
};
41-
renderWithStore(<CVECountCell hostDetails={hostDetailsMockIoP} />, {
42-
router: {
43-
location: {
44-
pathname: '/',
45-
search: '',
46-
hash: '',
47-
query: {},
48-
},
49-
},
50-
API: {
51-
ADVISOR_ENGINE_CONFIG: {
52-
response: { use_iop_mode: true },
53-
status: 'RESOLVED',
54-
},
55-
},
71+
72+
renderComponent({ hostDetails: hostDetailsMockNoUuid });
73+
expect(screen.getByText('—')).toBeInTheDocument();
74+
});
75+
76+
it('renders — when IoP is not enabled', () => {
77+
ConfigHooks.useIopConfig.mockReturnValue(false);
78+
APIHooks.useAPI.mockReturnValue({
79+
status: STATUS.RESOLVED,
80+
response: null,
5681
});
57-
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();
82+
83+
renderComponent();
84+
expect(screen.getByText('—')).toBeInTheDocument();
5885
});
5986

60-
it('renders UnknownIcon when IoP is not enabled', () => {
61-
renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
62-
router: {
63-
location: {
64-
pathname: '/',
65-
search: '',
66-
hash: '',
67-
query: {},
68-
},
69-
},
70-
API: {
71-
ADVISOR_ENGINE_CONFIG: {
72-
response: { use_iop_mode: false },
73-
status: 'RESOLVED',
74-
},
75-
},
87+
it('renders — when IoP is enabled but CVE API call fails', () => {
88+
ConfigHooks.useIopConfig.mockReturnValue(true);
89+
APIHooks.useAPI.mockReturnValue({
90+
status: STATUS.ERROR,
91+
response: null,
7692
});
77-
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();
93+
94+
renderComponent();
95+
expect(screen.getByText('—')).toBeInTheDocument();
7896
});
7997

80-
it('renders UnknownIcon when IoP is enabled but CVE API call fails', () => {
81-
// Mock CVE API failure - override the global mock for this test
82-
API.get.mockImplementationOnce(async () => {
83-
throw new Error('CVE API call failed');
98+
it('renders — when IoP is undefined (API call pending)', () => {
99+
ConfigHooks.useIopConfig.mockReturnValue(undefined);
100+
APIHooks.useAPI.mockReturnValue({
101+
status: STATUS.PENDING,
102+
response: null,
84103
});
85104

86-
renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
87-
router: {
88-
location: {
89-
pathname: '/',
90-
search: '',
91-
hash: '',
92-
query: {},
93-
},
94-
},
95-
API: {
96-
ADVISOR_ENGINE_CONFIG: {
97-
response: { use_iop_mode: true },
98-
status: 'RESOLVED',
99-
},
100-
// Mock the API failure state for the CVE endpoint
101-
[`HOST_CVE_COUNT_${hostDetailsMock.subscription_facet_attributes.uuid}`]: {
102-
status: 'ERROR',
103-
error: 'CVE API call failed',
104-
},
105+
renderComponent();
106+
expect(screen.getByText('—')).toBeInTheDocument();
107+
});
108+
109+
it('renders "Analysis disabled" when opt_out is true', () => {
110+
ConfigHooks.useIopConfig.mockReturnValue(true);
111+
APIHooks.useAPI.mockReturnValue({
112+
status: STATUS.RESOLVED,
113+
response: {
114+
data: [
115+
{
116+
attributes: {
117+
opt_out: true,
118+
},
119+
},
120+
],
105121
},
106122
});
107-
// Should render UnknownIcon when CVE API fails
108-
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();
123+
124+
renderComponent();
125+
expect(screen.getByText('Analysis disabled')).toBeInTheDocument();
109126
});
110127

111-
it('renders UnknownIcon when IoP is undefined (API call pending)', () => {
112-
renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
113-
router: {
114-
location: {
115-
pathname: '/',
116-
search: '',
117-
hash: '',
118-
query: {},
119-
},
120-
},
121-
API: {
122-
ADVISOR_ENGINE_CONFIG: {
123-
status: 'PENDING',
124-
},
128+
it('renders CVE count link when valid count is returned', () => {
129+
ConfigHooks.useIopConfig.mockReturnValue(true);
130+
APIHooks.useAPI.mockReturnValue({
131+
status: STATUS.RESOLVED,
132+
response: {
133+
data: [
134+
{
135+
attributes: {
136+
cve_count: 1,
137+
},
138+
},
139+
],
125140
},
126141
});
127-
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();
142+
143+
renderComponent();
144+
145+
// Should render a link with the CVE count
146+
const link = screen.getByRole('link');
147+
expect(link).toBeInTheDocument();
148+
expect(link).toHaveTextContent('1');
149+
expect(link).toHaveAttribute(
150+
'href',
151+
`/hosts/${hostDetailsMock.name}#/Vulnerabilities`
152+
);
128153
});
129154
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
import camelCase from 'lodash/camelCase';
2+
13
export const getURIQuery = jest.fn(() => ({}));
24

35
export const noop = Function.prototype;
6+
7+
export const propsToCamelCase = ob => {
8+
if (typeof ob !== 'object' || ob === null) return ob;
9+
10+
return Object.keys(ob).reduce((memo, key) => {
11+
memo[camelCase(key)] = ob[key];
12+
return memo;
13+
}, {});
14+
};

0 commit comments

Comments
 (0)