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; + }, {}); +};