From 453e22e3185d7b9c887a2e36ce4f07de7fe94e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Alvarez=20Pi=C3=B1eiro?= <95703246+emilioalvap@users.noreply.github.com> Date: Fri, 11 Feb 2022 09:45:23 +0100 Subject: [PATCH] [Uptime] Fix flaky monitor details tests (#124700) * Rewrite flaky tests to e2e pipeline, add data initialization helpers, add monitor details page object --- .../plugins/uptime/e2e/helpers/make_checks.ts | 182 ++++++++++++++++++ .../plugins/uptime/e2e/helpers/make_ping.ts | 126 ++++++++++++ x-pack/plugins/uptime/e2e/helpers/make_tls.ts | 68 +++++++ .../e2e/journeys/data_view_permissions.ts | 3 +- x-pack/plugins/uptime/e2e/journeys/index.ts | 1 + .../e2e/journeys/monitor_details/index.ts | 10 + .../monitor_details/monitor_alerts.journey.ts | 94 +++++++++ .../monitor_details.journey.ts | 60 ++++++ .../monitor_details/ping_redirects.journey.ts | 73 +++++++ .../journeys/monitor_management.journey.ts | 3 + .../uptime/e2e/journeys/uptime.journey.ts | 6 +- x-pack/plugins/uptime/e2e/journeys/utils.ts | 8 + .../e2e/page_objects/monitor_details.tsx | 129 +++++++++++++ .../e2e/page_objects/monitor_management.tsx | 7 +- x-pack/plugins/uptime/e2e/playwright_start.ts | 22 +-- .../components/monitor/ml/manage_ml_job.tsx | 2 +- x-pack/test/functional/apps/uptime/index.ts | 5 - x-pack/test/functional/apps/uptime/monitor.ts | 60 ------ .../functional/apps/uptime/ping_redirects.ts | 76 -------- .../apps/uptime/anomaly_alert.ts | 134 ------------- .../apps/uptime/index.ts | 1 - 21 files changed, 765 insertions(+), 305 deletions(-) create mode 100644 x-pack/plugins/uptime/e2e/helpers/make_checks.ts create mode 100644 x-pack/plugins/uptime/e2e/helpers/make_ping.ts create mode 100644 x-pack/plugins/uptime/e2e/helpers/make_tls.ts create mode 100644 x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts create mode 100644 x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts create mode 100644 x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts create mode 100644 x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts create mode 100644 x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx delete mode 100644 x-pack/test/functional/apps/uptime/monitor.ts delete mode 100644 x-pack/test/functional/apps/uptime/ping_redirects.ts delete mode 100644 x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts diff --git a/x-pack/plugins/uptime/e2e/helpers/make_checks.ts b/x-pack/plugins/uptime/e2e/helpers/make_checks.ts new file mode 100644 index 0000000000000..b9e913524cb1f --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_checks.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { merge, flattenDeep } from 'lodash'; +import type { Client } from '@elastic/elasticsearch'; +import { makePing } from './make_ping'; +import { TlsProps } from './make_tls'; + +interface CheckProps { + es: Client; + monitorId?: string; + numIps?: number; + fields?: { [key: string]: any }; + mogrify?: (doc: any) => any; + refresh?: boolean; + tls?: boolean | TlsProps; + isFleetManaged?: boolean; +} + +const getRandomMonitorId = () => { + return 'monitor-' + Math.random().toString(36).substring(7); +}; +export const makeCheck = async ({ + es, + monitorId = getRandomMonitorId(), + numIps = 1, + fields = {}, + mogrify = (d) => d, + refresh = true, + tls = false, + isFleetManaged = false, +}: CheckProps): Promise<{ monitorId: string; docs: any }> => { + const cgFields = { + monitor: { + check_group: uuid.v4(), + }, + }; + + const docs = []; + const summary = { + up: 0, + down: 0, + }; + for (let i = 0; i < numIps; i++) { + const pingFields = merge(fields, cgFields, { + monitor: { + ip: `127.0.0.${i}`, + }, + }); + if (i === numIps - 1) { + pingFields.summary = summary; + } + const doc = await makePing( + es, + monitorId, + pingFields, + mogrify, + false, + tls as any, + isFleetManaged + ); + docs.push(doc); + // @ts-ignore + summary[doc.monitor.status]++; + } + + if (refresh) { + await es.indices.refresh(); + } + + return { monitorId, docs }; +}; + +export const makeChecks = async ( + es: Client, + monitorId: string, + numChecks: number = 1, + numIps: number = 1, + every: number = 10000, // number of millis between checks + fields: { [key: string]: any } = {}, + mogrify: (doc: any) => any = (d) => d, + refresh: boolean = true, + isFleetManaged: boolean = false +) => { + const checks = []; + const oldestTime = new Date().getTime() - numChecks * every; + let newestTime = oldestTime; + for (let li = 0; li < numChecks; li++) { + const checkDate = new Date(newestTime + every); + newestTime = checkDate.getTime() + every; + fields = merge(fields, { + '@timestamp': checkDate.toISOString(), + monitor: { + timespan: { + gte: checkDate.toISOString(), + lt: new Date(newestTime).toISOString(), + }, + }, + }); + const { docs } = await makeCheck({ + es, + monitorId, + numIps, + fields, + mogrify, + refresh: false, + isFleetManaged, + }); + checks.push(docs); + } + + if (refresh) { + await es.indices.refresh(); + } + + return checks; +}; + +export const makeChecksWithStatus = async ( + es: Client, + monitorId: string, + numChecks: number, + numIps: number, + every: number, + fields: { [key: string]: any } = {}, + status: 'up' | 'down', + mogrify: (doc: any) => any = (d) => d, + refresh: boolean = true, + isFleetManaged: boolean = false +) => { + const oppositeStatus = status === 'up' ? 'down' : 'up'; + + return await makeChecks( + es, + monitorId, + numChecks, + numIps, + every, + fields, + (d) => { + d.monitor.status = status; + if (d.summary) { + d.summary[status] += d.summary[oppositeStatus]; + d.summary[oppositeStatus] = 0; + } + + return mogrify(d); + }, + refresh, + isFleetManaged + ); +}; + +// Helper for processing a list of checks to find the time picker bounds. +export const getChecksDateRange = (checks: any[]) => { + // Flatten 2d arrays + const flattened = flattenDeep(checks); + + let startTime = 1 / 0; + let endTime = -1 / 0; + flattened.forEach((c) => { + const ts = Date.parse(c['@timestamp']); + + if (ts < startTime) { + startTime = ts; + } + + if (ts > endTime) { + endTime = ts; + } + }); + + return { + start: new Date(startTime).toISOString(), + end: new Date(endTime).toISOString(), + }; +}; diff --git a/x-pack/plugins/uptime/e2e/helpers/make_ping.ts b/x-pack/plugins/uptime/e2e/helpers/make_ping.ts new file mode 100644 index 0000000000000..c93d6f437268d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_ping.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { merge } from 'lodash'; +import type { Client } from '@elastic/elasticsearch'; +import { makeTls, TlsProps } from './make_tls'; + +const DEFAULT_INDEX_NAME = 'heartbeat-8-full-test'; +const DATA_STREAM_INDEX_NAME = 'synthetics-http-default'; + +export const makePing = async ( + es: Client, + monitorId: string, + fields: { [key: string]: any }, + mogrify: (doc: any) => any, + refresh: boolean = true, + tls: boolean | TlsProps = false, + isFleetManaged: boolean | undefined = false +) => { + const timestamp = new Date(); + const baseDoc: any = { + tcp: { + rtt: { + connect: { + us: 14687, + }, + }, + }, + observer: { + geo: { + name: 'mpls', + location: '37.926868, -78.024902', + }, + hostname: 'avc-x1e', + }, + agent: { + hostname: 'avc-x1e', + id: '10730a1a-4cb7-45ce-8524-80c4820476ab', + type: 'heartbeat', + ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', + version: '8.0.0', + }, + '@timestamp': timestamp.toISOString(), + resolve: { + rtt: { + us: 350, + }, + ip: '127.0.0.1', + }, + ecs: { + version: '1.1.0', + }, + host: { + name: 'avc-x1e', + }, + http: { + rtt: { + response_header: { + us: 19349, + }, + total: { + us: 48954, + }, + write_request: { + us: 33, + }, + content: { + us: 51, + }, + validate: { + us: 19400, + }, + }, + response: { + status_code: 200, + body: { + bytes: 3, + hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', + }, + }, + }, + monitor: { + duration: { + us: 49347, + }, + ip: '127.0.0.1', + id: monitorId, + check_group: uuid.v4(), + type: 'http', + status: 'up', + timespan: { + gte: timestamp.toISOString(), + lt: new Date(timestamp.getTime() + 5000).toISOString, + }, + }, + event: { + dataset: 'uptime', + }, + url: { + path: '/pattern', + scheme: 'http', + port: 5678, + domain: 'localhost', + query: 'r=200x5,500x1', + full: 'http://localhost:5678/pattern?r=200x5,500x1', + }, + }; + + if (tls) { + baseDoc.tls = makeTls(tls as any); + } + + const doc = mogrify(merge(baseDoc, fields)); + + await es.index({ + index: isFleetManaged ? DATA_STREAM_INDEX_NAME : DEFAULT_INDEX_NAME, + refresh, + body: doc, + }); + return doc; +}; diff --git a/x-pack/plugins/uptime/e2e/helpers/make_tls.ts b/x-pack/plugins/uptime/e2e/helpers/make_tls.ts new file mode 100644 index 0000000000000..e654a2754e51d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_tls.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import crypto from 'crypto'; + +export interface TlsProps { + valid?: boolean; + commonName?: string; + expiry?: string; + sha256?: string; +} + +type Props = TlsProps & boolean; + +// Note This is just a mock sha256 value, this doesn't actually generate actually sha 256 val +export const getSha256 = () => { + return crypto.randomBytes(64).toString('hex').toUpperCase(); +}; + +export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha256 }: Props) => { + const expiryDate = + expiry ?? + moment() + .add(valid ? 2 : -2, 'months') + .toISOString(); + + return { + version: '1.3', + cipher: 'TLS-AES-128-GCM-SHA256', + certificate_not_valid_before: '2020-03-01T00:00:00.000Z', + certificate_not_valid_after: expiryDate, + server: { + x509: { + not_before: '2020-03-01T00:00:00.000Z', + not_after: expiryDate, + issuer: { + distinguished_name: + 'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US', + common_name: 'DigiCert SHA2 High Assurance Server CA', + }, + subject: { + common_name: commonName, + distinguished_name: 'CN=*.facebook.com,O=Facebook Inc.,L=Menlo Park,ST=California,C=US', + }, + serial_number: '10043199409725537507026285099403602396', + signature_algorithm: 'SHA256-RSA', + public_key_algorithm: 'ECDSA', + public_key_curve: 'P-256', + }, + hash: { + sha256: sha256 ?? '1a48f1db13c3bd1482ba1073441e74a1bb1308dc445c88749e0dc4f1889a88a4', + sha1: '23291c758d925b9f4bb3584de3763317e94c6ce9', + }, + }, + established: true, + rtt: { + handshake: { + us: 33103, + }, + }, + version_protocol: 'tls', + }; +}; diff --git a/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts index cf50e0d6b58ae..5d8d2a1a5d848 100644 --- a/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts @@ -6,8 +6,7 @@ */ import { journey, step, expect, before } from '@elastic/synthetics'; -import { loginToKibana, waitForLoadingToFinish } from './utils'; -import { byTestId } from './uptime.journey'; +import { byTestId, loginToKibana, waitForLoadingToFinish } from './utils'; import { callKibana } from '../../../apm/scripts/create_apm_users_and_roles/helpers/call_kibana'; journey('DataViewPermissions', async ({ page, params }) => { diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index fe8a4960eac12..ae3450a992256 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -12,3 +12,4 @@ export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; export * from './monitor_management.journey'; +export * from './monitor_details'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts new file mode 100644 index 0000000000000..51b782f118c2a --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './monitor_details.journey'; +export * from './monitor_alerts.journey'; +export * from './ping_redirects.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts new file mode 100644 index 0000000000000..c44dbc187bd53 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { noop } from 'lodash'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId, delay } from '../utils'; + +const dateRangeStart = '2019-09-10T12:40:08.078Z'; +const dateRangeEnd = '2019-09-11T19:40:08.078Z'; +const monitorId = '0000-intermittent'; + +const alertId = 'uptime-anomaly-alert'; +const alertThreshold = 'major'; + +journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + }); + + step('go to overview', async () => { + await monitorDetails.navigateToOverviewPage({ dateRangeEnd, dateRangeStart }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(monitorId); + await monitorDetails.waitForLoadingToFinish(); + }); + + step('clean previous data if available', async () => { + // Should only happen locally + await monitorDetails.disableAnomalyDetectionAlert().catch(noop); + await monitorDetails.disableAnomalyDetection().catch(noop); + }); + + step('open anomaly detection flyout', async () => { + await monitorDetails.waitAndRefresh(5000); + await monitorDetails.enableAnomalyDetection(); + await monitorDetails.ensureAnomalyDetectionFlyoutIsOpen(); + }); + + step('can create job', async () => { + const canCreateJob = await monitorDetails.canCreateJob(); + const missingLicense = await page + .waitForSelector('uptimeMLLicenseInfo', { timeout: 10000 }) + .then(() => true) + .catch(() => false); + expect(canCreateJob).toBeTruthy(); + expect(missingLicense).toBeFalsy(); + }); + + step('creates ML job', async () => { + await page.click(byTestId('uptimeMLCreateJobBtn')); + await page.waitForSelector(byTestId('uptimeMLJobSuccessfullyCreated'), { timeout: 30000 }); + await page.click(byTestId('toastCloseButton')); + }); + + step('close anomaly detection flyout', async () => { + await page.click(byTestId('cancelSaveAlertButton')); + }); + + step('open anomaly detection alert', async () => { + await monitorDetails.waitAndRefresh(3000); + await monitorDetails.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeEnableAnomalyAlertBtn')); + }); + + step('update anomaly detection alert', async () => { + await monitorDetails.updateAlert({ id: alertId, threshold: alertThreshold }); + }); + + step('save anomaly detection alert', async () => { + await page.click(byTestId('saveAlertButton')); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector(`text=Created rule "${alertId}"`); + }); + + step('disable anomaly detection alert', async () => { + await monitorDetails.waitAndRefresh(5000); + await monitorDetails.disableAnomalyDetectionAlert(); + await delay(1000); // Menu has delay when closing + await monitorDetails.disableAnomalyDetection(); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts new file mode 100644 index 0000000000000..2965c1acf2c2b --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, before, Page } from '@elastic/synthetics'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId } from '../utils'; + +const dateRangeStart = '2019-09-10T12:40:08.078Z'; +const dateRangeEnd = '2019-09-11T19:40:08.078Z'; +const monitorId = '0000-intermittent'; + +journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + }); + + step('go to overview', async () => { + await monitorDetails.navigateToOverviewPage({ dateRangeEnd, dateRangeStart }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(monitorId); + await monitorDetails.waitForLoadingToFinish(); + }); + + step('should select the ping list location filter', async () => { + await monitorDetails.selectFilterItem('Location', 'mpls'); + }); + + step('should set the status filter', async () => { + await monitorDetails.setStatusFilterUp(); + }); + + step('displays ping data as expected', async () => { + await Promise.all( + [ + 'XZtoHm0B0I9WX_CznN-6', + '7ZtoHm0B0I9WX_CzJ96M', + 'pptnHm0B0I9WX_Czst5X', + 'I5tnHm0B0I9WX_CzPd46', + 'y5tmHm0B0I9WX_Czx93x', + 'XZtmHm0B0I9WX_CzUt3H', + '-JtlHm0B0I9WX_Cz3dyX', + 'k5tlHm0B0I9WX_CzaNxm', + 'NZtkHm0B0I9WX_Cz89w9', + 'zJtkHm0B0I9WX_CzftsN', + ].map((id) => page.waitForSelector(byTestId(`"xpack.uptime.pingList.ping-${id}"`))) + ); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts new file mode 100644 index 0000000000000..ffbc322d7e779 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { makeChecksWithStatus } from '../../helpers/make_checks'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId, delay } from '../utils'; + +journey('MonitorPingRedirects', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + const testMonitor = { + id: '0000-intermittent', + start: 'now-15m', + end: 'now', + redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], + }; + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + await makeChecksWithStatus( + params.getService('es'), + testMonitor.id, + 5, + 2, + 10000, + { + http: { + rtt: { total: { us: 157784 } }, + response: { + status_code: 200, + redirects: testMonitor.redirects, + body: { + bytes: 642102, + hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', + }, + }, + }, + }, + 'up' + ); + await delay(5000); + }); + + step('go to monitor-management', async () => { + await monitorDetails.navigateToOverviewPage({ + dateRangeEnd: testMonitor.end, + dateRangeStart: testMonitor.start, + }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(testMonitor.id); + }); + + step('displays redirect info in detail panel', async () => { + await monitorDetails.waitForLoadingToFinish(); + expect(await monitorDetails.getMonitorRedirects()).toEqual(`${testMonitor.redirects.length}`); + }); + + step('displays redirects in ping list expand row', async () => { + await monitorDetails.expandPingDetails(); + await monitorDetails.waitForLoadingToFinish(); + await page.waitForSelector(byTestId('uptimeMonitorPingListRedirectInfo')); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index 0286f7e898add..d2decba3e9a99 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -8,6 +8,7 @@ import { journey, step, expect, before, after, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { DataStream } from '../../common/runtime_types/monitor_management'; +import { byTestId } from './utils'; const basicMonitorDetails = { location: 'US Central', @@ -197,6 +198,8 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; step('edit http monitor and check breadcrumb', async () => { await uptime.editMonitor(); + // breadcrumb is available before edit page is loaded, make sure its edit view + await page.waitForSelector(byTestId('monitorManagementMonitorName')); const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]'); expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); diff --git a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts index 1c3f82c517b82..7a256e1157272 100644 --- a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts @@ -6,11 +6,7 @@ */ import { journey, step, before } from '@elastic/synthetics'; -import { waitForLoadingToFinish } from './utils'; - -export const byTestId = (testId: string) => { - return `[data-test-subj=${testId}]`; -}; +import { byTestId, waitForLoadingToFinish } from './utils'; journey('uptime', ({ page, params }) => { before(async () => { diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index 8dbc2699a438f..a042d37a99375 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -44,3 +44,11 @@ export const assertText = async ({ page, text }: { page: Page; text: string }) = export const assertNotText = async ({ page, text }: { page: Page; text: string }) => { expect(await page.$(`text=${text}`)).toBeFalsy(); }; + +export const getQuerystring = (params: object) => { + return Object.entries(params) + .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) + .join('&'); +}; + +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx new file mode 100644 index 0000000000000..efadc26a383c0 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Page } from '@elastic/synthetics'; +import { byTestId, delay } from '../journeys/utils'; +import { monitorManagementPageProvider } from './monitor_management'; + +interface AlertType { + id: string; + threshold: string; +} + +export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) { + return { + ...monitorManagementPageProvider({ page, kibanaUrl }), + + async navigateToMonitorDetails(monitorId: string) { + await page.click(byTestId(`monitor-page-link-${monitorId}`)); + }, + + async selectFilterItem(filterType: string, itemArg: string | string[]) { + const itemList = Array.isArray(itemArg) ? itemArg : [itemArg]; + await page.click(`[aria-label="expands filter group for ${filterType} filter"]`); + await this.clickFilterItems(itemList); + return this.applyFilterItems(filterType); + }, + + async clickFilterItems(itemList: string[]) { + for (const title of itemList) { + await page.click(`li[title="${title}"]`); + } + }, + + async applyFilterItems(filterType: string) { + await page.click(`[aria-label="Apply the selected filters for ${filterType}"]`); + }, + + async setStatusFilterUp() { + await page.click('[data-test-subj="xpack.uptime.filterBar.filterStatusUp"]'); + }, + + async setStatusFilterDown() { + await page.click('[data-test-subj="xpack.uptime.filterBar.filterStatusDown"]'); + }, + + async refreshFromES() { + await this.byTestId('superDatePickerApplyTimeButton'); + }, + + async enableAnomalyDetection() { + await page.click(byTestId('uptimeEnableAnomalyBtn')); + }, + + async getMonitorRedirects() { + return await page.textContent(byTestId('uptimeMonitorRedirectInfo')); + }, + + async expandPingDetails() { + await page.click(byTestId('uptimePingListExpandBtn')); + }, + + async ensureAnomalyDetectionFlyoutIsOpen() { + await page.waitForSelector(byTestId('uptimeMLFlyout')); + }, + + async isMLMenuVisible() { + return await page.isVisible(byTestId('uptimeManageMLContextMenu'), { + timeout: 3000, + }); + }, + + async canCreateJob(): Promise { + await this.ensureAnomalyDetectionFlyoutIsOpen(); + const createJobBtn = await page.$(byTestId('uptimeMLCreateJobBtn')); + return await createJobBtn!.isEnabled(); + }, + + async openAnomalyDetectionMenu() { + const visible = await this.isMLMenuVisible(); + if (visible === false) { + await page.click(byTestId('uptimeManageMLJobBtn'), { timeout: 5000 }); + } + }, + + async closeAnomalyDetectionMenu() { + if ((await this.isMLMenuVisible()) === true) { + await page.click(byTestId('uptimeManageMLJobBtn'), { timeout: 5000 }); + } + }, + + async waitAndRefresh(timeout?: number) { + await delay(timeout ?? 1000); + await this.refreshFromES(); + await this.waitForLoadingToFinish(); + }, + + async updateAlert({ id, threshold }: AlertType) { + await this.fillByTestSubj('alertNameInput', id); + await this.selectAlertThreshold(threshold); + }, + + async selectAlertThreshold(threshold: string) { + await this.clickByTestSubj('uptimeAnomalySeverity'); + await this.clickByTestSubj('anomalySeveritySelect'); + await page.click(`text=${threshold}`); + }, + + async disableAnomalyDetection() { + await this.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeDeleteMLJobBtn'), { timeout: 10000 }); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector('text=Job deleted'); + await this.closeAnomalyDetectionMenu(); + }, + + async disableAnomalyDetectionAlert() { + await this.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeManageAnomalyAlertBtn'), { timeout: 10000 }); + await page.click(byTestId('uptimeDisableAnomalyAlertBtn')); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector('text=Rule successfully disabled!'); + await this.closeAnomalyDetectionMenu(); + }, + }; +} diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 384242bc9af70..ab21a372681f3 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -7,6 +7,7 @@ import { Page } from '@elastic/synthetics'; import { DataStream } from '../../common/runtime_types/monitor_management'; +import { getQuerystring } from '../journeys/utils'; import { loginPageProvider } from './login'; import { utilsPageProvider } from './utils'; @@ -46,8 +47,8 @@ export function monitorManagementPageProvider({ }); }, - async navigateToOverviewPage() { - await page.goto(overview, { + async navigateToOverviewPage(options?: object) { + await page.goto(`${overview}${options ? `?${getQuerystring(options)}` : ''}`, { waitUntil: 'networkidle', }); }, @@ -62,7 +63,7 @@ export function monitorManagementPageProvider({ }, async editMonitor() { - await this.clickByTestSubj('monitorManagementEditMonitor'); + await page.click(this.byTestId('monitorManagementEditMonitor'), { delay: 800 }); }, async findMonitorConfiguration(monitorConfig: Record) { diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index cb616be66b7c9..9108412405602 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -15,26 +15,12 @@ import './journeys'; import { createApmAndObsUsersAndRoles } from '../../apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles'; import { importMonitors } from './tasks/import_monitors'; -const listOfJourneys = [ - 'uptime', - 'StepsDuration', - 'TlsFlyoutInAlertingApp', - 'StatusFlyoutInAlertingApp', - 'DefaultEmailSettings', - 'MonitorManagement-http', - 'MonitorManagement-tcp', - 'MonitorManagement-icmp', - 'MonitorManagement-browser', - 'MonitorManagement breadcrumbs', - 'Monitor Management read only user', -] as const; - export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { - const result = await playwrightStart(getService, headless, match); + const results = await playwrightStart(getService, headless, match); - listOfJourneys.forEach((journey) => { - if (result?.[journey] && result[journey].status !== 'succeeded') { + Object.entries(results).forEach(([_journey, result]) => { + if (result.status !== 'succeeded') { throw new Error('Tests failed'); } }); @@ -66,7 +52,7 @@ async function playwrightStart(getService: any, headless = true, match?: string) }); const res = await playwrightRun({ - params: { kibanaUrl }, + params: { kibanaUrl, getService }, playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 }, match: match === 'undefined' ? '' : match, }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index df0abcb88180b..1176ea5e6a5c3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -75,7 +75,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const button = ( setIsPopOverOpen(true) : onEnableJob} + onClick={hasMLJob ? () => setIsPopOverOpen(!isPopOverOpen) : onEnableJob} disabled={hasMLJob && !canDeleteMLJob} isLoading={showLoading} size="s" diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index d36f8124599fe..6ecf87f88b8be 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -61,10 +61,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./certificates')); }); - describe('with generated data but no data reset', () => { - loadTestFile(require.resolve('./ping_redirects')); - }); - describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); @@ -75,7 +71,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { after(async () => await esArchiver.unload(ARCHIVE)); loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./monitor')); loadTestFile(require.resolve('./ml_anomaly')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/uptime/monitor.ts b/x-pack/test/functional/apps/uptime/monitor.ts deleted file mode 100644 index de88bd12a00fb..0000000000000 --- a/x-pack/test/functional/apps/uptime/monitor.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const uptimeService = getService('uptime'); - const { uptime } = getPageObjects(['uptime']); - const archive = 'x-pack/test/functional/es_archives/uptime/full_heartbeat'; - - describe('monitor page', function () { - this.tags(['skipFirefox']); - const dateStart = 'Sep 10, 2019 @ 12:40:08.078'; - const dateEnd = 'Sep 11, 2019 @ 19:40:08.078'; - const monitorId = '0000-intermittent'; - - before(async () => { - await esArchiver.loadIfNeeded(archive); - await uptimeService.navigation.goToUptime(); - }); - - after(async () => { - await esArchiver.unload(archive); - }); - - describe('navigation to monitor page', () => { - before(async () => { - await uptime.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId); - }); - - it('should select the ping list location filter', async () => { - await uptimeService.common.selectFilterItem('Location', 'mpls'); - }); - - it('should set the status filter', async () => { - await uptimeService.common.setStatusFilterUp(); - }); - - it('displays ping data as expected', async () => { - await uptime.checkPingListInteractions([ - 'XZtoHm0B0I9WX_CznN-6', - '7ZtoHm0B0I9WX_CzJ96M', - 'pptnHm0B0I9WX_Czst5X', - 'I5tnHm0B0I9WX_CzPd46', - 'y5tmHm0B0I9WX_Czx93x', - 'XZtmHm0B0I9WX_CzUt3H', - '-JtlHm0B0I9WX_Cz3dyX', - 'k5tlHm0B0I9WX_CzaNxm', - 'NZtkHm0B0I9WX_Cz89w9', - 'zJtkHm0B0I9WX_CzftsN', - ]); - }); - }); - }); -}; diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts deleted file mode 100644 index 06352d37ada28..0000000000000 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const { uptime: uptimePage, header } = getPageObjects(['uptime', 'header']); - const uptime = getService('uptime'); - const esArchiver = getService('esArchiver'); - - const archive = 'x-pack/test/functional/es_archives/uptime/blank'; - - const monitor = () => uptime.monitor; - - // FLAKY: https://github.com/elastic/kibana/issues/118476 - describe.skip('Ping redirects', () => { - const start = '~ 15 minutes ago'; - const end = 'now'; - - const MONITOR_ID = 'redirect-testing-id'; - - before(async () => { - await esArchiver.loadIfNeeded(archive); - }); - - after('unload', async () => { - await esArchiver.unload(archive); - }); - - beforeEach(async () => { - await makeChecksWithStatus( - getService('es'), - MONITOR_ID, - 5, - 2, - 10000, - { - http: { - rtt: { total: { us: 157784 } }, - response: { - status_code: 200, - redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], - body: { - bytes: 642102, - hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', - }, - }, - }, - }, - 'up' - ); - await delay(1000); - }); - - it('loads and goes to details page', async () => { - await uptime.navigation.goToUptime(); - await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); - }); - - it('display redirect info in detail panel', async () => { - await header.waitUntilLoadingHasFinished(); - await monitor().hasRedirectInfo(); - }); - - it('displays redirects in ping list expand row', async () => { - await monitor().hasRedirectInfoInPingList(); - }); - }); -}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts deleted file mode 100644 index 6f65c620b527c..0000000000000 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/111667 - describe.skip('uptime anomaly alert', () => { - const pageObjects = getPageObjects(['common', 'uptime']); - const supertest = getService('supertest'); - const retry = getService('retry'); - - const monitorId = '0000-intermittent'; - - const uptime = getService('uptime'); - - const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; - const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; - let alerts: any; - const alertId = 'uptime-anomaly-alert'; - - before(async () => { - alerts = getService('uptime').alerts; - - await uptime.navigation.goToUptime(); - - await uptime.navigation.loadDataAndGoToMonitorPage( - DEFAULT_DATE_START, - DEFAULT_DATE_END, - monitorId - ); - }); - - it('can delete existing job', async () => { - if (await uptime.ml.alreadyHasJob()) { - await uptime.ml.openMLManageMenu(); - await uptime.ml.deleteMLJob(); - await uptime.navigation.refreshApp(); - } - }); - - it('can open ml flyout', async () => { - await uptime.ml.openMLFlyout(); - }); - - it('has permission to create job', async () => { - expect(uptime.ml.canCreateJob()).to.eql(true); - expect(uptime.ml.hasNoLicenseInfo()).to.eql(false); - }); - - it('can create job successfully', async () => { - await uptime.ml.createMLJob(); - await pageObjects.common.closeToast(); - await uptime.ml.cancelAlertFlyout(); - }); - - it('can open ML Manage Menu', async () => { - await uptime.ml.openMLManageMenu(); - }); - - it('can open anomaly alert flyout', async () => { - await uptime.ml.openAlertFlyout(); - }); - - it('can set alert name', async () => { - await alerts.setAlertName(alertId); - }); - - it('can set alert tags', async () => { - await alerts.setAlertTags(['uptime', 'anomaly-alert']); - }); - - it('can change anomaly alert threshold', async () => { - await uptime.ml.changeAlertThreshold('major'); - }); - - it('can save alert', async () => { - await alerts.clickSaveAlertButton(); - await alerts.clickSaveAlertsConfirmButton(); - await pageObjects.common.closeToast(); - }); - - it('has created a valid alert with expected parameters', async () => { - let alert: any; - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === alertId - ); - expect(alertsFromThisTest).to.have.length(1); - alert = alertsFromThisTest[0]; - }); - - // Ensure the parameters and other stateful data - // on the alert match up with the values we provided - // for our test helper to input into the flyout. - const { actions, alertTypeId, consumer, id, params, tags } = alert; - try { - expect(actions).to.eql([]); - expect(alertTypeId).to.eql('xpack.uptime.alerts.durationAnomaly'); - expect(consumer).to.eql('uptime'); - expect(tags).to.eql(['uptime', 'anomaly-alert']); - expect(params.monitorId).to.eql(monitorId); - expect(params.severity).to.eql(50); - } finally { - await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); - } - }); - - it('change button to disable anomaly alert', async () => { - await uptime.ml.openMLManageMenu(); - expect(uptime.ml.manageAnomalyAlertIsVisible()).to.eql(true); - }); - - it('can delete job successfully', async () => { - await uptime.ml.deleteMLJob(); - }); - - it('verifies that alert is also deleted', async () => { - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === alertId - ); - expect(alertsFromThisTest).to.have.length(0); - }); - }); - }); -}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts index 222dcd22d6f86..d2078267bde85 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts @@ -24,7 +24,6 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => { after(async () => await esArchiver.unload(ARCHIVE)); loadTestFile(require.resolve('./alert_flyout')); - loadTestFile(require.resolve('./anomaly_alert')); loadTestFile(require.resolve('./simple_down_alert')); }); });