From d4a631cf8e2e6ef9fcc4c86256013d4389d5df7d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 15 Dec 2020 08:31:59 -0500 Subject: [PATCH] [Security Solution][Resolver] Fixing resolver functional tests (#85647) * Fixing resolver functional tests * Import the animation constant * Only check specific nodes instead of all the ones in view * Removing check for link text * updating test description * Adding comments --- .../resolver/view/panels/event_detail.tsx | 10 +- .../view/panels/node_events_of_type.tsx | 1 + .../timeline/body/column_headers/index.tsx | 6 +- .../apps/endpoint/resolver.ts | 159 ++++------ .../page_objects/hosts_page.ts | 290 ++++++++++++------ 5 files changed, 261 insertions(+), 205 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 003182bd5f1b..3c134eb6ba51 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -8,7 +8,7 @@ /* eslint-disable react/display-name */ -import React, { memo, useMemo, Fragment } from 'react'; +import React, { memo, useMemo, Fragment, HTMLAttributes } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; @@ -246,7 +246,12 @@ function EventDetailBreadcrumbs({ panelParameters: { nodeID, eventCategory: breadcrumbEventCategory }, }); const breadcrumbs = useMemo(() => { - const crumbs = [ + const crumbs: Array< + { + text: JSX.Element | string; + 'data-test-subj'?: string; + } & HTMLAttributes + > = [ { text: i18n.translate( 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', @@ -254,6 +259,7 @@ function EventDetailBreadcrumbs({ defaultMessage: 'Events', } ), + 'data-test-subj': 'resolver:event-detail:breadcrumbs:node-list-link', ...nodesLinkNavProps, }, { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index fbfba38295ea..2f6aa2ccbaa1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -262,6 +262,7 @@ const NodeEventsInCategoryBreadcrumbs = memo(function ({ defaultMessage: 'Events', } ), + 'data-test-subj': 'resolver:node-events-in-category:breadcrumbs:node-list-link', ...nodesLinkNavProps, }, { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index e3808514856e..ddeb1331564e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -304,7 +304,11 @@ export const ColumnHeadersComponent = ({ } className={fullScreen ? FULL_SCREEN_TOGGLED_CLASS_NAME : ''} color={fullScreen ? 'ghost' : 'primary'} - data-test-subj="full-screen" + data-test-subj={ + // a full screen button gets created for timeline and for the host page + // this sets the data-test-subj for each case so that tests can differentiate between them + timelineId === TimelineId.active ? 'full-screen-active' : 'full-screen' + } iconType="fullScreen" onClick={toggleFullScreen} /> diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 1d7b2861a1a3..debde49e3587 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -12,10 +12,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const queryBar = getService('queryBar'); - // FLAKY: https://github.com/elastic/kibana/issues/85085 - describe.skip('Endpoint Event Resolver', function () { + describe('Endpoint Event Resolver', function () { before(async () => { await pageObjects.hosts.navigateToSecurityHostsPage(); await pageObjects.common.dismissBanner(); @@ -28,7 +26,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await esArchiver.load('empty_kibana'); await esArchiver.load('endpoint/resolver_tree/functions', { useCreate: true }); - await pageObjects.hosts.navigateToEventsPanel(); await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.events.file'); }); after(async () => { @@ -194,114 +191,74 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); }); - - it('Check Related Events for event.file Node', async () => { - const expectedData = [ - '17 authentication', - '1 registry', - '17 session', - '8 file', - '1 registry', - ]; - await pageObjects.hosts.runNodeEvents(expectedData); - }); }); - describe('Resolver Tree events', function () { - const expectedData = [ - '17 authentication', - '1 registry', - '17 session', - '80 registry', - '8 network', - '60 registry', - ]; + describe('node related event pills', function () { + /** + * Verifies that the pills of a node have the correct text. + * + * @param id the node ID to verify the pills for. + * @param expectedPills a map of expected pills for all nodes + */ + const verifyPills = async (id: string, expectedPills: Set) => { + const relatedEventPills = await pageObjects.hosts.findNodePills(id); + expect(relatedEventPills.length).to.equal(expectedPills.size); + for (const pill of relatedEventPills) { + const pillText = await pill._webElement.getText(); + // check that we have the pill text in our expected map + expect(expectedPills.has(pillText)).to.equal(true); + } + }; + before(async () => { await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); + await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); }); after(async () => { await pageObjects.hosts.deleteDataStreams(); }); - it('Check Related Events for event.process Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.process' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); + describe('endpoint.alerts filter', () => { + before(async () => { + await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); + await pageObjects.hosts.clickZoomOut(); + await browser.setWindowSize(2100, 1500); + }); - it('Check Related Events for event.security Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.security' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); + it('has the correct pill text', async () => { + const expectedData: Map> = new Map([ + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTc2MzYtMTMyNDc2MTQ0NDIuOTU5MTE2NjAw', + new Set(['1 library']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTMxMTYtMTMyNDcyNDk0MjQuOTg4ODI4NjAw', + new Set(['157 file', '520 registry']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTUwODQtMTMyNDc2MTQ0NDIuOTcyODQ3MjAw', + new Set(), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTg2OTYtMTMyNDc2MTQ0MjEuNjc1MzY0OTAw', + new Set(['3 file']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTcyNjAtMTMyNDc2MTQ0MjIuMjQwNDI2MTAw', + new Set(), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTczMDAtMTMyNDc2MTQ0MjEuNjg2NzI4NTAw', + new Set(), + ], + ]); - it('Check Related Events for event.registry Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.registry' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); - - it('Check Related Events for event.network Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.network' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); - - it('Check Related Events for event.library Node', async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/library_events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); - const expectedLibraryData = [ - '1 authentication', - '1 session', - '329 network', - '1 library', - '1 library', - ]; - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.library' - ); - // This lines will move the resolver view for clear visibility of the related events. - for (let i = 0; i < 7; i++) { - await (await testSubjects.find('resolver:graph-controls:west-button')).click(); - } - await pageObjects.hosts.runNodeEvents(expectedLibraryData); - }); - - it('Check Related Events for event.alert Node', async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); - const expectedAlertData = [ - '1 library', - '157 file', - '520 registry', - '3 file', - '5 library', - '5 library', - ]; - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - await browser.setWindowSize(2100, 1500); - for (let i = 0; i < 2; i++) { - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - } - await pageObjects.hosts.runNodeEvents(expectedAlertData); + for (const [id, expectedPills] of expectedData.entries()) { + // center the node in the view + await pageObjects.hosts.clickNodeLinkInPanel(id); + await verifyPills(id, expectedPills); + } + }); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts index c76a5a7c22f6..09160a6ada15 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { nudgeAnimationDuration } from '../../../plugins/security_solution/public/resolver/store/camera/scaling_constants'; import { FtrProviderContext } from '../ftr_provider_context'; -import { deleteEventsStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteAlertsStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteMetadataStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deletePolicyStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteTelemetryStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; +import { + deleteEventsStream, + deleteAlertsStream, + deleteMetadataStream, + deletePolicyStream, + deleteTelemetryStream, +} from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; + export interface DataStyle { left: string; top: string; @@ -22,6 +26,109 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro const pageObjects = getPageObjects(['common', 'header']); const testSubjects = getService('testSubjects'); const queryBar = getService('queryBar'); + const find = getService('find'); + + /** + * Returns the node IDs for the visible nodes in the resolver graph. + */ + const findVisibleNodeIDs = async (): Promise => { + const visibleNodes = await testSubjects.findAll('resolver:node'); + return Promise.all( + visibleNodes.map(async (node: WebElementWrapper) => { + return node.getAttribute('data-test-resolver-node-id'); + }) + ); + }; + + /** + * This assumes you are on the process list in the panel and will find and click the node + * with the given ID to bring it into view in the graph. + * + * @param id the ID of the node to find and click. + */ + const clickNodeLinkInPanel = async (id: string): Promise => { + await navigateToProcessListInPanel(); + const panelNodeButton = await find.byCssSelector( + `[data-test-subj='resolver:node-list:node-link'][data-test-node-id='${id}']` + ); + + await panelNodeButton?.click(); + // ensure that we wait longer than the animation time + await pageObjects.common.sleep(nudgeAnimationDuration * 2); + }; + + /** + * Finds all the pills for a particular node. + * + * @param id the ID of the node + */ + const findNodePills = async (id: string): Promise => { + return testSubjects.findAllDescendant( + 'resolver:map:node-submenu-item', + await find.byCssSelector( + `[data-test-subj='resolver:node'][data-test-resolver-node-id='${id}']` + ) + ); + }; + + /** + * Navigate back to the process list view in the panel. + */ + const navigateToProcessListInPanel = async () => { + const [ + isOnNodeListPage, + isOnCategoryPage, + isOnNodeDetailsPage, + isOnRelatedEventDetailsPage, + ] = await Promise.all([ + testSubjects.exists('resolver:node-list', { timeout: 1 }), + testSubjects.exists('resolver:node-events-in-category:breadcrumbs:node-list-link', { + timeout: 1, + }), + testSubjects.exists('resolver:node-detail:breadcrumbs:node-list-link', { timeout: 1 }), + testSubjects.exists('resolver:event-detail:breadcrumbs:node-list-link', { timeout: 1 }), + ]); + + if (isOnNodeListPage) { + return; + } else if (isOnCategoryPage) { + await ( + await testSubjects.find('resolver:node-events-in-category:breadcrumbs:node-list-link') + ).click(); + } else if (isOnNodeDetailsPage) { + await (await testSubjects.find('resolver:node-detail:breadcrumbs:node-list-link')).click(); + } else if (isOnRelatedEventDetailsPage) { + await (await testSubjects.find('resolver:event-detail:breadcrumbs:node-list-link')).click(); + } else { + // unknown page + return; + } + + await pageObjects.common.sleep(100); + }; + + /** + * Click the zoom out control. + */ + const clickZoomOut = async () => { + await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); + }; + + /** + * Navigate to Events Panel + */ + const navigateToEventsPanel = async () => { + const isFullScreen = await testSubjects.exists('exit-full-screen', { timeout: 400 }); + if (isFullScreen) { + await (await testSubjects.find('exit-full-screen')).click(); + } + + if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { + await (await testSubjects.find('navigation-hosts')).click(); + await testSubjects.click('navigation-events'); + await testSubjects.existOrFail('event'); + } + }; /** * @function parseStyles @@ -54,101 +161,82 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro }), {} ); - return { - /** - * Navigate to the Security Hosts page - */ - async navigateToSecurityHostsPage() { - await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); - await pageObjects.header.waitUntilLoadingHasFinished(); - }, - /** - * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. - * It uses euiTableCellContent to avoid poluting the array data with the euiTableRowCell__mobileHeader data. - * @param dataTestSubj - * @param element - * @returns Promise - */ - async getEndpointEventResolverNodeData(dataTestSubj: string, element: string) { - await testSubjects.exists(dataTestSubj); - const Elements = await testSubjects.findAll(dataTestSubj); - const $ = []; - for (const value of Elements) { - $.push(await value.getAttribute(element)); - } - return $; - }, - /** - * Gets a array of not parsed styles and returns the Array of parsed styles. - * @returns Promise - */ - async parseStyles() { - const tableData = await this.getEndpointEventResolverNodeData('resolver:node', 'style'); - const styles: DataStyle[] = []; - for (let i = 1; i < tableData.length; i++) { - const eachStyle = parseStyle(tableData[i]); - styles.push({ - top: eachStyle.top ?? '', - height: eachStyle.height ?? '', - left: eachStyle.left ?? '', - width: eachStyle.width ?? '', - }); - } - return styles; - }, - /** - * Deletes DataStreams from Index Management. - */ - async deleteDataStreams() { - await deleteEventsStream(getService); - await deleteAlertsStream(getService); - await deletePolicyStream(getService); - await deleteMetadataStream(getService); - await deleteTelemetryStream(getService); - }, - /** - * Runs Nodes Events - */ - async runNodeEvents(expectedData: string[]) { - await testSubjects.exists('resolver:submenu:button', { timeout: 400 }); - const NodeSubmenuButtons = await testSubjects.findAll('resolver:submenu:button'); - for (let b = 0; b < NodeSubmenuButtons.length; b++) { - await (await testSubjects.findAll('resolver:submenu:button'))[b].click(); - } - await testSubjects.exists('resolver:map:node-submenu-item', { timeout: 400 }); - const NodeSubmenuItems = await testSubjects.findAll('resolver:map:node-submenu-item'); - for (let i = 0; i < NodeSubmenuItems.length; i++) { - await (await testSubjects.findAll('resolver:map:node-submenu-item'))[i].click(); - const Events = await testSubjects.findAll('resolver:map:node-submenu-item'); - // this sleep is for the AMP enabled run - await pageObjects.common.sleep(300); - const EventName = await Events[i]._webElement.getText(); - const LinkText = await testSubjects.find('resolver:breadcrumbs:last'); - const linkText = await LinkText._webElement.getText(); - expect(EventName).to.equal(linkText); - expect(EventName).to.equal(expectedData[i]); - } - await testSubjects.click('full-screen'); - }, - /** - * Navigate to Events Panel - */ - async navigateToEventsPanel() { - if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { - await (await testSubjects.find('navigation-hosts')).click(); - await testSubjects.click('navigation-events'); - await testSubjects.existOrFail('event'); - } - }, - /** - * execute Query And Open Resolver - */ - async executeQueryAndOpenResolver(query: string) { - await queryBar.setQuery(query); - await queryBar.submitQuery(); - await testSubjects.click('full-screen'); - await testSubjects.click('investigate-in-resolver-button'); - }, + /** + * Navigate to the Security Hosts page + */ + const navigateToSecurityHostsPage = async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }; + + /** + * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. + * It uses euiTableCellContent to avoid polluting the array data with the euiTableRowCell__mobileHeader data. + * @param dataTestSubj + * @param element + * @returns Promise + */ + const getEndpointEventResolverNodeData = async (dataTestSubj: string, element: string) => { + await testSubjects.exists(dataTestSubj); + const Elements = await testSubjects.findAll(dataTestSubj); + const $ = []; + for (const value of Elements) { + $.push(await value.getAttribute(element)); + } + return $; + }; + + /** + * Gets a array of not parsed styles and returns the Array of parsed styles. + * @returns Promise + */ + const parseStyles = async () => { + const tableData = await getEndpointEventResolverNodeData('resolver:node', 'style'); + const styles: DataStyle[] = []; + for (let i = 1; i < tableData.length; i++) { + const eachStyle = parseStyle(tableData[i]); + styles.push({ + top: eachStyle.top ?? '', + height: eachStyle.height ?? '', + left: eachStyle.left ?? '', + width: eachStyle.width ?? '', + }); + } + return styles; + }; + /** + * Deletes DataStreams from Index Management. + */ + const deleteDataStreams = async () => { + await deleteEventsStream(getService); + await deleteAlertsStream(getService); + await deletePolicyStream(getService); + await deleteMetadataStream(getService); + await deleteTelemetryStream(getService); + }; + + /** + * execute Query And Open Resolver + */ + const executeQueryAndOpenResolver = async (query: string) => { + await navigateToEventsPanel(); + await queryBar.setQuery(query); + await queryBar.submitQuery(); + await testSubjects.click('full-screen'); + await testSubjects.click('investigate-in-resolver-button'); + }; + + return { + navigateToProcessListInPanel, + findNodePills, + clickNodeLinkInPanel, + findVisibleNodeIDs, + clickZoomOut, + navigateToEventsPanel, + navigateToSecurityHostsPage, + parseStyles, + deleteDataStreams, + executeQueryAndOpenResolver, }; }