import React from 'react'; import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper'; import * as hooks from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; import { nockInstance, assertNockRequest, mockForemanAutocomplete } from '../../../../../test-utils/nockWrapper'; import { foremanApi } from '../../../../../services/api'; import { HOST_DEBS_KEY, PACKAGES_SEARCH_QUERY, SELECTED_UPDATE_VERSIONS } from '../DebsTab/HostDebsConstants'; import { DebsTab } from '../DebsTab/DebsTab.js'; import mockDebsData from './debs.fixtures.json'; import { REX_FEATURES } from '../RemoteExecutionConstants'; jest.mock('../../hostDetailsHelpers', () => ({ ...jest.requireActual('../../hostDetailsHelpers'), userPermissionsFromHostDetails: () => ({ create_job_invocations: true, edit_hosts: true, }), })); const contentFacetAttributes = { id: 11, uuid: 'e5761ea3-4117-4ecf-83d0-b694f99b389e', content_view_default: false, lifecycle_environment_library: false, }; const hostname = 'test-host.example.com'; const renderOptions = (facetAttributes = contentFacetAttributes) => ({ apiNamespace: HOST_DEBS_KEY, initialState: { API: { HOST_DETAILS: { response: { id: 1, name: hostname, content_facet_attributes: { ...facetAttributes }, }, status: 'RESOLVED', }, }, }, }); const hostDebs = foremanApi.getApiUrl('/hosts/1/debs'); const jobInvocations = foremanApi.getApiUrl('/job_invocations'); const autocompleteUrl = '/hosts/1/debs/auto_complete_search'; const defaultQueryWithoutSearch = { include_latest_upgradable: true, sort_by: 'name', sort_order: 'asc', per_page: 20, page: 1, }; const defaultQuery = { ...defaultQueryWithoutSearch, search: '' }; let firstDeb; let secondDeb; beforeEach(() => { const { results } = mockDebsData; [firstDeb, secondDeb] = results; }); test('Can call API for packages and show on screen on page load', async (done) => { // Setup autocomplete with mockForemanAutoComplete since we aren't adding /katello const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const { getAllByText } = renderWithRedux(, renderOptions()); // Assert that the packages are now showing on the screen, but wait for them to appear. await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); // Assert request was made and completed, see helper function assertNockRequest(autocompleteScope); assertNockRequest(scope, done); // Pass jest callback to confirm test is done }); test('Can handle no packages being present', async (done) => { // Setup autocomplete with mockForemanAutoComplete since we aren't adding /katello const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const noResults = { total: 0, subtotal: 0, page: 1, per_page: 20, results: [], }; const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, noResults); const { queryByText } = renderWithRedux(, renderOptions()); // Assert that there are not any packages showing on the screen. await patientlyWaitFor(() => expect(queryByText('This host does not have any packages.')).toBeInTheDocument()); // Assert request was made and completed, see helper function assertNockRequest(autocompleteScope); assertNockRequest(scope, done); // Pass jest callback to confirm test is done }); test('Can filter by package status', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const scope2 = nockInstance .get(hostDebs) .query({ ...defaultQuery, status: 'upgradable' }) .reply(200, { ...mockDebsData, results: [firstDeb, secondDeb] }); const { queryByText, getByRole, getAllByText, getByText, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); // the Upgradable text in the table is just a text node, while the dropdown is a button expect(getByText('Up-to date', { ignore: ['button', 'title'] })).toBeInTheDocument(); expect(getByText('libapt-pkg6.0', { ignore: ['button', 'title'] })).toBeInTheDocument(); expect(getByText('libmagic1', { ignore: ['button', 'title'] })).toBeInTheDocument(); expect(getByText('libacl1', { ignore: ['button', 'title'] })).toBeInTheDocument(); const statusDropdown = queryByText('Status', { ignore: 'th' }); expect(statusDropdown).toBeInTheDocument(); fireEvent.click(statusDropdown); const upgradable = getByRole('option', { name: 'select Upgradable' }); fireEvent.click(upgradable); await patientlyWaitFor(() => { expect(queryByText('libmagic1')).toBeInTheDocument(); expect(queryByText('libapt-pkg6.0')).toBeInTheDocument(); expect(queryByText('libacl1')).not.toBeInTheDocument(); }); assertNockRequest(autocompleteScope); assertNockRequest(scope); assertNockRequest(scope2, done); // Pass jest callback to confirm test is done }); test('Can upgrade a package via remote execution', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const statusScope = nockInstance .get(hostDebs) .query({ ...defaultQuery, status: 'upgradable' }) .reply(200, { ...mockDebsData, results: [firstDeb, secondDeb] }); const upgradeScope = nockInstance .post(jobInvocations, { job_invocation: { inputs: { package: firstDeb.name, }, search_query: `name ^ (${hostname})`, feature: REX_FEATURES.KATELLO_PACKAGE_UPDATE, }, }) .reply(201); const { getByRole, getAllByText, getAllByLabelText, getByText, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); const statusDropdown = getByText('Status', { ignore: 'th' }); expect(statusDropdown).toBeInTheDocument(); fireEvent.click(statusDropdown); const upgradable = getByRole('option', { name: 'select Upgradable' }); fireEvent.click(upgradable); await patientlyWaitFor(() => { expect(getByText('libmagic1')).toBeInTheDocument(); expect(getByText('libapt-pkg6.0')).toBeInTheDocument(); }); const kebabDropdown = getAllByLabelText('Actions'); kebabDropdown[0].click(); const rexAction = getByText('Upgrade via remote execution'); await patientlyWaitFor(() => expect(rexAction).toBeInTheDocument()); fireEvent.click(rexAction); assertNockRequest(autocompleteScope); assertNockRequest(scope); assertNockRequest(statusScope); assertNockRequest(upgradeScope, done); }); test('Can upgrade a package via customized remote execution', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const statusScope = nockInstance .get(hostDebs) .query({ ...defaultQuery, status: 'upgradable' }) .reply(200, { ...mockDebsData, results: [firstDeb, secondDeb] }); const { getByRole, getAllByText, getAllByLabelText, getByText, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); const statusDropdown = getByText('Status', { ignore: 'th' }); expect(statusDropdown).toBeInTheDocument(); fireEvent.click(statusDropdown); const upgradable = getByRole('option', { name: 'select Upgradable' }); fireEvent.click(upgradable); await patientlyWaitFor(() => { expect(getByText('libapt-pkg6.0')).toBeInTheDocument(); expect(getByText('libmagic1')).toBeInTheDocument(); }); const kebabDropdown = getAllByLabelText('Actions'); kebabDropdown[0].click(); const rexAction = getByText('Upgrade via customized remote execution'); const feature = REX_FEATURES.KATELLO_PACKAGE_UPDATE; const packageName = firstDeb.name; expect(rexAction).toBeInTheDocument(); expect(rexAction).toHaveAttribute( 'href', `/job_invocations/new?feature=${feature}&search=name%20%5E%20(${hostname})&inputs%5Bpackage%5D=${packageName}`, ); fireEvent.click(rexAction); assertNockRequest(autocompleteScope); assertNockRequest(scope); assertNockRequest(statusScope, done); }); test('Can bulk upgrade via remote execution', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const upgradeScope = nockInstance .post(jobInvocations, { job_invocation: { inputs: { [PACKAGES_SEARCH_QUERY]: `id ^ (${firstDeb.id},${secondDeb.id})`, [SELECTED_UPDATE_VERSIONS]: JSON.stringify([]), }, search_query: `name ^ (${hostname})`, feature: REX_FEATURES.KATELLO_PACKAGES_UPDATE_BY_SEARCH, }, }) .reply(201); const { getAllByRole, getAllByText, getByRole, getByLabelText, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); getByRole('checkbox', { name: 'Select row 0' }).click(); expect(getByLabelText('Select row 0').checked).toEqual(true); getByRole('checkbox', { name: 'Select row 1' }).click(); expect(getByLabelText('Select row 1').checked).toEqual(true); const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1]; fireEvent.click(upgradeDropdown); const rexAction = getByLabelText('bulk_upgrade_rex'); expect(rexAction).toBeInTheDocument(); fireEvent.click(rexAction); assertNockRequest(autocompleteScope); assertNockRequest(scope); assertNockRequest(upgradeScope, done); }); test('Can bulk upgrade via customized remote execution', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const { getAllByRole, getAllByText, getByRole, getByLabelText, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); const feature = REX_FEATURES.KATELLO_PACKAGES_UPDATE_BY_SEARCH; const packages = `${firstDeb.id},${secondDeb.id}`; const job = `/job_invocations/new?feature=${feature}&search=name%20%5E%20(${hostname})&inputs%5BPackages%20search%20query%5D=id%20%5E%20(${packages})&inputs%5BSelected%20update%20versions%5D=%5B%5D`; getByRole('checkbox', { name: 'Select row 0' }).click(); expect(getByLabelText('Select row 0').checked).toEqual(true); getByRole('checkbox', { name: 'Select row 1' }).click(); expect(getByLabelText('Select row 1').checked).toEqual(true); const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1]; fireEvent.click(upgradeDropdown); expect(upgradeDropdown).not.toHaveAttribute('disabled'); const rexAction = getByLabelText('bulk_upgrade_customized_rex'); expect(rexAction).toBeInTheDocument(); expect(rexAction).toHaveAttribute('href', job); assertNockRequest(autocompleteScope); assertNockRequest(scope, done); }); test('Upgrade is disabled when there are non-upgradable packages selected', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const { getAllByRole, getAllByText, getByLabelText, getByRole, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); // select an upgradable package getByRole('checkbox', { name: 'Select row 0' }).click(); // select an up-to-date package getByRole('checkbox', { name: 'Select row 2' }).click(); expect(getByLabelText('Select row 2').checked).toEqual(true); const upgradeDropdown = getAllByRole('button', { name: 'Select' })[1]; expect(upgradeDropdown).toHaveAttribute('disabled'); assertNockRequest(autocompleteScope); assertNockRequest(scope, done); }); test('Remove is disabled when in select all mode', async (done) => { const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query(defaultQuery) .reply(200, mockDebsData); const { getAllByText, getByRole, } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); // find and click the select all checkbox const selectAllCheckbox = getByRole('checkbox', { name: 'Select all' }); fireEvent.click(selectAllCheckbox); getByRole('button', { name: 'bulk_actions' }).click(); const removeButton = getByRole('menuitem', { name: 'bulk_remove' }); await patientlyWaitFor(() => expect(removeButton).toBeInTheDocument()); expect(removeButton).toHaveAttribute('aria-disabled', 'true'); assertNockRequest(autocompleteScope); assertNockRequest(scope, done); }); test('Sets initial search query from url params', async (done) => { // Setup autocomplete with mockForemanAutoComplete since we aren't adding /katello const autocompleteScope = mockForemanAutocomplete(nockInstance, autocompleteUrl); const scope = nockInstance .get(hostDebs) .query({ ...defaultQuery, search: `name=${firstDeb.name}` }) .reply(200, { ...mockDebsData, results: [firstDeb] }); jest.spyOn(hooks, 'useUrlParams').mockImplementation(() => ({ searchParam: `name=${firstDeb.name}`, })); const { getAllByText, queryByText } = renderWithRedux(, renderOptions()); await patientlyWaitFor(() => expect(getAllByText(firstDeb.name)[0]).toBeInTheDocument()); expect(queryByText(secondDeb.name)).not.toBeInTheDocument(); assertNockRequest(autocompleteScope); assertNockRequest(scope, done); // Pass jest callback to confirm test is done });