Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion webpack/ForemanColumnExtensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const hostsIndexColumnExtensions = [
columnName: 'cves_count',
title: __('Total CVEs'),
wrapper: hostDetails => <CVECountCell hostDetails={hostDetails} />,
weight: 2600,
weight: 1600,
tableName: 'hosts',
categoryName: insightsCategoryName,
categoryKey: 'insights',
Expand Down
24 changes: 12 additions & 12 deletions webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { UnknownIcon } from '@patternfly/react-icons';
import { translate as __ } from 'foremanReact/common/I18n';
import { Link } from 'react-router-dom';
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
import { propsToCamelCase } from 'foremanReact/common/helpers';
import { insightsCloudUrl } from '../InsightsCloudSync/InsightsCloudSyncHelpers';
import { useIopConfig } from '../common/Hooks/ConfigHooks';

Expand All @@ -24,24 +25,23 @@ export const CVECountCell = ({ hostDetails }) => {
}
);

if (!isIopEnabled) {
return <UnknownIcon />;
if (!isIopEnabled || uuid === undefined) {
return '—';
}

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

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

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

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

CVECountCell.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
import { API } from 'foremanReact/redux/API';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import { createMemoryHistory } from 'history';
import configureMockStore from 'redux-mock-store';
import { STATUS } from 'foremanReact/constants';
import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
import * as ConfigHooks from '../../common/Hooks/ConfigHooks';
import { CVECountCell } from '../CVECountCell';

jest.mock('foremanReact/redux/API');
jest.mock('foremanReact/common/hooks/API/APIHooks');
jest.mock('../../common/Hooks/ConfigHooks');

const { renderWithStore } = rtlHelpers;

API.get.mockImplementation(async () => ({
data: [
{
attributes: {
cve_count: 1,
},
const mockStore = configureMockStore();
const history = createMemoryHistory();
const store = mockStore({
router: {
location: {
pathname: '/',
search: '',
hash: '',
state: null,
},
],
}));
action: 'POP',
},
});

const hostDetailsMock = {
name: 'test-host.example.com',
Expand All @@ -26,104 +34,121 @@ const hostDetailsMock = {
},
};

const renderComponent = (props = {}) => {
const allProps = {
hostDetails: hostDetailsMock,
...props,
};

return render(
<Provider store={store}>
<ConnectedRouter history={history}>
<CVECountCell {...allProps} />
</ConnectedRouter>
</Provider>
);
};

describe('CVECountCell', () => {
afterEach(() => {
beforeEach(() => {
store.clearActions();
jest.clearAllMocks();
});

it('renders an empty cves count column when no subscription UUID', () => {
const hostDetailsMockIoP = {
ConfigHooks.useIopConfig.mockReturnValue(true);
APIHooks.useAPI.mockReturnValue({
status: STATUS.RESOLVED,
response: null,
});

const hostDetailsMockNoUuid = {
name: 'test-host.example.com',
subscription_facet_attributes: {
uuid: null, // no subscription
},
};
renderWithStore(<CVECountCell hostDetails={hostDetailsMockIoP} />, {
router: {
location: {
pathname: '/',
search: '',
hash: '',
query: {},
},
},
API: {
ADVISOR_ENGINE_CONFIG: {
response: { use_iop_mode: true },
status: 'RESOLVED',
},
},

renderComponent({ hostDetails: hostDetailsMockNoUuid });
expect(screen.getByText('—')).toBeInTheDocument();
});

it('renders — when IoP is not enabled', () => {
ConfigHooks.useIopConfig.mockReturnValue(false);
APIHooks.useAPI.mockReturnValue({
status: STATUS.RESOLVED,
response: null,
});
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();

renderComponent();
expect(screen.getByText('—')).toBeInTheDocument();
});

it('renders UnknownIcon when IoP is not enabled', () => {
renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
router: {
location: {
pathname: '/',
search: '',
hash: '',
query: {},
},
},
API: {
ADVISOR_ENGINE_CONFIG: {
response: { use_iop_mode: false },
status: 'RESOLVED',
},
},
it('renders — when IoP is enabled but CVE API call fails', () => {
ConfigHooks.useIopConfig.mockReturnValue(true);
APIHooks.useAPI.mockReturnValue({
status: STATUS.ERROR,
response: null,
});
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();

renderComponent();
expect(screen.getByText('—')).toBeInTheDocument();
});

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

renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
router: {
location: {
pathname: '/',
search: '',
hash: '',
query: {},
},
},
API: {
ADVISOR_ENGINE_CONFIG: {
response: { use_iop_mode: true },
status: 'RESOLVED',
},
// Mock the API failure state for the CVE endpoint
[`HOST_CVE_COUNT_${hostDetailsMock.subscription_facet_attributes.uuid}`]: {
status: 'ERROR',
error: 'CVE API call failed',
},
renderComponent();
expect(screen.getByText('—')).toBeInTheDocument();
});

it('renders "Analysis disabled" when opt_out is true', () => {
ConfigHooks.useIopConfig.mockReturnValue(true);
APIHooks.useAPI.mockReturnValue({
status: STATUS.RESOLVED,
response: {
data: [
{
attributes: {
opt_out: true,
},
},
],
},
});
// Should render UnknownIcon when CVE API fails
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();

renderComponent();
expect(screen.getByText('Analysis disabled')).toBeInTheDocument();
});

it('renders UnknownIcon when IoP is undefined (API call pending)', () => {
renderWithStore(<CVECountCell hostDetails={hostDetailsMock} />, {
router: {
location: {
pathname: '/',
search: '',
hash: '',
query: {},
},
},
API: {
ADVISOR_ENGINE_CONFIG: {
status: 'PENDING',
},
it('renders CVE count link when valid count is returned', () => {
ConfigHooks.useIopConfig.mockReturnValue(true);
APIHooks.useAPI.mockReturnValue({
status: STATUS.RESOLVED,
response: {
data: [
{
attributes: {
cve_count: 1,
},
},
],
},
});
expect(screen.getByRole('img', { hidden: true })).toBeTruthy();

renderComponent();

// Should render a link with the CVE count
const link = screen.getByRole('link');
expect(link).toBeInTheDocument();
expect(link).toHaveTextContent('1');
expect(link).toHaveAttribute(
'href',
`/hosts/${hostDetailsMock.name}#/Vulnerabilities`
);
});
});
11 changes: 11 additions & 0 deletions webpack/__mocks__/foremanReact/common/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import camelCase from 'lodash/camelCase';

export const getURIQuery = jest.fn(() => ({}));

export const noop = Function.prototype;

export const propsToCamelCase = ob => {
if (typeof ob !== 'object' || ob === null) return ob;

return Object.keys(ob).reduce((memo, key) => {
memo[camelCase(key)] = ob[key];
return memo;
}, {});
};
Loading