From 7f000632de0c6b36e25b57717a2f943232f8a7c4 Mon Sep 17 00:00:00 2001 From: Paulo Silva Date: Tue, 5 Nov 2024 14:34:18 -0800 Subject: [PATCH] [Fleet] [Cloud Security] Add Testing Library ESLint for handling waitFor (#198735) ## Summary This PR aims to fix Flaky tests related to agentless detected by https://github.com/elastic/kibana/issues/189038 and https://github.com/elastic/kibana/issues/192126 by adding proper handling of the `waitFor` methods. It was also detected with https://github.com/elastic/security-team/issues/10979 that some other methods were not proper handled by `waitFor`, leading to the assertions inside those unhandled `waitFor` being skipped by Jest. This PR also introduces ESLint to enforce proper handling of waitFor methods in tests files for Fleet and Cloud Security plugins. Additional note: These changes should also unblock the failing tests on the [React18 use waitFor with assertion callbacks in place of waitForNextUpdate](https://github.com/elastic/kibana/pull/195087) PR **Fleet changes** - ESLint rule added to enforce handling `waitFor` on React Testing Library. - `useSetupTechnology` hook tests reviewed and updated to handle the waitFor. Fixed issue identified when reviewing the tests. - step_define_package_policy.test.tsx: Added package policy vars to the mock to proper handle the use cases - step_select_hosts.test.tsx: Handled waitFor, identified outdated test - step_edit_hosts.test.tsx: Handled waitFor, identified outdated test With the introduction of the ESLint rule other tests were triggering ESLint errors, I attempted to fix them while retaining the same intention, let me know if more changes are needed. **Cloud Security changes** - ESLint rule added to enforce handling `waitFor` on React Testing Library. - Updated cloud security posture version to include agentless global tags on End to End tests **@elastic/kibana-operations changes** - Added [eslint-plugin-testing-library](https://testing-library.com/docs/ecosystem-eslint-plugin-testing-library/) an ESLint plugin for Testing Library that helps users to follow best practices and anticipate common mistakes when writing tests. - The adoption and enablement of the rules are opt-in. (cherry picked from commit 5ab59fba401a189c290e55b3f73fd4fd23106e13) # Conflicts: # x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts # x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts --- .eslintrc.js | 12 + package.json | 1 + .../common/constants.ts | 2 +- .../steps/step_define_package_policy.test.tsx | 122 ++++--- .../steps/step_select_hosts.test.tsx | 46 ++- .../hooks/setup_technology.test.ts | 334 +++++++++++++++--- .../hooks/setup_technology.ts | 8 +- .../components/step_edit_hosts.test.tsx | 32 +- yarn.lock | 9 +- 9 files changed, 438 insertions(+), 128 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e46dde5a3c56f..b97bab2bff8b4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1024,7 +1024,9 @@ module.exports = { */ { files: ['x-pack/plugins/fleet/**/*.{js,mjs,ts,tsx}'], + plugins: ['testing-library'], rules: { + 'testing-library/await-async-utils': 'error', '@typescript-eslint/consistent-type-imports': 'error', 'import/order': [ 'warn', @@ -1954,6 +1956,16 @@ module.exports = { }, }, + /** + * Cloud Security Team overrides + */ + { + files: ['x-pack/plugins/cloud_security_posture/**/*.{js,mjs,ts,tsx}'], + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-utils': 'error', + }, + }, /** * Code inside .buildkite runs separately from everything else in CI, before bootstrap, with ts-node. It needs a few tweaks because of this. */ diff --git a/package.json b/package.json index e8a0bb3cceda6..f67fb7c909903 100644 --- a/package.json +++ b/package.json @@ -1728,6 +1728,7 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-perf": "^3.3.1", + "eslint-plugin-testing-library": "^6.4.0", "eslint-traverse": "^1.0.0", "exit-hook": "^2.2.0", "expect": "^29.7.0", diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index efc56a0da7995..80a6532c4a094 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -174,4 +174,4 @@ export const SINGLE_ACCOUNT = 'single-account'; export const CLOUD_SECURITY_PLUGIN_VERSION = '1.9.0'; // Cloud Credentials Template url was implemented in 1.10.0-preview01. See PR - https://github.com/elastic/integrations/pull/9828 -export const CLOUD_CREDENTIALS_PACKAGE_VERSION = '1.11.0-preview10'; +export const CLOUD_CREDENTIALS_PACKAGE_VERSION = '1.11.0-preview13'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx index 1ebfe5a897b07..62b39e2e8708a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; -import { act, fireEvent, waitFor } from '@testing-library/react'; +import { waitFor, act } from '@testing-library/react'; + +import { userEvent } from '@testing-library/user-event'; import { getInheritedNamespace } from '../../../../../../../../common/services'; @@ -60,18 +62,6 @@ describe('StepDefinePackagePolicy', () => { package_policies: [], is_protected: false, }, - { - id: 'agent-policy-2', - namespace: 'default', - name: 'Agent policy 2', - is_managed: false, - status: 'active', - updated_at: '', - updated_by: '', - revision: 1, - package_policies: [], - is_protected: false, - }, ]; let packagePolicy: NewPackagePolicy; const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { @@ -86,20 +76,23 @@ describe('StepDefinePackagePolicy', () => { description: null, namespace: null, inputs: {}, - vars: {}, + vars: { + 'Required var': ['Required var is required'], + }, }; let testRenderer: TestRenderer; let renderResult: ReturnType; - const render = () => + + const render = (namespacePlaceholder = getInheritedNamespace(agentPolicies)) => (renderResult = testRenderer.render( )); @@ -107,57 +100,100 @@ describe('StepDefinePackagePolicy', () => { packagePolicy = { name: '', description: 'desc', - namespace: 'default', + namespace: 'package-policy-ns', + enabled: true, policy_id: '', policy_ids: [''], - enabled: true, + package: { + name: 'apache', + title: 'Apache', + version: '1.0.0', + }, inputs: [], + vars: { + 'Show user var': { + type: 'string', + value: 'showUserVarVal', + }, + 'Required var': { + type: 'bool', + value: undefined, + }, + 'Advanced var': { + type: 'bool', + value: true, + }, + }, }; testRenderer = createFleetTestRendererMock(); }); describe('default API response', () => { - beforeEach(() => { - render(); - }); - it('should display vars coming from package policy', async () => { - waitFor(() => { - expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); - expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Required var'); - expect(renderResult.getByText('Required var is required')).toHaveAttribute( - 'class', - 'euiFormErrorText' + act(() => { + render(); + }); + expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); + expect(renderResult.getByRole('switch', { name: 'Required var' })).toBeInTheDocument(); + expect(renderResult.queryByRole('switch', { name: 'Advanced var' })).not.toBeInTheDocument(); + + expect(renderResult.getByText('Required var is required')).toHaveClass('euiFormErrorText'); + + await userEvent.click(renderResult.getByText('Advanced options').closest('button')!); + + await waitFor(() => { + expect(renderResult.getByRole('switch', { name: 'Advanced var' })).toBeInTheDocument(); + expect(renderResult.getByTestId('packagePolicyNamespaceInput')).toHaveTextContent( + 'package-policy-ns' ); }); + }); - await act(async () => { - fireEvent.click(renderResult.getByText('Advanced options').closest('button')!); + it(`should display namespace from agent policy when there's no package policy namespace`, async () => { + packagePolicy.namespace = ''; + act(() => { + render(); }); - waitFor(() => { - expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Advanced var'); - expect(renderResult.getByTestId('packagePolicyNamespaceInput')).toHaveAttribute( + await userEvent.click(renderResult.getByText('Advanced options').closest('button')!); + + await waitFor(() => { + expect(renderResult.getByTestId('comboBoxSearchInput')).toHaveAttribute( 'placeholder', 'ns' ); }); }); + + it(`should fallback to the default namespace when namespace is not set in package policy and there's no agent policy`, async () => { + packagePolicy.namespace = ''; + act(() => { + render(getInheritedNamespace([])); + }); + + await userEvent.click(renderResult.getByText('Advanced options').closest('button')!); + + await waitFor(() => { + expect(renderResult.getByTestId('comboBoxSearchInput')).toHaveAttribute( + 'placeholder', + 'default' + ); + }); + }); }); describe('update', () => { describe('when package vars are introduced in a new package version', () => { - it('should display new package vars', () => { - render(); - - waitFor(async () => { - expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); - expect(renderResult.getByText('Required var')).toBeInTheDocument(); + it('should display new package vars', async () => { + act(() => { + render(); + }); + expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); + expect(renderResult.getByText('Required var')).toBeInTheDocument(); - await act(async () => { - fireEvent.click(renderResult.getByText('Advanced options').closest('button')!); - }); + await userEvent.click(renderResult.getByText('Advanced options').closest('button')!); + await waitFor(async () => { expect(renderResult.getByText('Advanced var')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx index 7d1962939d1fa..583957861bd79 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; -import { act, fireEvent, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; + +import { userEvent } from '@testing-library/user-event'; import type { TestRenderer } from '../../../../../../../mock'; import { createFleetTestRendererMock } from '../../../../../../../mock'; @@ -108,22 +110,23 @@ describe('StepSelectHosts', () => { testRenderer = createFleetTestRendererMock(); }); - it('should display create form when no agent policies', () => { + it('should display create form when no agent policies', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [], }, }); + (useAllNonManagedAgentPolicies as jest.MockedFunction).mockReturnValue([]); render(); - waitFor(() => { - expect(renderResult.getByText('Agent policy 1')).toBeInTheDocument(); + await waitFor(() => { + expect(renderResult.getByText('New agent policy name')).toBeInTheDocument(); }); expect(renderResult.queryByRole('tablist')).not.toBeInTheDocument(); }); - it('should display tabs with New hosts selected when agent policies exist', () => { + it('should display tabs with New hosts selected when agent policies exist', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [{ id: '1', name: 'Agent policy 1', namespace: 'default' }], @@ -135,10 +138,7 @@ describe('StepSelectHosts', () => { render(); - waitFor(() => { - expect(renderResult.getByRole('tablist')).toBeInTheDocument(); - expect(renderResult.getByText('Agent policy 3')).toBeInTheDocument(); - }); + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); expect(renderResult.getByText('New hosts').closest('button')).toHaveAttribute( 'aria-selected', 'true' @@ -157,16 +157,15 @@ describe('StepSelectHosts', () => { render(); - waitFor(() => { - expect(renderResult.getByRole('tablist')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); - }); + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + + await userEvent.click(renderResult.getByText('Existing hosts').closest('button')!); - expect( - renderResult.container.querySelector('[data-test-subj="agentPolicySelect"]')?.textContent - ).toContain('Agent policy 1'); + await waitFor(() => { + expect( + renderResult.container.querySelector('[data-test-subj="agentPolicySelect"]')?.textContent + ).toContain('Agent policy 1'); + }); }); it('should display dropdown without preselected value when Existing hosts selected with mulitple agent policies', async () => { @@ -185,14 +184,11 @@ describe('StepSelectHosts', () => { render(); - waitFor(() => { - expect(renderResult.getByRole('tablist')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); - }); + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + + await userEvent.click(renderResult.getByText('Existing hosts').closest('button')!); - await act(async () => { + await waitFor(() => { const select = renderResult.container.querySelector('[data-test-subj="agentPolicySelect"]'); expect((select as any)?.value).toEqual(''); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index 550a288dad371..677cb38dd38c4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks/dom'; import { waitFor } from '@testing-library/react'; @@ -195,7 +195,7 @@ describe('useSetupTechnology', () => { }); it('should fetch agentless policy if agentless feature is enabled and isServerless is true', async () => { - const { waitForNextUpdate } = renderHook(() => + renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -205,9 +205,9 @@ describe('useSetupTechnology', () => { }) ); - await waitForNextUpdate(); - - expect(sendGetOneAgentPolicy).toHaveBeenCalled(); + await waitFor(() => { + expect(sendGetOneAgentPolicy).toHaveBeenCalled(); + }); }); it('should set agentless setup technology if agent policy supports agentless in edit page', async () => { @@ -253,7 +253,7 @@ describe('useSetupTechnology', () => { isCloudEnabled: true, }, }); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -268,14 +268,13 @@ describe('useSetupTechnology', () => { act(() => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); - - waitForNextUpdate(); - - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); - expect(setNewAgentPolicy).toHaveBeenCalledWith({ - name: 'Agentless policy for endpoint-1', - supports_agentless: true, - inactivity_timeout: 3600, + await waitFor(() => { + expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + name: 'Agentless policy for endpoint-1', + supports_agentless: true, + inactivity_timeout: 3600, + }); }); }); @@ -293,15 +292,18 @@ describe('useSetupTechnology', () => { isCloudEnabled: true, }, }); - const { result, rerender } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, - }) - ); + + const initialProps = { + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + }; + + const { result, rerender } = renderHook((props = initialProps) => useSetupTechnology(props), { + initialProps, + }); expect(generateNewAgentPolicyWithDefaults).toHaveBeenCalled(); @@ -327,7 +329,7 @@ describe('useSetupTechnology', () => { }, }); - waitFor(() => { + await waitFor(() => { expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-2', inactivity_timeout: 3600, @@ -344,7 +346,7 @@ describe('useSetupTechnology', () => { }, }); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -360,8 +362,7 @@ describe('useSetupTechnology', () => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED); }); - waitForNextUpdate(); - expect(setNewAgentPolicy).toHaveBeenCalledTimes(0); + await waitFor(() => expect(setNewAgentPolicy).toHaveBeenCalledTimes(0)); }); it('should not fetch agentless policy if agentless is enabled but serverless is disabled', async () => { @@ -386,7 +387,7 @@ describe('useSetupTechnology', () => { }); it('should update agent policy and selected policy tab when setup technology is agentless', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -396,18 +397,24 @@ describe('useSetupTechnology', () => { }) ); - await waitForNextUpdate(); - act(() => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); - expect(updateAgentPoliciesMock).toHaveBeenCalledWith([{ id: 'agentless-policy-id' }]); - expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.EXISTING); + await waitFor(() => { + expect(updateAgentPoliciesMock).toHaveBeenCalledWith([ + { + inactivity_timeout: 3600, + name: 'Agentless policy for endpoint-1', + supports_agentless: true, + }, + ]); + expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.EXISTING); + }); }); it('should update new agent policy and selected policy tab when setup technology is agent-based', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -417,8 +424,6 @@ describe('useSetupTechnology', () => { }) ); - await waitForNextUpdate(); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); act(() => { @@ -433,8 +438,10 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); - expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.NEW); + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); + expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.NEW); + }); }); it('should not update agent policy and selected policy tab when agentless is disabled', async () => { @@ -462,7 +469,7 @@ describe('useSetupTechnology', () => { }); it('should not update agent policy and selected policy tab when setup technology matches the current one ', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -472,7 +479,7 @@ describe('useSetupTechnology', () => { }) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); @@ -487,7 +494,7 @@ describe('useSetupTechnology', () => { }); it('should revert the agent policy name to the original value when switching from agentless back to agent-based', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, @@ -497,8 +504,6 @@ describe('useSetupTechnology', () => { }) ); - await waitForNextUpdate(); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); act(() => { @@ -507,7 +512,7 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); - waitFor(() => { + await waitFor(() => { expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', supports_agentless: true, @@ -522,4 +527,247 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); }); + + it('should have global_data_tags with the integration team when creating agentless policy with global_data_tags', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS, 'cspm'); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith( + expect.objectContaining({ + supports_agentless: true, + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }) + ); + }); + }); + + it('should not fail and not have global_data_tags when creating the agentless policy when it cannot find the policy template', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange( + SetupTechnology.AGENTLESS, + 'never-gonna-give-you-up' + ); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + name: 'Agentless policy for endpoint-1', + supports_agentless: true, + inactivity_timeout: 3600, + }); + expect(setNewAgentPolicy).not.toHaveBeenCalledWith({ + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }); + }); + }); + + it('should not fail and not have global_data_tags when creating the agentless policy without the policy template name', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + name: 'Agentless policy for endpoint-1', + supports_agentless: true, + inactivity_timeout: 3600, + }); + expect(setNewAgentPolicy).not.toHaveBeenCalledWith({ + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }); + }); + }); + + it('should not fail and not have global_data_tags when creating the agentless policy without the packageInfo', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS, 'cspm'); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + name: 'Agentless policy for endpoint-1', + supports_agentless: true, + inactivity_timeout: 3600, + }); + expect(setNewAgentPolicy).not.toHaveBeenCalledWith({ + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }); + }); + }); + + it('should not have global_data_tags when switching from agentless to agent-based policy', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS, 'cspm'); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith( + expect.objectContaining({ + supports_agentless: true, + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }) + ); + }); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED); + }); + + await waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); + expect(setNewAgentPolicy).not.toHaveBeenCalledWith({ + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 241dcfbb93f4e..a1298baa02138 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -157,7 +157,13 @@ export function useSetupTechnology({ if (setupTechnology === SetupTechnology.AGENTLESS) { if (isAgentlessApiEnabled) { - setNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); + const agentlessPolicy = { + ...newAgentlessPolicy, + ...getAdditionalAgentlessPolicyInfo(policyTemplateName, packageInfo), + } as NewAgentPolicy; + + setNewAgentPolicy(agentlessPolicy); + setNewAgentlessPolicy(agentlessPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/components/step_edit_hosts.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/components/step_edit_hosts.test.tsx index 064624d364a92..e30fa6c22c5ce 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/components/step_edit_hosts.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/components/step_edit_hosts.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; -import { act, fireEvent, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; + +import { userEvent } from '@testing-library/user-event'; import type { TestRenderer } from '../../../../../../mock'; import { createFleetTestRendererMock } from '../../../../../../mock'; @@ -111,18 +113,18 @@ describe('StepEditHosts', () => { testRenderer = createFleetTestRendererMock(); }); - it('should display create form when no agent policies', () => { + it('should display create form when no agent policies', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [], }, }); + (useAllNonManagedAgentPolicies as jest.MockedFunction).mockReturnValue([]); + render(); - waitFor(() => { - expect(renderResult.getByText('Agent policy 1')).toBeInTheDocument(); - }); + expect(renderResult.getByText('New agent policy name')).toBeInTheDocument(); expect(renderResult.queryByRole('tablist')).not.toBeInTheDocument(); }); @@ -144,7 +146,7 @@ describe('StepEditHosts', () => { ).toContain('Agent policy 1'); }); - it('should display dropdown without preselected value when mulitple agent policies', () => { + it('should display dropdown without preselected value when multiple agent policies', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [ @@ -156,12 +158,12 @@ describe('StepEditHosts', () => { render(); - waitFor(() => { - expect(renderResult.getByText('At least one agent policy is required.')).toBeInTheDocument(); - }); + expect( + renderResult.getByText('Select an agent policy to add this integration to') + ).toBeInTheDocument(); }); - it('should display delete button when add button clicked', () => { + it('should display delete button when add button clicked', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [{ id: '1', name: 'Agent policy 1', namespace: 'default' }], @@ -173,10 +175,12 @@ describe('StepEditHosts', () => { render(); - act(() => { - fireEvent.click(renderResult.getByTestId('createNewAgentPolicyButton').closest('button')!); - }); + await userEvent.click( + renderResult.getByTestId('createNewAgentPolicyButton').closest('button')! + ); - expect(renderResult.getByTestId('deleteNewAgentPolicyButton')).toBeInTheDocument(); + await waitFor(() => { + expect(renderResult.getByTestId('deleteNewAgentPolicyButton')).toBeInTheDocument(); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 4d62814469bad..4da30a3e4eabd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11801,7 +11801,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^6.18.1": +"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.62.0", "@typescript-eslint/utils@^6.18.1": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== @@ -17549,6 +17549,13 @@ eslint-plugin-react@^7.32.2: semver "^6.3.0" string.prototype.matchall "^4.0.8" +eslint-plugin-testing-library@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.4.0.tgz#1ba8a7422e3e31cc315a73ff17c34908f56f9838" + integrity sha512-yeWF+YgCgvNyPNI9UKnG0FjeE2sk93N/3lsKqcmR8dSfeXJwFT5irnWo7NjLf152HkRzfoFjh3LsBUrhvFz4eA== + dependencies: + "@typescript-eslint/utils" "^5.62.0" + eslint-rule-composer@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"