diff --git a/webpack/ForemanColumnExtensions/index.js b/webpack/ForemanColumnExtensions/index.js
index d869f5d71..3a968d8b2 100644
--- a/webpack/ForemanColumnExtensions/index.js
+++ b/webpack/ForemanColumnExtensions/index.js
@@ -74,7 +74,7 @@ const hostsIndexColumnExtensions = [
columnName: 'cves_count',
title: __('Total CVEs'),
wrapper: hostDetails => ,
- weight: 2600,
+ weight: 1600,
tableName: 'hosts',
categoryName: insightsCategoryName,
categoryKey: 'insights',
diff --git a/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js b/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
index d37fa61ca..70688ca1e 100644
--- a/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
+++ b/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js
@@ -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';
@@ -24,24 +25,23 @@ export const CVECountCell = ({ hostDetails }) => {
}
);
- if (!isIopEnabled) {
- return ;
+ if (!isIopEnabled || uuid === undefined) {
+ return '—';
}
- if (uuid === undefined) {
- return ;
- }
+ 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
- {cve_count}
+ {cveCount}
);
- // eslint-disable-next-line camelcase
- return cve_count === undefined ? : cveLink;
+ return cveCount === undefined ? '—' : cveLink;
};
CVECountCell.propTypes = {
diff --git a/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js b/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
index 5e6459925..ca4006a90 100644
--- a/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
+++ b/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
@@ -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',
@@ -26,104 +34,121 @@ const hostDetailsMock = {
},
};
+const renderComponent = (props = {}) => {
+ const allProps = {
+ hostDetails: hostDetailsMock,
+ ...props,
+ };
+
+ return render(
+
+
+
+
+
+ );
+};
+
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(, {
- 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(, {
- 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(, {
- 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(, {
- 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`
+ );
});
});
diff --git a/webpack/__mocks__/foremanReact/common/helpers.js b/webpack/__mocks__/foremanReact/common/helpers.js
index 29e97f87f..1b85dbf4c 100644
--- a/webpack/__mocks__/foremanReact/common/helpers.js
+++ b/webpack/__mocks__/foremanReact/common/helpers.js
@@ -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;
+ }, {});
+};