From 24ac6d29317526152de3e189557f816f5ffd2f5b Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jan 2020 11:32:36 +0100 Subject: [PATCH 01/16] [SIEM] Inspect readable (#56110) * extracts methods to tasks * uses cypress api for assertions * refactor * Inspect refactor * fixes rebase issue --- .../smoke_tests/inspect/inspect.spec.ts | 25 +++--- .../plugins/siem/cypress/screens/inspect.ts | 87 +++++++++++++++++++ .../siem/cypress/screens/timeline/main.ts | 4 + .../plugins/siem/cypress/tasks/inspect.ts | 29 +++++++ .../siem/cypress/tasks/timeline/main.ts | 15 ++++ 5 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/inspect.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts index 7b0b10831c4a0..e7411aba11af5 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts @@ -9,12 +9,15 @@ import { INSPECT_MODAL, INSPECT_NETWORK_BUTTONS_IN_SIEM, INSPECT_HOSTS_BUTTONS_IN_SIEM, - TIMELINE_SETTINGS_ICON, - TIMELINE_INSPECT_BUTTON, -} from '../../lib/inspect/selectors'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; -import { executeKQL, hostExistsQuery, toggleTimelineVisibility } from '../../lib/timeline/helpers'; -import { closesModal, openStatsAndTables } from '../../lib/inspect/helpers'; +} from '../../../screens/inspect'; +import { + executeTimelineKQL, + openTimeline, + openTimelineSettings, + openTimelineInspectButton, +} from '../../../tasks/timeline/main'; +import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; +import { closesModal, openStatsAndTables } from '../../../tasks/inspect'; describe('Inspect', () => { context('Hosts stats and tables', () => { @@ -51,12 +54,12 @@ describe('Inspect', () => { context('Timeline', () => { it('inspects the timeline', () => { + const hostExistsQuery = 'host.name: *'; loginAndWaitForPage(HOSTS_PAGE); - toggleTimelineVisibility(); - executeKQL(hostExistsQuery); - cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); - cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); - cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); + openTimeline(); + executeTimelineKQL(hostExistsQuery); + openTimelineSettings(); + openTimelineInspectButton(); cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('be.visible'); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/inspect.ts b/x-pack/legacy/plugins/siem/cypress/screens/inspect.ts new file mode 100644 index 0000000000000..8f2ff4ef401e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/inspect.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const INSPECT_BUTTON_ICON = '[data-test-subj="inspect-icon-button"]'; +export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; + +export interface InspectButtonMetadata { + altInspectId?: string; + id: string; + title: string; + tabId?: string; +} + +export const INSPECT_HOSTS_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ + { + id: '[data-test-subj="stat-hosts"]', + title: 'Hosts Stat', + }, + { + id: '[data-test-subj="stat-authentication"]', + title: 'User Authentications Stat', + }, + { + id: '[data-test-subj="stat-uniqueIps"]', + title: 'Unique IPs Stat', + }, + { + id: '[data-test-subj="table-allHosts-loading-false"]', + title: 'All Hosts Table', + tabId: '[data-test-subj="navigation-allHosts"]', + }, + { + id: '[data-test-subj="table-authentications-loading-false"]', + title: 'Authentications Table', + tabId: '[data-test-subj="navigation-authentications"]', + }, + { + id: '[data-test-subj="table-uncommonProcesses-loading-false"]', + title: 'Uncommon processes Table', + tabId: '[data-test-subj="navigation-uncommonProcesses"]', + }, + { + altInspectId: `[data-test-subj="events-viewer-panel"] ${INSPECT_BUTTON_ICON}`, + id: '[data-test-subj="events-container-loading-false"]', + title: 'Events Table', + tabId: '[data-test-subj="navigation-events"]', + }, +]; + +export const INSPECT_NETWORK_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ + { + id: '[data-test-subj="stat-networkEvents"]', + title: 'Network events Stat', + }, + { + id: '[data-test-subj="stat-dnsQueries"]', + title: 'DNS queries Stat', + }, + { + id: '[data-test-subj="stat-uniqueFlowId"]', + title: 'Unique flow IDs Stat', + }, + { + id: '[data-test-subj="stat-tlsHandshakes"]', + title: 'TLS handshakes Stat', + }, + { + id: '[data-test-subj="stat-UniqueIps"]', + title: 'Unique private IPs Stat', + }, + { + id: '[data-test-subj="table-topNFlowSource-loading-false"]', + title: 'Source IPs Table', + }, + { + id: '[data-test-subj="table-topNFlowDestination-loading-false"]', + title: 'Destination IPs Table', + }, + { + id: '[data-test-subj="table-dns-loading-false"]', + title: 'Top DNS Domains Table', + tabId: '[data-test-subj="navigation-dns"]', + }, +]; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts index cf3267d2b650e..ca11f48932263 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts @@ -16,3 +16,7 @@ export const TIMELINE_FIELDS_BUTTON = /** The total server-side count of the events matching the timeline's search criteria */ export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; + +export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; + +export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts b/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts new file mode 100644 index 0000000000000..26b1c0f7e4e39 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { INSPECT_BUTTON_ICON, InspectButtonMetadata } from '../screens/inspect'; + +import { DEFAULT_TIMEOUT } from '../tasks/login'; + +export const closesModal = () => { + cy.get('[data-test-subj="modal-inspect-close"]', { timeout: DEFAULT_TIMEOUT }).click(); +}; + +export const openStatsAndTables = (table: InspectButtonMetadata) => { + if (table.tabId) { + cy.get(table.tabId).click({ force: true }); + } + cy.get(table.id, { timeout: DEFAULT_TIMEOUT }); + if (table.altInspectId) { + cy.get(table.altInspectId, { timeout: DEFAULT_TIMEOUT }).trigger('click', { + force: true, + }); + } else { + cy.get(`${table.id} ${INSPECT_BUTTON_ICON}`, { + timeout: DEFAULT_TIMEOUT, + }).trigger('click', { force: true }); + } +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts index 51026fef757d8..ae2a863092907 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts @@ -11,6 +11,8 @@ import { SEARCH_OR_FILTER_CONTAINER, TIMELINE_FIELDS_BUTTON, SERVER_SIDE_EVENT_COUNT, + TIMELINE_SETTINGS_ICON, + TIMELINE_INSPECT_BUTTON, } from '../../screens/timeline/main'; export const hostExistsQuery = 'host.name: *'; @@ -29,3 +31,16 @@ export const populateTimeline = () => { export const openTimelineFieldsBrowser = () => { cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); }; + +export const executeTimelineKQL = (query: string) => { + cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`); +}; + +export const openTimelineSettings = () => { + cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); +}; + +export const openTimelineInspectButton = () => { + cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); + cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); +}; From 4d43639f0e142bf7f58f3b805863d31fe2d15839 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 28 Jan 2020 13:43:27 +0300 Subject: [PATCH 02/16] Move tests in dashboard into appropriate folders (#55304) * Move tests in dashboard into appropriate folders * Remove unused imports --- .../get_embeddable_factories_mock.ts | 26 ------------------- .../dashboard_empty_screen.test.tsx.snap | 0 .../dashboard_empty_screen.test.tsx | 5 +--- .../np_ready/dashboard_state.test.ts | 2 +- .../test_utils}/get_saved_dashboard_mock.ts | 5 ++-- .../test_utils}/index.ts | 1 - .../url_helper.test.ts | 2 +- 7 files changed, 5 insertions(+), 36 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts rename src/legacy/core_plugins/kibana/public/dashboard/{__tests__ => np_ready}/__snapshots__/dashboard_empty_screen.test.tsx.snap (100%) rename src/legacy/core_plugins/kibana/public/dashboard/{__tests__ => np_ready}/dashboard_empty_screen.test.tsx (96%) rename src/legacy/core_plugins/kibana/public/dashboard/{__tests__ => np_ready/test_utils}/get_saved_dashboard_mock.ts (85%) rename src/legacy/core_plugins/kibana/public/dashboard/{__tests__ => np_ready/test_utils}/index.ts (91%) rename src/legacy/core_plugins/kibana/public/dashboard/{__tests__ => np_ready}/url_helper.test.ts (99%) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts deleted file mode 100644 index 357ab307c3f12..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_embeddable_factories_mock.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* global jest */ -export function getEmbeddableFactoryMock(config?: any) { - const embeddableFactoryMockDefaults = { - create: jest.fn(() => Promise.resolve({})), - }; - return Object.assign(embeddableFactoryMockDefaults, config); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_empty_screen.test.tsx similarity index 96% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_empty_screen.test.tsx index 347502c2560ab..d5e22798b4f24 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_empty_screen.test.tsx @@ -18,10 +18,7 @@ */ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { - DashboardEmptyScreen, - DashboardEmptyScreenProps, -} from '../np_ready/dashboard_empty_screen'; +import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { coreMock } from '../../../../../../core/public/mocks'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 152cd84b7c38d..60ea14dad19e1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -20,7 +20,7 @@ import './np_core.test.mocks'; import { createBrowserHistory } from 'history'; import { DashboardStateManager } from './dashboard_state_manager'; -import { getSavedDashboardMock } from '../__tests__'; +import { getSavedDashboardMock } from './test_utils'; import { InputTimeRange, TimefilterContract, TimeRange } from 'src/plugins/data/public'; import { ViewMode } from 'src/plugins/embeddable/public'; import { createKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/get_saved_dashboard_mock.ts similarity index 85% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/get_saved_dashboard_mock.ts index baf5bad510ce1..d5e61936f67cf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/get_saved_dashboard_mock.ts @@ -17,9 +17,8 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { searchSourceMock } from '../../../../../../plugins/data/public/search/search_source/mocks'; -import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; +import { searchSourceMock } from '../../../../../../../plugins/data/public/search/search_source/mocks'; +import { SavedObjectDashboard } from '../../saved_dashboard/saved_dashboard'; export function getSavedDashboardMock( config?: Partial diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/index.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/index.ts index 2b992f95695f3..a9a306da7f1a2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/test_utils/index.ts @@ -18,4 +18,3 @@ */ export { getSavedDashboardMock } from './get_saved_dashboard_mock'; -export { getEmbeddableFactoryMock } from './get_embeddable_factories_mock'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts similarity index 99% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts index df2dbfd54c130..60ca1b39d29d6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts @@ -47,7 +47,7 @@ import { addEmbeddableToDashboardUrl, getLensUrlFromDashboardAbsoluteUrl, getUrlVars, -} from '../np_ready/url_helper'; +} from './url_helper'; describe('Dashboard URL Helper', () => { beforeEach(() => { From 5e9db02e926e570c57d650eafe015143683e7c20 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jan 2020 13:19:40 +0100 Subject: [PATCH 03/16] refactor (#56121) --- .../ml_conditional_links.spec.ts | 22 +++--- .../plugins/siem/cypress/screens/header.ts | 7 ++ .../siem/cypress/urls/ml_conditional_links.ts | 76 +++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/header.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 142729189e49b..fabd86cf51f91 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -18,14 +18,14 @@ import { mlHostMultiHostKqlQuery, mlHostVariableHostNullKqlQuery, mlHostVariableHostKqlQuery, -} from '../../lib/ml_conditional_links'; -import { loginAndWaitForPage } from '../../lib/util/helpers'; -import { KQL_INPUT } from '../../lib/url_state'; +} from '../../../urls/ml_conditional_links'; +import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../../tasks/login'; +import { KQL_INPUT } from '../../../screens/header'; describe('ml conditional links', () => { it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -34,7 +34,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple IPs with a null for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))' @@ -43,7 +43,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple IPs with a value for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))' @@ -52,7 +52,7 @@ describe('ml conditional links', () => { it('sets the KQL from a $ip$ with a value for the query', () => { loginAndWaitForPage(mlNetworkKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -61,7 +61,7 @@ describe('ml conditional links', () => { it('sets the KQL from a single host name with a value for query', () => { loginAndWaitForPage(mlHostSingleHostKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -70,7 +70,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple host names with null for query', () => { loginAndWaitForPage(mlHostMultiHostNullKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(host.name: "siem-windows" or host.name: "siem-suricata")' @@ -79,7 +79,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple host names with a value for query', () => { loginAndWaitForPage(mlHostMultiHostKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))' @@ -88,7 +88,7 @@ describe('ml conditional links', () => { it('sets the KQL from a undefined/null host name but with a value for query', () => { loginAndWaitForPage(mlHostVariableHostKqlQuery); - cy.get(KQL_INPUT, { timeout: 5000 }).should( + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' diff --git a/x-pack/legacy/plugins/siem/cypress/screens/header.ts b/x-pack/legacy/plugins/siem/cypress/screens/header.ts new file mode 100644 index 0000000000000..cb018cda8f68d --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/header.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const KQL_INPUT = '[data-test-subj="queryInput"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts b/x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts new file mode 100644 index 0000000000000..655418fc98bf8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * These links are for different test scenarios that try and capture different drill downs into + * ml-network and ml-hosts and are of the flavor of testing: + * A filter being null: (query:!n) + * A filter being set with single values: query=(query:%27process.name%20:%20%22conhost.exe%22%27,language:kuery) + * A filter being set with multiple values: query=(query:%27process.name%20:%20%22conhost.exe,sc.exe%22%27,language:kuery) + * A filter containing variables not replaced: query=(query:%27process.name%20:%20%$process.name$%22%27,language:kuery) + * + * In different combination with: + * network not being set: $ip$ + * host not being set: $host.name$ + * ...or... + * network being set normally: 127.0.0.1 + * host being set normally: suricata-iowa + * ...or... + * network having multiple values: 127.0.0.1,127.0.0.2 + * host having multiple values: suricata-iowa,siem-windows + */ + +// Single IP with a null for the Query: +export const mlNetworkSingleIpNullKqlQuery = + "/app/siem#/ml-network/ip/127.0.0.1?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Single IP with a value for the Query: +export const mlNetworkSingleIpKqlQuery = + "/app/siem#/ml-network/ip/127.0.0.1?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Multiple IPs with a null for the Query: +export const mlNetworkMultipleIpNullKqlQuery = + "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Multiple IPs with a value for the Query: +export const mlNetworkMultipleIpKqlQuery = + "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// $ip$ with a null Query: +export const mlNetworkNullKqlQuery = + "/app/siem#/ml-network/ip/$ip$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// $ip$ with a value for the Query: +export const mlNetworkKqlQuery = + "/app/siem#/ml-network/ip/$ip$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Single host name with a null for the Query: +export const mlHostSingleHostNullKqlQuery = + "/app/siem#/ml-hosts/siem-windows?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Single host name with a variable in the Query: +export const mlHostSingleHostKqlQueryVariable = + "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22$process.name$%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Single host name with a value for Query: +export const mlHostSingleHostKqlQuery = + "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Multiple host names with null for Query: +export const mlHostMultiHostNullKqlQuery = + "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=!n&&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Multiple host names with a value for Query: +export const mlHostMultiHostKqlQuery = + "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Undefined/null host name with a null for the KQL: +export const mlHostVariableHostNullKqlQuery = + "/app/siem#/ml-hosts/$host.name$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Undefined/null host name but with a value for Query: +export const mlHostVariableHostKqlQuery = + "/app/siem#/ml-hosts/$host.name$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; From a2a0f118d6cfc82faba3cb9949446c212f9b312b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 28 Jan 2020 05:50:30 -0700 Subject: [PATCH 04/16] [Uptime] Add timeout for slow process to skipped functional tests (#56065) * Reorder assertions in functional tests. * Introduce retry to functional tests. --- .../test/functional/apps/uptime/overview.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 2ef6a381a6a30..73b91a61196bf 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default ({ getPageObjects }: FtrProviderContext) => { - // TODO: add UI functional tests +export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['uptime']); + const retry = getService('retry'); describe('overview page', function() { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; @@ -84,16 +84,18 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); }); - // Flakey, see https://github.com/elastic/kibana/issues/54541 - describe.skip('snapshot counts', () => { + describe('snapshot counts', () => { it('updates the snapshot count when status filter is set to down', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange( DEFAULT_DATE_START, DEFAULT_DATE_END ); await pageObjects.uptime.setStatusFilter('down'); - const counts = await pageObjects.uptime.getSnapshotCount(); - expect(counts).to.eql({ up: '0', down: '7' }); + + await retry.tryForTime(12000, async () => { + const counts = await pageObjects.uptime.getSnapshotCount(); + expect(counts).to.eql({ up: '0', down: '7' }); + }); }); it('updates the snapshot count when status filter is set to up', async () => { @@ -102,8 +104,10 @@ export default ({ getPageObjects }: FtrProviderContext) => { DEFAULT_DATE_END ); await pageObjects.uptime.setStatusFilter('up'); - const counts = await pageObjects.uptime.getSnapshotCount(); - expect(counts).to.eql({ up: '93', down: '0' }); + await retry.tryForTime(12000, async () => { + const counts = await pageObjects.uptime.getSnapshotCount(); + expect(counts).to.eql({ up: '93', down: '0' }); + }); }); }); }); From 885f315623937315281c4d87572fe6aa3035c933 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 28 Jan 2020 13:56:39 +0100 Subject: [PATCH 05/16] [ML] Single Metric Viewer: Fix brush update on short recent timespans. (#56125) Fixes an issue where the context chart brush would render incorrectly for short recent time spans (e.g. 'now-15min`). Adds a check whether to display the brush and hide it if context and focus chart have the same timespan. --- .../components/timeseries_chart/timeseries_chart.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 7ee1c64e189a7..474b4f2470bde 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -432,6 +432,9 @@ const TimeseriesChartIntl = injectI18n( } focusLoadTo = Math.min(focusLoadTo, contextXMax); + const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; + this.setBrushVisibility(brushVisibility); + if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); const newSelectedBounds = { From a831710c6d865915915397d3edf98411503ce8e5 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 28 Jan 2020 14:21:43 +0100 Subject: [PATCH 06/16] [NP] add HTTP resources testing strategies (#54908) * add HTTP resources testing strategies * address comments * add error message test and update error test * Apply suggestions from code review Co-Authored-By: Rudolf Meijering * add controller testing example Co-authored-by: Rudolf Meijering --- src/core/TESTING.md | 324 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 316 insertions(+), 8 deletions(-) diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 467110b3874b8..23c0879e4411e 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -41,7 +41,7 @@ If the unit under test expects a particular response from a Core API, the test w #### Example -```ts +```typescript import { elasticsearchServiceMock } from 'src/core/server/mocks'; test('my test', async () => { @@ -59,11 +59,319 @@ test('my test', async () => { }); ``` -### Strategies for specific Core APIs +## Strategies for specific Core APIs -#### HTTP Routes +### HTTP Routes +The HTTP API interface is another public contract of Kibana, although not every Kibana endpoint is for external use. When evaluating the required level of test coverage for an HTTP resource, make your judgment based on whether an endpoint is considered to be public or private. Public API is expected to have a higher level of test coverage. +Public API tests should cover the **observable behavior** of the system, therefore they should be close to the real user interactions as much as possible, ideally by using HTTP requests to communicate with the Kibana server as a real user would do. -_How to test route handlers_ +##### Preconditions +We are going to add tests for `myPlugin` plugin that allows to format user-provided text, store and retrieve it later. +The plugin has *thin* route controllers isolating all the network layer dependencies and delegating all the logic to the plugin model. + +```typescript +class TextFormatter { + public static async format(text: string, sanitizer: Deps['sanitizer']) { + // sanitizer.sanitize throws MisformedTextError when passed text contains HTML markup + const sanitizedText = await sanitizer.sanitize(text); + return sanitizedText; + } + + public static async save(text: string, savedObjectsClient: SavedObjectsClient) { + const { id } = await savedObjectsClient.update('myPlugin-type', 'myPlugin', { + userText: text + }); + return { id }; + } + + public static async getById(id: string, savedObjectsClient: SavedObjectsClient) { + const { attributes } = await savedObjectsClient.get('myPlugin-type', id); + return { text: attributes.userText }; + } +} +router.get( + { + path: '/myPlugin/formatter', + validate: { + query: schema.object({ + text: schema.string({ maxLength: 100 }), + }), + }, + }, + async (context, request, response) => { + try { + const formattedText = await TextFormatter.format(request.query.text, deps.sanitizer); + return response.ok({ body: formattedText }); + } catch(error) { + if (error instanceof MisformedTextError) { + return response.badRequest({ body: error.message }) + } + + throw e; + } + } +); +router.post( + { + path: '/myPlugin/formatter/text', + validate: { + body: schema.object({ + text: schema.string({ maxLength: 100 }), + }), + }, + }, + async (context, request, response) => { + try { + const { id } = await TextFormatter.save(request.query.text, context.core.savedObjects.client); + return response.ok({ body: { id } }); + } catch(error) { + if (SavedObjectsErrorHelpers.isConflictError(error)) { + return response.conflict({ body: error.message }) + } + throw e; + } + } +); + +router.get( + { + path: '/myPlugin/formatter/text/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const { text } = await TextFormatter.getById(request.params.id, context.core.savedObjects.client); + return response.ok({ + body: text + }); + } catch(error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return response.notFound() + } + throw e; + } + } +); +``` + +#### Unit testing +Unit tests provide the simplest and fastest way to test the logic in your route controllers and plugin models. +Use them whenever adding an integration test is hard and slow due to complex setup or the number of logic permutations. +Since all external core and plugin dependencies are mocked, you don't have the guarantee that the whole system works as +expected. +Pros: +- fast +- easier to debug + +Cons: +- doesn't test against real dependencies +- doesn't cover integration with other plugins + +###### Example +You can leverage existing unit-test infrastructure for this. You should add `*.test.ts` file and use dependencies mocks to cover the functionality with a broader test suit that covers: +- input permutations +- input edge cases +- expected exception +- interaction with dependencies +```typescript +// src/plugins/my_plugin/server/formatter.test.ts +describe('TextFormatter', () => { + describe('format()', () => { + const sanitizer = sanitizerMock.createSetup(); + sanitizer.sanitize.mockImplementation((input: string) => `sanitizer result:${input}`); + + it('formats text to a ... format', async () => { + expect(await TextFormatter.format('aaa', sanitizer)).toBe('...'); + }); + + it('calls Sanitizer.sanitize with correct arguments', async () => { + await TextFormatter.format('aaa', sanitizer); + expect(sanitizer.sanitize).toHaveBeenCalledTimes(1); + expect(sanitizer.sanitize).toHaveBeenCalledWith('aaa'); + }); + + it('throws MisformedTextError if passed string contains banned symbols', async () => { + sanitizer.sanitize.mockRejectedValueOnce(new MisformedTextError()); + await expect(TextFormatter.format('any', sanitizer)).rejects.toThrow(MisformedTextError); + }); + // ... other tests + }); +}); +``` + +#### Integration tests +Depending on the number of external dependencies, you can consider implementing several high-level integration tests. +They would work as a set of [smoke tests](https://en.wikipedia.org/wiki/Smoke_testing_(software)) for the most important functionality. +Main subjects for tests should be: +- authenticated / unauthenticated access to an endpoint. +- endpoint validation (params, query, body). +- main business logic. +- dependencies on other plugins. + +##### Functional Test Runner +If your plugin relies on the elasticsearch server to store data and supports additional configuration, you can leverage the Functional Test Runner(FTR) to implement integration tests. +FTR bootstraps an elasticsearch and a Kibana instance and runs the test suite against it. +Pros: +- runs the whole Elastic stack +- tests cross-plugin integration +- emulates a real user interaction with the stack +- allows adjusting config values + +Cons: +- slow start +- hard to debug +- brittle tests + +###### Example +You can reuse existing [api_integration](/test/api_integration/config.js) setup by registering a test file within a [test loader](/test/api_integration/apis/index.js). More about the existing FTR setup in the [contribution guide](/CONTRIBUTING.md#running-specific-kibana-tests) + +The tests cover: +- authenticated / non-authenticated user access (when applicable) +```typescript +// TODO after https://github.com/elastic/kibana/pull/53208/ +``` +- request validation +```typescript +// test/api_integration/apis/my_plugin/something.ts +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('myPlugin', () => { + it('validate params before to store text', async () => { + const response = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'aaa'.repeat(100) }) + .expect(400); + + expect(response.body).to.have.property('message'); + expect(response.body.message).to.contain('must have a maximum length of [100]'); + }); + }); +``` +- the main logic of the plugin +```typescript +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('myPlugin', () => { + it('stores text', async () => { + const response = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'aaa' }) + .expect(200); + + expect(response.body).to.have.property('id'); + expect(response.body.id).to.be.a('string'); + }); + + it('retrieves text', async () => { + const { body } = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'bbb' }) + .expect(200); + + const response = await supertest.get(`/myPlugin/formatter/text/${body.id}`).expect(200); + expect(response.text).be('bbb'); + }); + + it('returns NotFound error when cannot find a text', async () => { + await supertest + .get('/myPlugin/something/missing') + .expect(404, 'Saved object [myPlugin-type/missing] not found'); + }); + }); +``` + +##### TestUtils +It can be utilized if your plugin doesn't interact with the elasticsearch server or mocks the own methods doing so. +Runs tests against real Kibana server instance. +Pros: +- runs the real Kibana instance +- tests cross-plugin integration +- emulates a real user interaction with the HTTP resources + +Cons: +- faster than FTR because it doesn't run elasticsearch instance, but still slow +- hard to debug +- doesn't cover Kibana CLI logic + +###### Example +To have access to Kibana TestUtils, you should create `integration_tests` folder and import `test_utils` within a test file: +```typescript +// src/plugins/my_plugin/server/integration_tests/formatter.test.ts +import * as kbnTestServer from 'src/test_utils/kbn_server'; + +describe('myPlugin', () => { + describe('GET /myPlugin/formatter', () => { + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + it('validates given text', async () => { + const response = await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: 'input string'.repeat(100) }) + .expect(400); + + expect(response.body).toHaveProperty('message'); + }); + + it('formats given text', async () => { + const response = await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: 'input string' }) + .expect(200); + + expect(response.text).toBe('...'); + }); + + it('returns BadRequest if passed string contains banned symbols', async () => { + await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: '