From c0f46471c19e8e71fbc3e428b18b06797601da05 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 17 May 2021 18:42:11 -0400 Subject: [PATCH] [Uptime] [Synthetics Integration] Add functional tests for Synthetics Integration (#100161) (#100242) * add functional tests for synthetics fleet package Co-authored-by: Dominique Clarke --- .../components/fleet_package/combo_box.tsx | 3 +- .../fleet_package/custom_fields.tsx | 8 + .../fleet_package/http_advanced_fields.tsx | 12 + .../index_response_body_field.tsx | 2 +- .../fleet_package/request_body_field.tsx | 4 + .../fleet_package/tcp_advanced_fields.tsx | 11 +- .../components/fleet_package/tls_fields.tsx | 5 + x-pack/test/functional/apps/uptime/index.ts | 1 + .../apps/uptime/synthetics_integration.ts | 442 ++++++++++++++++++ x-pack/test/functional/config.js | 3 + x-pack/test/functional/page_objects/index.ts | 2 + .../synthetics_integration_page.ts | 384 +++++++++++++++ .../services/uptime/synthetics_package.ts | 176 +++++++ .../test/functional/services/uptime/uptime.ts | 3 + 14 files changed, 1052 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/apps/uptime/synthetics_integration.ts create mode 100644 x-pack/test/functional/page_objects/synthetics_integration_page.ts create mode 100644 x-pack/test/functional/services/uptime/synthetics_package.ts diff --git a/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx b/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx index 12ee154dbcac..7a1df79b0a59 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx @@ -13,7 +13,7 @@ export interface Props { selectedOptions: string[]; } -export const ComboBox = ({ onChange, selectedOptions }: Props) => { +export const ComboBox = ({ onChange, selectedOptions, ...props }: Props) => { const [formattedSelectedOptions, setSelectedOptions] = useState< Array> >(selectedOptions.map((option) => ({ label: option, key: option }))); @@ -66,6 +66,7 @@ export const ComboBox = ({ onChange, selectedOptions }: Props) => { onChange={onOptionsChange} onSearchChange={onSearchChange} isInvalid={isInvalid} + {...props} /> ); }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 0bc2fc8823ce..e6703a6eaa97 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -76,6 +76,7 @@ export const CustomFields = memo( defaultMessage="Configure your monitor with the following options." /> } + data-test-subj="monitorSettingsSection" > @@ -104,6 +105,7 @@ export const CustomFields = memo( configKey: ConfigKeys.MONITOR_TYPE, }) } + data-test-subj="syntheticsMonitorTypeField" /> )} @@ -128,6 +130,7 @@ export const CustomFields = memo( onChange={(event) => handleInputChange({ value: event.target.value, configKey: ConfigKeys.URLS }) } + data-test-subj="syntheticsUrlField" /> )} @@ -155,6 +158,7 @@ export const CustomFields = memo( configKey: ConfigKeys.HOSTS, }) } + data-test-subj="syntheticsTCPHostField" /> )} @@ -182,6 +186,7 @@ export const CustomFields = memo( configKey: ConfigKeys.HOSTS, }) } + data-test-subj="syntheticsICMPHostField" /> )} @@ -268,6 +273,7 @@ export const CustomFields = memo( configKey: ConfigKeys.APM_SERVICE_NAME, }) } + data-test-subj="syntheticsAPMServiceName" /> {isHTTP && ( @@ -364,6 +370,7 @@ export const CustomFields = memo( handleInputChange({ value, configKey: ConfigKeys.TAGS })} + data-test-subj="syntheticsTags" /> @@ -385,6 +392,7 @@ export const CustomFields = memo( defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates." /> } + data-test-subj="syntheticsIsTLSEnabled" > (({ validate }) => { defaultMessage="Advanced HTTP options" /> } + data-test-subj="syntheticsHTTPAdvancedFieldsAccordion" > (({ validate }) => { defaultMessage="Configure an optional request to send to the remote host including method, body, and headers." /> } + data-test-subj="httpAdvancedFieldsSection" > (({ validate }) => { configKey: ConfigKeys.USERNAME, }) } + data-test-subj="syntheticsUsername" /> (({ validate }) => { configKey: ConfigKeys.PASSWORD, }) } + data-test-subj="syntheticsPassword" /> (({ validate }) => { configKey: ConfigKeys.PROXY_URL, }) } + data-test-subj="syntheticsProxyUrl" /> (({ validate }) => { configKey: ConfigKeys.REQUEST_METHOD_CHECK, }) } + data-test-subj="syntheticsRequestMethod" /> (({ validate }) => { defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself." /> } + data-test-subj="syntheticsRequestHeaders" > (({ validate }) => { http.response.body.headers } + data-test-subj="syntheticsIndexResponseHeaders" > (({ validate }) => { configKey: ConfigKeys.RESPONSE_STATUS_CHECK, }) } + data-test-subj="syntheticsResponseStatusCheck" /> (({ validate }) => { defaultMessage="A list of expected response headers." /> } + data-test-subj="syntheticsResponseHeaders" > (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsResponseBodyCheckPositive" /> (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsResponseBodyCheckNegative" /> diff --git a/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx index a82e7a093807..fc53b275f082 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx @@ -38,7 +38,7 @@ export const ResponseBodyIndexField = ({ defaultValue, onChange }: Props) => { return ( - + { { id: Mode.TEXT, name: modeLabels[Mode.TEXT], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.TEXT}`, content: ( { { id: Mode.JSON, name: modeLabels[Mode.JSON], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.JSON}`, content: ( { { id: Mode.XML, name: modeLabels[Mode.XML], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.XML}`, content: ( { { id: Mode.FORM, name: modeLabels[Mode.FORM], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.FORM}`, content: ( { ); return ( - + { configKey: ConfigKeys.PROXY_URL, }) } + data-test-subj="syntheticsProxyUrl" /> {!!fields[ConfigKeys.PROXY_URL] && ( - + { }), [handleInputChange] )} + data-test-subj="syntheticsTCPRequestSendCheck" /> @@ -166,6 +172,7 @@ export const TCPAdvancedFields = () => { }), [handleInputChange] )} + data-test-subj="syntheticsTCPResponseReceiveCheck" /> diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx index de8879ec3a81..a2db0d99088f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx @@ -139,6 +139,7 @@ export const TLSFields: React.FunctionComponent<{ }, })); }} + data-test-subj="syntheticsTLSVerificationMode" /> {fields[ConfigKeys.TLS_VERIFICATION_MODE].value === VerificationMode.NONE && ( @@ -229,6 +230,7 @@ export const TLSFields: React.FunctionComponent<{ }, })); }} + data-test-subj="syntheticsTLSCA" /> diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index f96d2d0255d2..0b02fd2bf322 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -59,6 +59,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./locations')); loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); + loadTestFile(require.resolve('./synthetics_integration')); }); describe('with generated data but no data reset', () => { diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts new file mode 100644 index 000000000000..52ec81b8bf7d --- /dev/null +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -0,0 +1,442 @@ +/* + * 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'; +import { FullAgentPolicy } from '../../../../plugins/fleet/common'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const monitorName = 'Sample Synthetics integration'; + + const uptimePage = getPageObjects(['syntheticsIntegration']); + const testSubjects = getService('testSubjects'); + const uptimeService = getService('uptime'); + + const generatePolicy = ({ + agentFullPolicy, + version, + monitorType, + name, + config, + }: { + agentFullPolicy: FullAgentPolicy; + version: string; + monitorType: string; + name: string; + config: Record; + }) => ({ + data_stream: { + namespace: 'default', + }, + id: agentFullPolicy.inputs[0].id, + meta: { + package: { + name: 'synthetics', + version, + }, + }, + name, + revision: 1, + streams: [ + { + data_stream: { + dataset: monitorType, + type: 'synthetics', + }, + id: `${agentFullPolicy.inputs[0]?.streams?.[0]?.id}`, + name, + type: monitorType, + processors: [ + { + add_observer_metadata: { + geo: { + name: 'Fleet managed', + }, + }, + }, + { + add_fields: { + fields: { + 'monitor.fleet_managed': true, + }, + target: '', + }, + }, + ], + ...config, + }, + ], + type: `synthetics/${monitorType}`, + use_output: 'default', + }); + + describe('When on the Synthetics Integration Policy Create Page', function () { + this.tags(['ciGroup6']); + const basicConfig = { + name: monitorName, + apmServiceName: 'Sample APM Service', + tags: 'sample tag', + }; + + const generateHTTPConfig = (url: string) => ({ + ...basicConfig, + url, + }); + + const generateTCPorICMPConfig = (host: string) => ({ + ...basicConfig, + host, + }); + + describe('displays custom UI', () => { + before(async () => { + const version = await uptimeService.syntheticsPackage.getSyntheticsPackageVersion(); + await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); + }); + + it('should display policy view', async () => { + await uptimePage.syntheticsIntegration.ensureIsOnPackagePage(); + }); + + it('prevent saving when integration name, url/host, or schedule is missing', async () => { + const saveButton = await uptimePage.syntheticsIntegration.findSaveButton(); + await saveButton.click(); + + await testSubjects.missingOrFail('packagePolicyCreateSuccessToast'); + }); + }); + + describe('create new policy', () => { + let version: string; + before(async () => { + await uptimeService.syntheticsPackage.deletePolicyByName('system-1'); + }); + + beforeEach(async () => { + version = (await uptimeService.syntheticsPackage.getSyntheticsPackageVersion())!; + await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); + await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); + }); + + afterEach(async () => { + await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); + }); + + it('allows saving when user enters a valid integration name and url/host', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + 'check.request.method': 'GET', + }, + }), + ]); + }); + + it('allows enabling tls with defaults', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.enableTLS(); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': 'GET', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows configuring tls', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + const tlsConfig = { + verificationMode: 'strict', + ca: 'ca', + cert: 'cert', + certKey: 'certKey', + certKeyPassphrase: 'certKeyPassphrase', + }; + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.configureTLSOptions(tlsConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': 'GET', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': tlsConfig.verificationMode, + 'ssl.certificate': tlsConfig.cert, + 'ssl.certificate_authorities': tlsConfig.ca, + 'ssl.key': tlsConfig.certKey, + 'ssl.key_passphrase': tlsConfig.certKeyPassphrase, + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows configuring http advanced options', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + const advancedConfig = { + username: 'username', + password: 'password', + proxyUrl: 'proxyUrl', + requestMethod: 'POST', + responseStatusCheck: '204', + responseBodyCheckPositive: 'success', + responseBodyCheckNegative: 'failure', + requestHeaders: { + sampleRequestHeader1: 'sampleRequestKey1', + sampleRequestHeader2: 'sampleRequestKey2', + }, + responseHeaders: { + sampleResponseHeader1: 'sampleResponseKey1', + sampleResponseHeader2: 'sampleResponseKey2', + }, + requestBody: { + type: 'xml', + value: 'samplexml', + }, + indexResponseBody: false, + indexResponseHeaders: false, + }; + await uptimePage.syntheticsIntegration.configureHTTPAdvancedOptions(advancedConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': advancedConfig.requestMethod, + 'check.request.headers': { + 'Content-Type': 'application/xml', + ...advancedConfig.requestHeaders, + }, + 'check.response.headers': advancedConfig.responseHeaders, + 'check.response.status': [advancedConfig.responseStatusCheck], + 'check.request.body': `${advancedConfig.requestBody.value}`, // code editor adds closing tag + 'check.response.body.positive': [advancedConfig.responseBodyCheckPositive], + 'check.response.body.negative': [advancedConfig.responseBodyCheckNegative], + 'response.include_body': advancedConfig.indexResponseBody ? 'on_error' : 'never', + 'response.include_headers': advancedConfig.indexResponseHeaders, + schedule: '@every 3m', + timeout: '16s', + urls: config.url, + proxy_url: advancedConfig.proxyUrl, + username: advancedConfig.username, + password: advancedConfig.password, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows saving tcp monitor when user enters a valid integration name and host+port', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('smtp.gmail.com:587'); + + await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'tcp', + config: { + proxy_use_local_resolver: false, + schedule: '@every 3m', + timeout: '16s', + hosts: config.host, + tags: [config.tags], + 'service.name': config.apmServiceName, + }, + }), + ]); + }); + + it('allows configuring tcp advanced options', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('smtp.gmail.com:587'); + + await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); + const advancedConfig = { + proxyUrl: 'proxyUrl', + requestSendCheck: 'body', + responseReceiveCheck: 'success', + proxyUseLocalResolver: true, + }; + await uptimePage.syntheticsIntegration.configureTCPAdvancedOptions(advancedConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'tcp', + config: { + schedule: '@every 3m', + timeout: '16s', + hosts: config.host, + proxy_url: advancedConfig.proxyUrl, + proxy_use_local_resolver: advancedConfig.proxyUseLocalResolver, + 'check.receive': advancedConfig.responseReceiveCheck, + 'check.send': advancedConfig.requestSendCheck, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows saving icmp monitor when user enters a valid integration name and host', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('1.1.1.1'); + + await uptimePage.syntheticsIntegration.createBasicICMPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'icmp', + config: { + schedule: '@every 3m', + timeout: '16s', + wait: '1s', + hosts: config.host, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + }); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 57d35590b3c8..699b63f39f7a 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -154,6 +154,9 @@ export default async function ({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, + fleet: { + pathname: '/app/fleet', + }, ml: { pathname: '/app/ml', }, diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 0e66615371a4..563382b37e1d 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -24,6 +24,7 @@ import { StatusPagePageProvider } from './status_page'; import { UpgradeAssistantPageProvider } from './upgrade_assistant_page'; import { RollupPageProvider } from './rollup_page'; import { UptimePageProvider } from './uptime_page'; +import { SyntheticsIntegrationPageProvider } from './synthetics_integration_page'; import { ApiKeysPageProvider } from './api_keys_page'; import { LicenseManagementPageProvider } from './license_management_page'; import { IndexManagementPageProvider } from './index_management_page'; @@ -65,6 +66,7 @@ export const pageObjects = { statusPage: StatusPagePageProvider, upgradeAssistant: UpgradeAssistantPageProvider, uptime: UptimePageProvider, + syntheticsIntegration: SyntheticsIntegrationPageProvider, rollup: RollupPageProvider, apiKeys: ApiKeysPageProvider, licenseManagement: LicenseManagementPageProvider, diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts new file mode 100644 index 000000000000..69ae3f43d26f --- /dev/null +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -0,0 +1,384 @@ +/* + * 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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SyntheticsIntegrationPageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'header']); + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + + return { + /** + * Navigates to the Synthetics Integration page + * + */ + async navigateToPackagePage(packageVersion: string) { + await pageObjects.common.navigateToUrl( + 'fleet', + `/integrations/synthetics-${packageVersion}/add-integration`, + { + shouldUseHashForSubUrl: true, + useActualUrl: true, + } + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, + + async navigateToPackageEditPage(packageId: string, agentId: string) { + await pageObjects.common.navigateToUrl( + 'fleet', + `/policies/${agentId}/edit-integration/${packageId}`, + { + shouldUseHashForSubUrl: true, + useActualUrl: true, + } + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, + + /** + * Finds and returns the Policy Details Page Save button + */ + async findSaveButton(isEditPage?: boolean) { + await this.ensureIsOnPackagePage(); + return await testSubjects.find( + isEditPage ? 'saveIntegration' : 'createPackagePolicySaveButton' + ); + }, + + /** + * Finds and returns the Policy Details Page Cancel Button + */ + async findCancelButton() { + await this.ensureIsOnPackagePage(); + return await testSubjects.find('policyDetailsCancelButton'); + }, + + /** + * Determines if the policy was created successfully by looking for the creation success toast + */ + async isPolicyCreatedSuccessfully() { + await testSubjects.existOrFail('packagePolicyCreateSuccessToast'); + }, + + /** + * Selects the monitor type + * @params {monitorType} the type of monitor, tcp, http, or icmp + */ + async selectMonitorType(monitorType: string) { + await testSubjects.selectValue('syntheticsMonitorTypeField', monitorType); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the input to fill + * @params {value} the value of the input + */ + async fillTextInputByTestSubj(testSubj: string, value: string) { + const field = await testSubjects.find(testSubj, 5000); + await field.click(); + await field.clearValue(); + await field.type(value); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the input to fill + * @params {value} the value of the input + */ + async fillTextInput(field: WebElementWrapper, value: string) { + await field.click(); + await field.clearValue(); + await field.type(value); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the comboBox + */ + async setComboBox(testSubj: string, value: string) { + await comboBox.setCustom(`${testSubj} > comboBoxInput`, value); + }, + + /** + * Finds and returns the HTTP advanced options accordion trigger + */ + async findHTTPAdvancedOptionsAccordion() { + await this.ensureIsOnPackagePage(); + const accordion = await testSubjects.find('syntheticsHTTPAdvancedFieldsAccordion', 5000); + return accordion; + }, + + /** + * Finds and returns the enable TLS checkbox + */ + async findEnableTLSCheckbox() { + await this.ensureIsOnPackagePage(); + const tlsCheckboxContainer = await testSubjects.find('syntheticsIsTLSEnabled'); + return await tlsCheckboxContainer.findByCssSelector('label'); + }, + + /** + * ensures that the package page is the currently display view + */ + async ensureIsOnPackagePage() { + await testSubjects.existOrFail('monitorSettingsSection'); + }, + + /** + * Clicks save button and confirms update on the Policy Details page + */ + async confirmAndSave(isEditPage?: boolean) { + await this.ensureIsOnPackagePage(); + const saveButton = await this.findSaveButton(isEditPage); + saveButton.click(); + }, + + /** + * Fills in the username and password field + * @params username {string} the value of the username + * @params password {string} the value of the password + */ + async configureUsernameAndPassword({ username, password }: Record) { + await this.fillTextInputByTestSubj('syntheticsUsername', username); + await this.fillTextInputByTestSubj('syntheticsPassword', password); + }, + + /** + * + * Configures request headers + * @params headers {string} an object containing desired headers + * + */ + async configureRequestHeaders(headers: Record) { + await this.configureHeaders('syntheticsRequestHeaders', headers); + }, + + /** + * + * Configures response headers + * @params headers {string} an object containing desired headers + * + */ + async configureResponseHeaders(headers: Record) { + await this.configureHeaders('syntheticsResponseHeaders', headers); + }, + + /** + * + * Configures headers + * @params testSubj {string} test subj + * @params headers {string} an object containing desired headers + * + */ + async configureHeaders(testSubj: string, headers: Record) { + const headersContainer = await testSubjects.find(testSubj); + const addHeaderButton = await headersContainer.findByCssSelector('button'); + const keys = Object.keys(headers); + + await Promise.all( + keys.map(async (key, index) => { + await addHeaderButton.click(); + const keyField = await headersContainer.findByCssSelector( + `[data-test-subj="keyValuePairsKey${index}"]` + ); + const valueField = await headersContainer.findByCssSelector( + `[data-test-subj="keyValuePairsValue${index}"]` + ); + await this.fillTextInput(keyField, key); + await this.fillTextInput(valueField, headers[key]); + }) + ); + }, + + /** + * + * Configures request body + * @params contentType {string} contentType of the request body + * @params value {string} value of the request body + * + */ + async configureRequestBody(testSubj: string, value: string) { + await testSubjects.click(`syntheticsRequestBodyTab__${testSubj}`); + const codeEditorContainer = await testSubjects.find('codeEditorContainer'); + const textArea = await codeEditorContainer.findByCssSelector('textarea'); + await textArea.clearValue(); + await textArea.type(value); + }, + + /** + * Creates basic common monitor details + * @params name {string} the name of the monitor + * @params url {string} the url of the monitor + * + */ + async createBasicMonitorDetails({ name, apmServiceName, tags }: Record) { + await this.fillTextInputByTestSubj('packagePolicyNameInput', name); + await this.fillTextInputByTestSubj('syntheticsAPMServiceName', apmServiceName); + await this.setComboBox('syntheticsTags', tags); + }, + + /** + * Fills in the fields to create a basic HTTP monitor + * @params name {string} the name of the monitor + * @params url {string} the url of the monitor + * + */ + async createBasicHTTPMonitorDetails({ + name, + url, + apmServiceName, + tags, + }: Record) { + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsUrlField', url); + }, + + /** + * Fills in the fields to create a basic TCP monitor + * @params name {string} the name of the monitor + * @params host {string} the host (and port) of the monitor + * + */ + async createBasicTCPMonitorDetails({ + name, + host, + apmServiceName, + tags, + }: Record) { + await this.selectMonitorType('tcp'); + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsTCPHostField', host); + }, + + /** + * Creates a basic ICMP monitor + * @params name {string} the name of the monitor + * @params host {string} the host of the monitor + */ + async createBasicICMPMonitorDetails({ + name, + host, + apmServiceName, + tags, + }: Record) { + await this.selectMonitorType('icmp'); + await this.fillTextInputByTestSubj('packagePolicyNameInput', name); + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsICMPHostField', host); + }, + + /** + * Enables TLS + */ + async enableTLS() { + const tlsCheckbox = await this.findEnableTLSCheckbox(); + await tlsCheckbox.click(); + }, + + /** + * Configures TLS settings + * @params verificationMode {string} the name of the monitor + */ + async configureTLSOptions({ + verificationMode, + ca, + cert, + certKey, + certKeyPassphrase, + }: Record) { + await this.enableTLS(); + await testSubjects.selectValue('syntheticsTLSVerificationMode', verificationMode); + await this.fillTextInputByTestSubj('syntheticsTLSCA', ca); + await this.fillTextInputByTestSubj('syntheticsTLSCert', cert); + await this.fillTextInputByTestSubj('syntheticsTLSCertKey', certKey); + await this.fillTextInputByTestSubj('syntheticsTLSCertKeyPassphrase', certKeyPassphrase); + }, + + /** + * Configure http advanced settings + */ + async configureHTTPAdvancedOptions({ + username, + password, + proxyUrl, + requestMethod, + requestHeaders, + responseStatusCheck, + responseBodyCheckPositive, + responseBodyCheckNegative, + requestBody, + responseHeaders, + indexResponseBody, + indexResponseHeaders, + }: { + username: string; + password: string; + proxyUrl: string; + requestMethod: string; + responseStatusCheck: string; + responseBodyCheckPositive: string; + responseBodyCheckNegative: string; + requestBody: { value: string; type: string }; + requestHeaders: Record; + responseHeaders: Record; + indexResponseBody: boolean; + indexResponseHeaders: boolean; + }) { + await testSubjects.click('syntheticsHTTPAdvancedFieldsAccordion'); + await this.configureResponseHeaders(responseHeaders); + await this.configureRequestHeaders(requestHeaders); + await this.configureRequestBody(requestBody.type, requestBody.value); + await this.configureUsernameAndPassword({ username, password }); + await this.setComboBox('syntheticsResponseStatusCheck', responseStatusCheck); + await this.setComboBox('syntheticsResponseBodyCheckPositive', responseBodyCheckPositive); + await this.setComboBox('syntheticsResponseBodyCheckNegative', responseBodyCheckNegative); + await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); + await testSubjects.selectValue('syntheticsRequestMethod', requestMethod); + if (!indexResponseBody) { + const field = await testSubjects.find('syntheticsIndexResponseBody'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + if (!indexResponseHeaders) { + const field = await testSubjects.find('syntheticsIndexResponseHeaders'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + }, + + /** + * Configure tcp advanced settings + */ + async configureTCPAdvancedOptions({ + proxyUrl, + requestSendCheck, + responseReceiveCheck, + proxyUseLocalResolver, + }: { + proxyUrl: string; + requestSendCheck: string; + responseReceiveCheck: string; + proxyUseLocalResolver: boolean; + }) { + await testSubjects.click('syntheticsTCPAdvancedFieldsAccordion'); + await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); + await this.fillTextInputByTestSubj('syntheticsTCPRequestSendCheck', requestSendCheck); + await this.fillTextInputByTestSubj('syntheticsTCPResponseReceiveCheck', responseReceiveCheck); + if (proxyUseLocalResolver) { + const field = await testSubjects.find('syntheticsUseLocalResolver'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/synthetics_package.ts b/x-pack/test/functional/services/uptime/synthetics_package.ts new file mode 100644 index 000000000000..78d0fcd61fde --- /dev/null +++ b/x-pack/test/functional/services/uptime/synthetics_package.ts @@ -0,0 +1,176 @@ +/* + * 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'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + DeletePackagePoliciesRequest, + GetPackagePoliciesResponse, + GetFullAgentPolicyResponse, + GetPackagesResponse, + GetAgentPoliciesResponse, +} from '../../../../plugins/fleet/common'; + +const INGEST_API_ROOT = '/api/fleet'; +const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`; +const INGEST_API_PACKAGE_POLICIES = `${INGEST_API_ROOT}/package_policies`; +const INGEST_API_PACKAGE_POLICIES_DELETE = `${INGEST_API_PACKAGE_POLICIES}/delete`; +const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; + +export function SyntheticsPackageProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + const retry = getService('retry'); + + const logSupertestApiErrorAndThrow = (message: string, error: any): never => { + const responseBody = error?.response?.body; + const responseText = error?.response?.text; + log.error(`Error occurred at ${Date.now()} | ${new Date().toISOString()}`); + log.error(JSON.stringify(responseBody || responseText, null, 2)); + log.error(error); + throw new Error(message); + }; + const retrieveSyntheticsPackageInfo = (() => { + // Retrieve information about the Synthetics package + // EPM does not currently have an API to get the "lastest" information for a page given its name, + // so we'll retrieve a list of packages and then find the package info in the list. + let apiRequest: Promise; + + return () => { + if (!apiRequest) { + log.info(`Setting up call to retrieve Synthetics package`); + + // Currently (as of 2020-june) the package registry used in CI is the public one and + // at times it encounters network connection issues. We use `retry.try` below to see if + // subsequent requests get through. + apiRequest = retry.try(() => { + return supertest + .get(INGEST_API_EPM_PACKAGES) + .set('kbn-xsrf', 'xxx') + .expect(200) + .catch((error) => { + return logSupertestApiErrorAndThrow(`Unable to retrieve packages via Ingest!`, error); + }) + .then((response: { body: GetPackagesResponse }) => { + const { body } = response; + const syntheticsPackageInfo = body.response.find( + (epmPackage) => epmPackage.name === 'synthetics' + ); + if (!syntheticsPackageInfo) { + throw new Error( + `Synthetics package was not in response from ${INGEST_API_EPM_PACKAGES}` + ); + } + return Promise.resolve(syntheticsPackageInfo); + }); + }); + } else { + log.info('Using cached retrieval of synthetics package'); + } + return apiRequest; + }; + })(); + + return { + /** + * Returns the synthetics package version for the currently installed package. This version can then + * be used to build URLs for Fleet pages or APIs + */ + async getSyntheticsPackageVersion() { + const syntheticsPackage = await retrieveSyntheticsPackageInfo()!; + + return syntheticsPackage?.version; + }, + + /** + * Retrieves the full Agent policy by id, which mirrors what the Elastic Agent would get + * once they checkin. + */ + async getFullAgentPolicy(agentPolicyId: string): Promise { + let fullAgentPolicy: GetFullAgentPolicyResponse['item']; + try { + const apiResponse: { body: GetFullAgentPolicyResponse } = await supertest + .get(`${INGEST_API_AGENT_POLICIES}/${agentPolicyId}/full`) + .expect(200); + + fullAgentPolicy = apiResponse.body.item; + } catch (error) { + return logSupertestApiErrorAndThrow('Unable to get full Agent policy', error); + } + + return fullAgentPolicy!; + }, + + /** + * Retrieves all the agent policies. + */ + async getAgentPolicyList(): Promise { + let agentPolicyList: GetAgentPoliciesResponse['items']; + try { + const apiResponse: { body: GetAgentPoliciesResponse } = await supertest + .get(INGEST_API_AGENT_POLICIES) + .expect(200); + + agentPolicyList = apiResponse.body.items; + } catch (error) { + return logSupertestApiErrorAndThrow('Unable to get full Agent policy list', error); + } + + return agentPolicyList!; + }, + + /** + * Deletes a policy (Package Policy) by using the policy name + * @param name + */ + async deletePolicyByName(name: string) { + const id = await this.getPackagePolicyIdByName(name); + + if (id) { + try { + const deletePackagePolicyData: DeletePackagePoliciesRequest['body'] = { + packagePolicyIds: [id], + }; + await supertest + .post(INGEST_API_PACKAGE_POLICIES_DELETE) + .set('kbn-xsrf', 'xxx') + .send(deletePackagePolicyData) + .expect(200); + } catch (error) { + logSupertestApiErrorAndThrow( + `Unable to delete Package Policy via Ingest! ${name}`, + error + ); + } + } + }, + + /** + * Gets the policy id (Package Policy) by using the policy name + * @param name + */ + async getPackagePolicyIdByName(name: string) { + const { + body: packagePoliciesResponse, + }: { body: GetPackagePoliciesResponse } = await supertest + .get(INGEST_API_PACKAGE_POLICIES) + .set('kbn-xsrf', 'xxx') + .query({ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: ${name}` }) + .send() + .expect(200); + const packagePolicyList: GetPackagePoliciesResponse['items'] = packagePoliciesResponse.items; + + if (packagePolicyList.length > 1) { + throw new Error(`Found ${packagePolicyList.length} Policies - was expecting only one!`); + } + + if (packagePolicyList.length) { + return packagePolicyList[0].id; + } + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/uptime.ts b/x-pack/test/functional/services/uptime/uptime.ts index b345be012968..1f808d4e5939 100644 --- a/x-pack/test/functional/services/uptime/uptime.ts +++ b/x-pack/test/functional/services/uptime/uptime.ts @@ -15,6 +15,7 @@ import { UptimeAlertsProvider } from './alerts'; import { UptimeMLAnomalyProvider } from './ml_anomaly'; import { UptimeCertProvider } from './certificates'; import { UptimeOverviewProvider } from './overview'; +import { SyntheticsPackageProvider } from './synthetics_package'; export function UptimeProvider(context: FtrProviderContext) { const common = UptimeCommonProvider(context); @@ -25,6 +26,7 @@ export function UptimeProvider(context: FtrProviderContext) { const ml = UptimeMLAnomalyProvider(context); const cert = UptimeCertProvider(context); const overview = UptimeOverviewProvider(context); + const syntheticsPackage = SyntheticsPackageProvider(context); return { common, @@ -35,5 +37,6 @@ export function UptimeProvider(context: FtrProviderContext) { ml, cert, overview, + syntheticsPackage, }; }