From 2cf323a97b9e2246cdd3950d00895563623b053a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Sat, 30 May 2026 00:43:36 +0200 Subject: [PATCH 1/2] [Synthetics] Migrate monitor create/edit API tests to Scout (batch 1) Migrates the synthetics monitor create/edit API integration tests from the deployment-agnostic FTR suite to Scout API tests (part of #263519): - create_monitor (UI create + space namespace) - create_monitor_public_api / _private_location - edit_monitor_public_api / _private_location Shared helpers (omitMonitorKeys, parseMonitorResponse, editMonitor, LOCAL_PUBLIC_LOCATION, http_monitor fixture) are added to the Scout api fixtures. The FTR create_monitor.ts is retained as a helpers-only module because addMonitorAPIHelper/omitMonitorKeys are still used by unmigrated FTR suites; the migrated loadTestFile entries are replaced with "migrated to Scout" pointers. Co-authored-by: Cursor --- .../test/scout/api/fixtures/constants.ts | 18 + .../scout/api/fixtures/data/http_monitor.ts | 89 +++++ .../test/scout/api/fixtures/index.ts | 2 + .../test/scout/api/fixtures/monitors.ts | 66 ++++ .../scout/api/tests/create_monitor.spec.ts | 94 ++++++ .../tests/create_monitor_public_api.spec.ts | 223 +++++++++++++ ...onitor_public_api_private_location.spec.ts | 280 ++++++++++++++++ .../api/tests/edit_monitor_public_api.spec.ts | 170 ++++++++++ ...onitor_public_api_private_location.spec.ts | 286 ++++++++++++++++ .../apis/synthetics/create_monitor.ts | 100 +----- .../synthetics/create_monitor_public_api.ts | 232 ------------- ...ate_monitor_public_api_private_location.ts | 290 ---------------- .../synthetics/edit_monitor_public_api.ts | 215 ------------ ...dit_monitor_public_api_private_location.ts | 309 ------------------ .../apis/synthetics/index.ts | 11 +- 15 files changed, 1245 insertions(+), 1140 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/data/http_monitor.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api.spec.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api_private_location.spec.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api.spec.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api_private_location.spec.ts delete mode 100644 x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api.ts delete mode 100644 x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api_private_location.ts delete mode 100644 x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api.ts delete mode 100644 x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api_private_location.ts diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/constants.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/constants.ts index 359899db5a7c3..1852893afd3d1 100644 --- a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/constants.ts +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/constants.ts @@ -25,6 +25,24 @@ export const SYNTHETICS_API_URLS = { export const PUBLIC_API_VERSION = '2023-10-31'; export const INTERNAL_API_VERSION = '1'; +/** + * Synthetics monitor saved-object types created by the create/edit specs. + * Cleaned in `beforeAll`/`afterAll` so the worker-shared Fleet package and + * private location survive across specs (matching the FTR suite's design). + */ +export const SYNTHETICS_MONITOR_SO_TYPES = ['synthetics-monitor', 'synthetics-monitor-multi-space']; + +/** + * Elastic-managed "local" public location available in the test environment. + * Mirrors `LOCAL_PUBLIC_LOCATION` from the FTR `apis/synthetics/helpers/location.ts`. + */ +export const LOCAL_PUBLIC_LOCATION = { + geo: { lat: 0, lon: 0 }, + id: 'dev', + label: 'Dev Service', + isServiceManaged: true, +} as const; + /** Default headers for Synthetics *public* API requests (versioned). */ export const PUBLIC_API_HEADERS = { 'elastic-api-version': PUBLIC_API_VERSION, diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/data/http_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/data/http_monitor.ts new file mode 100644 index 0000000000000..a3e5e6adfacde --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/data/http_monitor.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ + +/** + * HTTP monitor fixture used by the create-monitor spec. Ported from the FTR + * fixture `apis/synthetics/fixtures/http_monitor.json` (read there via + * `getFixtureJson`). Kept as a typed module so Scout specs can import it + * directly without filesystem reads. + */ +export const httpMonitorFixture: Record = { + type: 'http', + enabled: true, + alert: { + status: { + enabled: true, + }, + }, + tags: ['tag1', 'tag2'], + schedule: { + number: '5', + unit: 'm', + }, + 'service.name': '', + config_id: '', + timeout: '180', + __ui: { + is_tls_enabled: false, + }, + max_attempts: 2, + max_redirects: '3', + password: 'test', + urls: 'https://nextjs-test-synthetics.vercel.app/api/users', + 'url.port': null, + proxy_url: 'http://proxy.com', + proxy_headers: {}, + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'check.response.json': [], + 'response.include_body': 'never', + 'response.include_body_max_bytes': '1024', + 'check.request.headers': { + sampleHeader: 'sampleHeaderValue', + }, + 'response.include_headers': true, + 'check.response.status': ['200', '201'], + 'check.request.body': { + value: 'testValue', + type: 'json', + }, + 'check.response.headers': {}, + 'check.request.method': '', + username: 'test-username', + 'ssl.certificate_authorities': 't.string', + 'ssl.certificate': 't.string', + 'ssl.key': 't.string', + 'ssl.key_passphrase': 't.string', + 'ssl.verification_mode': 'certificate', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2'], + name: 'test-monitor-name', + locations: [ + { + id: 'dev', + label: 'Dev Service', + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }, + ], + namespace: 'testnamespace', + revision: 1, + origin: 'ui', + form_monitor_type: 'http', + journey_id: '', + id: '', + hash: '', + mode: 'any', + ipv4: true, + ipv6: true, + params: '', + labels: {}, + maintenance_windows: [], + spaces: ['default'], +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/index.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/index.ts index 03ce2efc05e3d..a61020f15fd4a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/index.ts +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/index.ts @@ -48,5 +48,7 @@ export { PUBLIC_API_VERSION, INTERNAL_API_VERSION, SYNTHETICS_API_URLS, + SYNTHETICS_MONITOR_SO_TYPES, + LOCAL_PUBLIC_LOCATION, mergeSyntheticsApiHeaders, } from './constants'; diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/monitors.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/monitors.ts index 37a47498ce6ed..03688412ceabf 100644 --- a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/monitors.ts +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/fixtures/monitors.ts @@ -5,8 +5,13 @@ * 2.0. */ +import { omit, omitBy } from 'lodash'; import type { ApiClientFixture } from '@kbn/scout-oblt'; import { expect } from '@kbn/scout-oblt/api'; +import { + removeMonitorEmptyValues, + transformPublicKeys, +} from '../../../../server/routes/monitor_cruds/formatters/saved_object_to_monitor'; import { PUBLIC_API_VERSION } from './constants'; /** @@ -40,3 +45,64 @@ export async function addMonitor( expect(res).toHaveStatusCode(statusCode); return res; } + +/** + * Thin wrapper around `PUT /api/synthetics/monitors/{id}` for Scout API specs. + * + * Mirrors the FTR `editMonitorAPIHelper` from the `edit_monitor_public_api*` + * suites. The caller supplies the auth headers (typically API key + internal + * origin) and the expected `statusCode`. + */ +export async function editMonitor( + apiClient: ApiClientFixture, + headers: Record, + monitorId: string, + monitor: Record | unknown, + opts: { statusCode?: number } = {} +) { + const { statusCode = 200 } = opts; + const res = await apiClient.put(`api/synthetics/monitors/${monitorId}`, { + headers: { ...headers, 'elastic-api-version': PUBLIC_API_VERSION }, + body: monitor, + responseType: 'json', + }); + expect(res).toHaveStatusCode(statusCode); + return res; +} + +/** + * Keys removed from the *expected* monitor input before comparing it against a + * create/edit response. Ported verbatim from the FTR `create_monitor.ts` + * `keyToOmitList`. + */ +export const keyToOmitList = [ + 'created_at', + 'updated_at', + 'id', + 'config_id', + 'form_monitor_type', + 'spaceId', + 'private_locations', +]; + +/** + * Normalizes an *expected* monitor input the same way the public API + * serializes a monitor saved object: applies the public-key transform, drops + * server-generated keys, then strips empty values. Mirrors `omitMonitorKeys` + * from the FTR `create_monitor.ts` so migrated specs can assert deep equality. + */ +export const omitMonitorKeys = (monitor: Record) => + omitBy( + omit(transformPublicKeys(monitor as Parameters[0]), keyToOmitList), + removeMonitorEmptyValues + ); + +const RESPONSE_OMIT_KEYS = ['created_at', 'updated_at', 'id', 'config_id', 'form_monitor_type']; + +/** + * Strips the server-generated fields from a create/edit response body so it + * can be compared against `omitMonitorKeys(expectedInput)`. Mirrors the + * response-side `omit(...)` in the FTR add/edit helpers. + */ +export const parseMonitorResponse = (body: Record) => + omit(body, RESPONSE_OMIT_KEYS); diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.ts new file mode 100644 index 0000000000000..a6d16ebcb721d --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.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 { v4 as uuidv4 } from 'uuid'; +import { expect } from '@kbn/scout-oblt/api'; +import { formatKibanaNamespace } from '../../../../common/formatters'; +import { + apiTest, + mergeSyntheticsApiHeaders, + PUBLIC_API_VERSION, + SYNTHETICS_MONITOR_SO_TYPES, +} from '../fixtures'; +import { addMonitor, omitMonitorKeys, parseMonitorResponse } from '../fixtures/monitors'; +import { httpMonitorFixture } from '../fixtures/data/http_monitor'; + +/** + * Ported from FTR + * `x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor.ts`. + * + * The FTR suite carried `skipCloud`/`skipMKI`; the equivalent Scout coverage is + * local stateful + serverless only. + */ +apiTest.describe( + 'AddNewMonitorsUI', + { + tag: ['@local-stateful-classic', '@local-serverless-observability_complete'], + }, + () => { + let editorHeaders: Record; + let privateLocation: { id: string; label: string }; + let spaceToCleanUp: string | null = null; + + apiTest.beforeAll(async ({ requestAuth, apiServices, kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + const { apiKeyHeader } = await requestAuth.getApiKey('editor'); + editorHeaders = mergeSyntheticsApiHeaders(apiKeyHeader, { Accept: 'application/json' }); + privateLocation = await apiServices.syntheticsPrivateLocations.getSharedPrivateLocation(); + }); + + apiTest.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + if (spaceToCleanUp) { + await kbnClient.spaces.delete(spaceToCleanUp).catch(() => {}); + spaceToCleanUp = null; + } + }); + + apiTest('returns the newly added monitor', async ({ apiClient }) => { + const newMonitor = { + ...httpMonitorFixture, + locations: [privateLocation], + }; + + const res = await addMonitor(apiClient, editorHeaders, newMonitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys(newMonitor) + ); + }); + + apiTest( + 'sets namespace to Kibana space when not set to a custom namespace', + async ({ apiClient, apiServices, kbnClient }) => { + const SPACE_ID = `test-space-${uuidv4()}`; + const SPACE_NAME = `test-space-name ${uuidv4()}`; + const EXPECTED_NAMESPACE = formatKibanaNamespace(SPACE_ID); + spaceToCleanUp = SPACE_ID; + + await kbnClient.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); + const spacePrivateLocation = + await apiServices.syntheticsPrivateLocations.addTestPrivateLocation(SPACE_ID); + + const monitor = { + ...httpMonitorFixture, + namespace: 'default', + locations: [spacePrivateLocation], + spaces: [], + }; + + const res = await apiClient.post(`s/${SPACE_ID}/api/synthetics/monitors`, { + headers: { ...editorHeaders, 'elastic-api-version': PUBLIC_API_VERSION }, + body: monitor, + responseType: 'json', + }); + expect(res).toHaveStatusCode(200); + expect((res.body as Record).namespace).toStrictEqual(EXPECTED_NAMESPACE); + } + ); + } +); diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api.spec.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api.spec.ts new file mode 100644 index 0000000000000..5abcb38a3d8a1 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api.spec.ts @@ -0,0 +1,223 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { expect } from '@kbn/scout-oblt/api'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; +import { + apiTest, + LOCAL_PUBLIC_LOCATION, + mergeSyntheticsApiHeaders, + SYNTHETICS_MONITOR_SO_TYPES, +} from '../fixtures'; +import { addMonitor, omitMonitorKeys, parseMonitorResponse } from '../fixtures/monitors'; + +/** + * Ported from FTR + * `x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api.ts`. + * + * The FTR file grouped cases under per-type nested `describe`s; Scout keeps a + * flat suite (one describe, one `test` per case). The FTR `skipCloud`/`skipMKI` + * tags map to local stateful + serverless coverage. + */ +apiTest.describe( + 'AddNewMonitorsPublicAPI - Public locations', + { + tag: ['@local-stateful-classic', '@local-serverless-observability_complete'], + }, + () => { + let editorHeaders: Record; + + apiTest.beforeAll(async ({ requestAuth, kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + const { apiKeyHeader } = await requestAuth.getApiKey('editor'); + editorHeaders = mergeSyntheticsApiHeaders(apiKeyHeader, { Accept: 'application/json' }); + }); + + apiTest.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + }); + + apiTest('HTTP - returns error for empty monitor', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: [LOCAL_PUBLIC_LOCATION.id], private_locations: [] }, + { statusCode: 400 } + ); + const body = res.body as { message: string; attributes: unknown }; + expect(body.message).toBe('Monitor is not a valid monitor of type http'); + expect(body.attributes).toStrictEqual({ + details: + 'Invalid field "url", must be a non-empty string. | Invalid value "undefined" supplied to "name"', + payload: { type: 'http' }, + }); + }); + + apiTest('HTTP - base http monitor', async ({ apiClient }) => { + const monitor = { + type: 'http', + locations: [LOCAL_PUBLIC_LOCATION.id], + url: 'https://www.google.com', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('HTTP - can enable retries', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + type: 'http', + locations: [LOCAL_PUBLIC_LOCATION.id], + url: 'https://www.google.com', + name, + retest_on_failure: true, + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name, + retest_on_failure: true, + spaces: ['default'], + }) + ); + }); + + apiTest('HTTP - can disable retries', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + type: 'http', + locations: [LOCAL_PUBLIC_LOCATION.id], + url: 'https://www.google.com', + name, + retest_on_failure: false, + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name, + max_attempts: 1, + // this key is not part of the SO and should not be defined + retest_on_failure: undefined, + spaces: ['default'], + }) + ); + }); + + apiTest('TCP - base tcp monitor', async ({ apiClient }) => { + const monitor = { + type: 'tcp', + locations: [LOCAL_PUBLIC_LOCATION.id], + host: 'https://www.google.com/', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.tcp, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name: 'https://www.google.com/', + spaces: ['default'], + }) + ); + }); + + apiTest('ICMP - base icmp monitor', async ({ apiClient }) => { + const monitor = { + type: 'icmp', + locations: [LOCAL_PUBLIC_LOCATION.id], + host: 'https://8.8.8.8', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.icmp, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name: 'https://8.8.8.8', + spaces: ['default'], + }) + ); + }); + + apiTest('Browser - returns error for empty browser monitor', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'browser', locations: [LOCAL_PUBLIC_LOCATION.id], name: 'simple journey' }, + { statusCode: 400 } + ); + + expect(res.body).toStrictEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'Monitor is not a valid monitor of type browser', + attributes: { + details: 'source.inline.script: Script is required for browser monitor.', + payload: { type: 'browser', name: 'simple journey' }, + }, + }); + }); + + apiTest('Browser - base browser monitor', async ({ apiClient }) => { + const monitor = { + type: 'browser', + locations: [LOCAL_PUBLIC_LOCATION.id], + name: 'simple journey', + 'source.inline.script': 'step("simple journey", async () => {});', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.browser, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + spaces: ['default'], + }) + ); + }); + + apiTest('Browser - base browser monitor with inline_script', async ({ apiClient }) => { + const monitor = { + type: 'browser', + locations: [LOCAL_PUBLIC_LOCATION.id], + name: 'simple journey inline_script', + inline_script: 'step("simple journey", async () => {});', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.browser, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + spaces: ['default'], + }) + ); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api_private_location.spec.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api_private_location.spec.ts new file mode 100644 index 0000000000000..533c3abad15ea --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api_private_location.spec.ts @@ -0,0 +1,280 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { expect } from '@kbn/scout-oblt/api'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; +import { LOCATION_REQUIRED_ERROR } from '../../../../server/routes/monitor_cruds/monitor_validation'; +import type { ScoutPrivateLocation } from '../services/synthetics_private_location_api_service'; +import { apiTest, mergeSyntheticsApiHeaders, SYNTHETICS_MONITOR_SO_TYPES } from '../fixtures'; +import { addMonitor, omitMonitorKeys, parseMonitorResponse } from '../fixtures/monitors'; + +/** + * Ported from FTR + * `x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api_private_location.ts`. + * + * Validation cases and per-type create cases share a single private location. + * The FTR file grouped the create cases under per-type nested `describe`s; + * Scout keeps a flat suite. + */ +apiTest.describe( + 'AddNewMonitorsPublicAPI - Private locations', + { + tag: ['@local-stateful-classic', '@local-serverless-observability_complete'], + }, + () => { + let editorHeaders: Record; + let privateLocation: ScoutPrivateLocation; + + apiTest.beforeAll(async ({ requestAuth, apiServices, kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + const { apiKeyHeader } = await requestAuth.getApiKey('editor'); + editorHeaders = mergeSyntheticsApiHeaders(apiKeyHeader, { Accept: 'application/json' }); + privateLocation = await apiServices.syntheticsPrivateLocations.getSharedPrivateLocation(); + }); + + apiTest.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + }); + + apiTest('should return error for empty monitor', async ({ apiClient }) => { + const res = await addMonitor(apiClient, editorHeaders, {}, { statusCode: 400 }); + expect((res.body as { message: string }).message).toBe( + 'Invalid value "undefined" supplied to "type"' + ); + }); + + apiTest('return error if no location specified', async ({ apiClient }) => { + const res = await addMonitor(apiClient, editorHeaders, { type: 'http' }, { statusCode: 400 }); + expect((res.body as { message: string }).message).toBe(LOCATION_REQUIRED_ERROR); + }); + + apiTest('return error if invalid location specified', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: ['mars'] }, + { statusCode: 400 } + ); + expect((res.body as { message: string }).message).toContain( + "Invalid locations specified. Elastic managed Location(s) 'mars' not found." + ); + }); + + apiTest('return error if invalid private location specified', async ({ apiClient }) => { + const wrongKey = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: ['mars'], privateLocations: ['moon'] }, + { statusCode: 400 } + ); + expect((wrongKey.body as { message: string }).message).toBe( + 'Invalid monitor key(s) for http type: privateLocations' + ); + + const notFound = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: ['mars'], private_locations: ['moon'] }, + { statusCode: 400 } + ); + expect((notFound.body as { message: string }).message).toContain( + "Private Location(s) 'moon' not found." + ); + }); + + apiTest('return error for origin project', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: ['dev'], url: 'https://www.google.com', origin: 'project' }, + { statusCode: 400 } + ); + expect((res.body as { message: string }).message).toBe( + 'Unsupported origin type project, only ui type is supported via API.' + ); + }); + + apiTest('HTTP - returns error for empty http', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'http', locations: [], private_locations: [privateLocation.id] }, + { statusCode: 400 } + ); + const body = res.body as { message: string; attributes: unknown }; + expect(body.message).toBe('Monitor is not a valid monitor of type http'); + expect(body.attributes).toStrictEqual({ + details: + 'Invalid field "url", must be a non-empty string. | Invalid value "undefined" supplied to "name"', + payload: { type: 'http' }, + }); + }); + + apiTest('HTTP - base http monitor', async ({ apiClient }) => { + const monitor = { + type: 'http', + private_locations: [privateLocation.id], + url: 'https://www.google.com', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [privateLocation], + name: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('HTTP - can enable retries', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + type: 'http', + private_locations: [privateLocation.id], + url: 'https://www.google.com', + name, + retest_on_failure: true, + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [privateLocation], + name, + retest_on_failure: true, + spaces: ['default'], + }) + ); + }); + + apiTest('HTTP - can disable retries', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + type: 'http', + private_locations: [privateLocation.id], + url: 'https://www.google.com', + name, + retest_on_failure: false, + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.http, + ...monitor, + locations: [privateLocation], + name, + max_attempts: 1, + // this key is not part of the SO and should not be defined + retest_on_failure: undefined, + spaces: ['default'], + }) + ); + }); + + apiTest('TCP - base tcp monitor', async ({ apiClient }) => { + const monitor = { + type: 'tcp', + private_locations: [privateLocation.id], + host: 'https://www.google.com/', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.tcp, + ...monitor, + locations: [privateLocation], + name: 'https://www.google.com/', + spaces: ['default'], + }) + ); + }); + + apiTest('ICMP - base icmp monitor', async ({ apiClient }) => { + const monitor = { + type: 'icmp', + private_locations: [privateLocation.id], + host: 'https://8.8.8.8', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.icmp, + ...monitor, + locations: [privateLocation], + name: 'https://8.8.8.8', + spaces: ['default'], + }) + ); + }); + + apiTest('Browser - returns error for empty browser monitor', async ({ apiClient }) => { + const res = await addMonitor( + apiClient, + editorHeaders, + { type: 'browser', private_locations: [privateLocation.id], name: 'simple journey' }, + { statusCode: 400 } + ); + + expect(res.body).toStrictEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'Monitor is not a valid monitor of type browser', + attributes: { + details: 'source.inline.script: Script is required for browser monitor.', + payload: { type: 'browser', name: 'simple journey' }, + }, + }); + }); + + apiTest('Browser - base browser monitor', async ({ apiClient }) => { + const monitor = { + type: 'browser', + private_locations: [privateLocation.id], + name: 'simple journey', + 'source.inline.script': 'step("simple journey", async () => {});', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.browser, + ...monitor, + locations: [privateLocation], + spaces: ['default'], + }) + ); + }); + + apiTest('Browser - base browser monitor with inline_script', async ({ apiClient }) => { + const monitor = { + type: 'browser', + private_locations: [privateLocation.id], + name: 'simple journey inline_script', + inline_script: 'step("simple journey", async () => {});', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...DEFAULT_FIELDS.browser, + ...monitor, + locations: [privateLocation], + spaces: ['default'], + }) + ); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api.spec.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api.spec.ts new file mode 100644 index 0000000000000..9f33d11602102 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api.spec.ts @@ -0,0 +1,170 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { expect } from '@kbn/scout-oblt/api'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; +import { + apiTest, + LOCAL_PUBLIC_LOCATION, + mergeSyntheticsApiHeaders, + SYNTHETICS_MONITOR_SO_TYPES, +} from '../fixtures'; +import { + addMonitor, + editMonitor, + omitMonitorKeys, + parseMonitorResponse, +} from '../fixtures/monitors'; + +/** + * Ported from FTR + * `x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api.ts`. + * + * This suite edits a single monitor across successive `test()` blocks, so the + * revision counter increments through the file. Scout API specs share + * worker-scoped Kibana state and run in file order, so the chain behaves like + * the original FTR `describe` that relied on shared state between `it`s. + */ +apiTest.describe( + 'EditMonitorsPublicAPI - Public location', + { + tag: ['@local-stateful-classic', '@local-serverless-observability_complete'], + }, + () => { + const defaultFields = DEFAULT_FIELDS.http; + let editorHeaders: Record; + let monitorId = 'test-id'; + const updates: Record = {}; + + apiTest.beforeAll(async ({ requestAuth, apiServices, kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + const { apiKeyHeader } = await requestAuth.getApiKey('editor'); + editorHeaders = mergeSyntheticsApiHeaders(apiKeyHeader, { Accept: 'application/json' }); + await apiServices.syntheticsPrivateLocations.installSyntheticsPackage(); + }); + + apiTest.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + }); + + apiTest('adds test monitor', async ({ apiClient }) => { + const monitor = { + type: 'http', + locations: [LOCAL_PUBLIC_LOCATION.id], + url: 'https://www.google.com', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + monitorId = (res.body as { id: string }).id; + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('can change name of monitor', async ({ apiClient }) => { + updates.name = `updated name ${uuidv4()}`; + const monitor = { name: updates.name }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + ...updates, + locations: [LOCAL_PUBLIC_LOCATION], + revision: 2, + url: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('prevents duplicate name of monitor', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + name, + type: 'http', + locations: [LOCAL_PUBLIC_LOCATION.id], + url: 'https://www.google.com', + }; + // create one monitor with one name + await addMonitor(apiClient, editorHeaders, monitor); + // create another monitor with a different name + const res = await addMonitor(apiClient, editorHeaders, { ...monitor, name: 'test name' }); + const newMonitorId = (res.body as { id: string }).id; + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + locations: [LOCAL_PUBLIC_LOCATION], + name: 'test name', + spaces: ['default'], + }) + ); + + const editResult = await editMonitor( + apiClient, + editorHeaders, + newMonitorId, + { name }, + { statusCode: 400 } + ); + + expect(editResult.body).toStrictEqual({ + statusCode: 400, + error: 'Bad Request', + message: `Monitor name must be unique, "${name}" already exists.`, + attributes: { + details: `Monitor name must be unique, "${name}" already exists.`, + }, + }); + }); + + apiTest('can add a second public location to existing monitor', async ({ apiClient }) => { + const monitor = { locations: [LOCAL_PUBLIC_LOCATION.id, 'dev2'] }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...updates, + revision: 3, + url: 'https://www.google.com', + locations: [ + LOCAL_PUBLIC_LOCATION, + { ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }, + ], + spaces: ['default'], + }) + ); + }); + + apiTest('can remove public location from existing monitor', async ({ apiClient }) => { + const monitor = { locations: ['dev2'] }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...updates, + revision: 4, + url: 'https://www.google.com', + locations: [{ ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }], + spaces: ['default'], + }) + ); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api_private_location.spec.ts b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api_private_location.spec.ts new file mode 100644 index 0000000000000..09dc9dfdba880 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api_private_location.spec.ts @@ -0,0 +1,286 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { omit } from 'lodash'; +import { expect } from '@kbn/scout-oblt/api'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; +import { LOCATION_REQUIRED_ERROR } from '../../../../server/routes/monitor_cruds/monitor_validation'; +import type { ScoutPrivateLocation } from '../services/synthetics_private_location_api_service'; +import { apiTest, mergeSyntheticsApiHeaders, SYNTHETICS_MONITOR_SO_TYPES } from '../fixtures'; +import { + addMonitor, + editMonitor, + omitMonitorKeys, + parseMonitorResponse, +} from '../fixtures/monitors'; + +/** + * Ported from FTR + * `x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api_private_location.ts`. + * + * Validation cases run against a single monitor created up front, then the + * happy-path edits walk that monitor through revisions 2-4. Scout API specs + * share worker-scoped Kibana state and run in file order, so the chain behaves + * like the original FTR `describe` that relied on shared state between `it`s. + */ +apiTest.describe( + 'EditMonitorsPublicAPI - Private Location', + { + tag: ['@local-stateful-classic', '@local-serverless-observability_complete'], + }, + () => { + const defaultFields = DEFAULT_FIELDS.http; + let editorHeaders: Record; + let privateLocation1: ScoutPrivateLocation; + let privateLocation2: ScoutPrivateLocation; + let monitorId = 'test-id'; + const updates: Record = {}; + + apiTest.beforeAll(async ({ requestAuth, apiServices, kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + const { apiKeyHeader } = await requestAuth.getApiKey('editor'); + editorHeaders = mergeSyntheticsApiHeaders(apiKeyHeader, { Accept: 'application/json' }); + privateLocation1 = await apiServices.syntheticsPrivateLocations.getSharedPrivateLocation(); + privateLocation2 = await apiServices.syntheticsPrivateLocations.addTestPrivateLocation(); + }); + + apiTest.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.clean({ types: SYNTHETICS_MONITOR_SO_TYPES }); + }); + + apiTest('adds test monitor', async ({ apiClient }) => { + const monitor = { + type: 'http', + private_locations: [privateLocation1.id], + url: 'https://www.google.com', + }; + const res = await addMonitor(apiClient, editorHeaders, monitor); + monitorId = (res.body as { id: string }).id; + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + locations: [privateLocation1], + name: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('should return error for empty monitor', async ({ apiClient }) => { + const errMessage = 'Monitor must be a non-empty object'; + const testCases = [{}, null, undefined, false, [], '']; + for (const testCase of testCases) { + const res = await editMonitor(apiClient, editorHeaders, monitorId, testCase, { + statusCode: 400, + }); + expect((res.body as { message: string }).message).toBe(errMessage); + } + }); + + apiTest('return error if type is being changed', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { type: 'tcp' }, + { + statusCode: 400, + } + ); + expect((res.body as { message: string }).message).toBe( + 'Monitor type cannot be changed from http to tcp.' + ); + }); + + apiTest('return error if monitor not found', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + 'invalid-monitor-id', + { type: 'tcp' }, + { statusCode: 404 } + ); + expect((res.body as { message: string }).message).toBe( + 'Monitor id invalid-monitor-id not found!' + ); + }); + + apiTest('return error if invalid location specified', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { type: 'http', locations: ['mars'] }, + { statusCode: 400 } + ); + expect((res.body as { message: string }).message).toContain( + "Invalid locations specified. Elastic managed Location(s) 'mars' not found." + ); + }); + + apiTest('return error if invalid private location specified', async ({ apiClient }) => { + const wrongKey = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { type: 'http', locations: ['mars'], privateLocations: ['moon'] }, + { statusCode: 400 } + ); + expect((wrongKey.body as { message: string }).message).toBe( + 'Invalid monitor key(s) for http type: privateLocations' + ); + + const notFound = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { type: 'http', locations: ['mars'], private_locations: ['moon'] }, + { statusCode: 400 } + ); + expect((notFound.body as { message: string }).message).toContain( + "Private Location(s) 'moon' not found." + ); + }); + + apiTest('throws an error if empty locations', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { locations: [], private_locations: [] }, + { statusCode: 400 } + ); + expect((res.body as { message: string }).message).toBe(LOCATION_REQUIRED_ERROR); + }); + + apiTest('cannot change origin type', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { origin: 'project' }, + { statusCode: 400 } + ); + + expect(res.body).toStrictEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'Unsupported origin type project, only ui type is supported via API.', + attributes: { + details: 'Unsupported origin type project, only ui type is supported via API.', + payload: { origin: 'project' }, + }, + }); + }); + + apiTest('can change name of monitor', async ({ apiClient }) => { + updates.name = `updated name ${uuidv4()}`; + const monitor = { name: updates.name }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + ...updates, + locations: [privateLocation1], + revision: 2, + url: 'https://www.google.com', + spaces: ['default'], + }) + ); + }); + + apiTest('prevents duplicate name of monitor', async ({ apiClient }) => { + const name = `test name ${uuidv4()}`; + const monitor = { + name, + type: 'http', + private_locations: [privateLocation1.id], + url: 'https://www.google.com', + }; + // create one monitor with one name + await addMonitor(apiClient, editorHeaders, monitor); + // create another monitor with a different name + const res = await addMonitor(apiClient, editorHeaders, { ...monitor, name: 'test name' }); + const newMonitorId = (res.body as { id: string }).id; + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...monitor, + locations: [privateLocation1], + name: 'test name', + spaces: ['default'], + }) + ); + + const editResult = await editMonitor( + apiClient, + editorHeaders, + newMonitorId, + { name }, + { statusCode: 400 } + ); + + expect(editResult.body).toStrictEqual({ + statusCode: 400, + error: 'Bad Request', + message: `Monitor name must be unique, "${name}" already exists.`, + attributes: { + details: `Monitor name must be unique, "${name}" already exists.`, + }, + }); + }); + + apiTest('can add a second private location to existing monitor', async ({ apiClient }) => { + const monitor = { private_locations: [privateLocation1.id, privateLocation2.id] }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...updates, + revision: 3, + url: 'https://www.google.com', + locations: [omit(privateLocation1, 'spaces'), omit(privateLocation2, 'spaces')], + spaces: ['default'], + }) + ); + }); + + apiTest('can remove private location from existing monitor', async ({ apiClient }) => { + const monitor = { private_locations: [privateLocation2.id] }; + const res = await editMonitor(apiClient, editorHeaders, monitorId, monitor); + + expect(parseMonitorResponse(res.body as Record)).toStrictEqual( + omitMonitorKeys({ + ...defaultFields, + ...updates, + revision: 4, + url: 'https://www.google.com', + locations: [omit(privateLocation2, 'spaces')], + spaces: ['default'], + }) + ); + }); + + apiTest('can not remove all locations', async ({ apiClient }) => { + const res = await editMonitor( + apiClient, + editorHeaders, + monitorId, + { locations: [], private_locations: [] }, + { statusCode: 400 } + ); + expect((res.body as { message: string }).message).toBe(LOCATION_REQUIRED_ERROR); + }); + } +); diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor.ts index 271e558dbd2c5..6a73146831fec 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor.ts @@ -4,26 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +/** + * The `AddNewMonitorsUI` test suite that used to live here was migrated to Scout: + * `x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.ts`. + * + * This module is retained as a helpers-only file because `addMonitorAPIHelper`, + * `keyToOmitList`, and `omitMonitorKeys` are still imported by FTR suites that + * have not been migrated yet (e.g. `enable_default_alerting`, `get_monitor`, + * `sync_global_params`, `create_monitor_private_location`, `edit_private_location`, + * `get_private_location_monitors`). + */ import expect from '@kbn/expect'; import type { RoleCredentials, SamlAuthProviderType } from '@kbn/ftr-common-functional-services'; import moment from 'moment/moment'; -import { v4 as uuidv4 } from 'uuid'; import { omit, omitBy } from 'lodash'; -import type { HTTPFields, PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; -import { ConfigKey } from '@kbn/synthetics-plugin/common/runtime_types'; -import { formatKibanaNamespace } from '@kbn/synthetics-plugin/common/formatters'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { removeMonitorEmptyValues, transformPublicKeys, } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/formatters/saved_object_to_monitor'; -import type { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -import { getFixtureJson } from './helpers/get_fixture_json'; -import { SyntheticsMonitorTestService } from '../../services/synthetics_monitor'; -import { - PrivateLocationTestService, - cleanSyntheticsTestData, -} from '../../services/synthetics_private_location'; export const addMonitorAPIHelper = async ( supertestAPI: any, @@ -77,81 +77,3 @@ export const keyToOmitList = [ export const omitMonitorKeys = (monitor: any) => { return omitBy(omit(transformPublicKeys(monitor), keyToOmitList), removeMonitorEmptyValues); }; - -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('AddNewMonitorsUI', function () { - this.tags(['skipCloud', 'skipMKI']); - const supertestAPI = getService('supertestWithoutAuth'); - const samlAuth = getService('samlAuth'); - const kibanaServer = getService('kibanaServer'); - const monitorTestService = new SyntheticsMonitorTestService(getService); - const privateLocationsService = new PrivateLocationTestService(getService); - - let privateLocation: PrivateLocation; - let _httpMonitorJson: HTTPFields; - let httpMonitorJson: HTTPFields; - let editorRoleAuthc: RoleCredentials; - - const addMonitorAPI = async (monitor: any, statusCode = 200) => { - return addMonitorAPIHelper(supertestAPI, monitor, statusCode, editorRoleAuthc, samlAuth); - }; - - const deleteMonitor = async ( - monitorId?: string | string[], - statusCode = 200, - spaceId?: string - ) => { - return monitorTestService.deleteMonitor(editorRoleAuthc, monitorId, statusCode, spaceId); - }; - - before(async () => { - _httpMonitorJson = getFixtureJson('http_monitor'); - await cleanSyntheticsTestData(kibanaServer); - editorRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - }); - - beforeEach(async () => { - privateLocation = await privateLocationsService.addTestPrivateLocation(); - httpMonitorJson = { - ..._httpMonitorJson, - locations: [privateLocation], - }; - }); - - it('returns the newly added monitor', async () => { - const newMonitor = httpMonitorJson; - - const { body: apiResponse } = await addMonitorAPI(newMonitor); - - expect(apiResponse).eql(omitMonitorKeys(newMonitor)); - }); - - it('sets namespace to Kibana space when not set to a custom namespace', async () => { - const SPACE_ID = `test-space-${uuidv4()}`; - const SPACE_NAME = `test-space-name ${uuidv4()}`; - const EXPECTED_NAMESPACE = formatKibanaNamespace(SPACE_ID); - privateLocation = await privateLocationsService.addTestPrivateLocation(SPACE_ID); - const monitor = { - ...httpMonitorJson, - [ConfigKey.NAMESPACE]: 'default', - locations: [privateLocation], - }; - let monitorId = ''; - - try { - await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); - - const apiResponse = await supertestAPI - .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) - .set(editorRoleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send({ ...monitor, spaces: [] }) - .expect(200); - monitorId = apiResponse.body.id; - expect(apiResponse.body[ConfigKey.NAMESPACE]).eql(EXPECTED_NAMESPACE); - } finally { - await deleteMonitor(monitorId, 200, SPACE_ID); - } - }); - }); -} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api.ts deleted file mode 100644 index 982835b3f28f8..0000000000000 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api.ts +++ /dev/null @@ -1,232 +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 { v4 as uuidv4 } from 'uuid'; -import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import type { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; -import { cleanSyntheticsTestData } from '../../services/synthetics_private_location'; -import { LOCAL_PUBLIC_LOCATION } from './helpers/location'; - -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('AddNewMonitorsPublicAPI - Public locations', function () { - this.tags(['skipCloud', 'skipMKI']); - const supertestAPI = getService('supertestWithoutAuth'); - const kibanaServer = getService('kibanaServer'); - const samlAuth = getService('samlAuth'); - let editorUser: RoleCredentials; - - async function addMonitorAPI(monitor: any, statusCode: number = 200) { - return await addMonitorAPIHelper(supertestAPI, monitor, statusCode, editorUser, samlAuth); - } - - before(async () => { - await cleanSyntheticsTestData(kibanaServer); - editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - }); - - after(async () => { - await cleanSyntheticsTestData(kibanaServer); - }); - - describe('HTTP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.http; - - it('return error empty http', async () => { - const { message, attributes } = await addMonitorAPI( - { - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - private_locations: [], - }, - 400 - ); - - expect(message).eql('Monitor is not a valid monitor of type http'); - expect(attributes).eql({ - details: - 'Invalid field "url", must be a non-empty string. | Invalid value "undefined" supplied to "name"', - payload: { type: 'http' }, - }); - }); - - it('base http monitor', async () => { - const monitor = { - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - url: 'https://www.google.com', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - it('can enable retries', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - url: 'https://www.google.com', - name, - retest_on_failure: true, - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name, - retest_on_failure: true, - spaces: ['default'], - }) - ); - }); - - it('can disable retries', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - url: 'https://www.google.com', - name, - retest_on_failure: false, - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name, - max_attempts: 1, - retest_on_failure: undefined, // this key is not part of the SO and should not be defined - spaces: ['default'], - }) - ); - }); - }); - - describe('TCP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.tcp; - - it('base tcp monitor', async () => { - const monitor = { - type: 'tcp', - locations: [LOCAL_PUBLIC_LOCATION.id], - host: 'https://www.google.com/', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name: 'https://www.google.com/', - spaces: ['default'], - }) - ); - }); - }); - - describe('ICMP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.icmp; - - it('base icmp monitor', async () => { - const monitor = { - type: 'icmp', - locations: [LOCAL_PUBLIC_LOCATION.id], - host: 'https://8.8.8.8', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name: 'https://8.8.8.8', - spaces: ['default'], - }) - ); - }); - }); - - describe('Browser Monitor', () => { - const defaultFields = DEFAULT_FIELDS.browser; - - it('empty browser monitor', async () => { - const monitor = { - type: 'browser', - locations: [LOCAL_PUBLIC_LOCATION.id], - name: 'simple journey', - }; - const result = await addMonitorAPI(monitor, 400); - - expect(result).eql({ - statusCode: 400, - error: 'Bad Request', - message: 'Monitor is not a valid monitor of type browser', - attributes: { - details: 'source.inline.script: Script is required for browser monitor.', - payload: { type: 'browser', name: 'simple journey' }, - }, - }); - }); - - it('base browser monitor', async () => { - const monitor = { - type: 'browser', - locations: [LOCAL_PUBLIC_LOCATION.id], - name: 'simple journey', - 'source.inline.script': 'step("simple journey", async () => {});', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - spaces: ['default'], - }) - ); - }); - - it('base browser monitor with inline_script', async () => { - const monitor = { - type: 'browser', - locations: [LOCAL_PUBLIC_LOCATION.id], - name: 'simple journey inline_script', - inline_script: 'step("simple journey", async () => {});', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - spaces: ['default'], - }) - ); - }); - }); - }); -} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api_private_location.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api_private_location.ts deleted file mode 100644 index f2720c7cbebd8..0000000000000 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/create_monitor_public_api_private_location.ts +++ /dev/null @@ -1,290 +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 rawExpect from 'expect'; -import { v4 as uuidv4 } from 'uuid'; -import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import type { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; -import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { LOCATION_REQUIRED_ERROR } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/monitor_validation'; -import type { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; -import { - PrivateLocationTestService, - cleanSyntheticsTestData, -} from '../../services/synthetics_private_location'; - -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('AddNewMonitorsPublicAPI - Private locations', function () { - const supertestAPI = getService('supertestWithoutAuth'); - const kibanaServer = getService('kibanaServer'); - const samlAuth = getService('samlAuth'); - let editorUser: RoleCredentials; - let privateLocation: PrivateLocation; - const privateLocationTestService = new PrivateLocationTestService(getService); - - async function addMonitorAPI(monitor: any, statusCode: number = 200) { - return await addMonitorAPIHelper(supertestAPI, monitor, statusCode, editorUser, samlAuth); - } - - before(async () => { - await cleanSyntheticsTestData(kibanaServer); - editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - privateLocation = await privateLocationTestService.addTestPrivateLocation(); - }); - - after(async () => { - await cleanSyntheticsTestData(kibanaServer); - }); - - it('should return error for empty monitor', async function () { - const { message } = await addMonitorAPI({}, 400); - expect(message).eql('Invalid value "undefined" supplied to "type"'); - }); - - it('return error if no location specified', async () => { - const { message } = await addMonitorAPI({ type: 'http' }, 400); - expect(message).eql(LOCATION_REQUIRED_ERROR); - }); - - it('return error if invalid location specified', async () => { - const { message } = await addMonitorAPI({ type: 'http', locations: ['mars'] }, 400); - rawExpect(message).toContain( - "Invalid locations specified. Elastic managed Location(s) 'mars' not found." - ); - }); - - it('return error if invalid private location specified', async () => { - const { message } = await addMonitorAPI( - { - type: 'http', - locations: ['mars'], - privateLocations: ['moon'], - }, - 400 - ); - expect(message).eql('Invalid monitor key(s) for http type: privateLocations'); - - const result = await addMonitorAPI( - { - type: 'http', - locations: ['mars'], - private_locations: ['moon'], - }, - 400 - ); - rawExpect(result.message).toContain("Private Location(s) 'moon' not found."); - }); - - it('return error for origin project', async () => { - const { message } = await addMonitorAPI( - { - type: 'http', - locations: ['dev'], - url: 'https://www.google.com', - origin: 'project', - }, - 400 - ); - expect(message).eql('Unsupported origin type project, only ui type is supported via API.'); - }); - - describe('HTTP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.http; - it('return error empty http', async () => { - const { message, attributes } = await addMonitorAPI( - { - type: 'http', - locations: [], - private_locations: [privateLocation.id], - }, - 400 - ); - - expect(message).eql('Monitor is not a valid monitor of type http'); - expect(attributes).eql({ - details: - 'Invalid field "url", must be a non-empty string. | Invalid value "undefined" supplied to "name"', - payload: { type: 'http' }, - }); - }); - - it('base http monitor', async () => { - const monitor = { - type: 'http', - private_locations: [privateLocation.id], - url: 'https://www.google.com', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - name: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - it('can enable retries', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - type: 'http', - private_locations: [privateLocation.id], - url: 'https://www.google.com', - name, - retest_on_failure: true, - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - name, - retest_on_failure: true, - spaces: ['default'], - }) - ); - }); - - it('can disable retries', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - type: 'http', - private_locations: [privateLocation.id], - url: 'https://www.google.com', - name, - retest_on_failure: false, - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - name, - max_attempts: 1, - retest_on_failure: undefined, // this key is not part of the SO and should not be defined - spaces: ['default'], - }) - ); - }); - }); - - describe('TCP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.tcp; - - it('base tcp monitor', async () => { - const monitor = { - type: 'tcp', - private_locations: [privateLocation.id], - host: 'https://www.google.com/', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - name: 'https://www.google.com/', - spaces: ['default'], - }) - ); - }); - }); - - describe('ICMP Monitor', () => { - const defaultFields = DEFAULT_FIELDS.icmp; - - it('base icmp monitor', async () => { - const monitor = { - type: 'icmp', - private_locations: [privateLocation.id], - host: 'https://8.8.8.8', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - name: 'https://8.8.8.8', - spaces: ['default'], - }) - ); - }); - }); - - describe('Browser Monitor', () => { - const defaultFields = DEFAULT_FIELDS.browser; - - it('empty browser monitor', async () => { - const monitor = { - type: 'browser', - private_locations: [privateLocation.id], - name: 'simple journey', - }; - const result = await addMonitorAPI(monitor, 400); - - expect(result).eql({ - statusCode: 400, - error: 'Bad Request', - message: 'Monitor is not a valid monitor of type browser', - attributes: { - details: 'source.inline.script: Script is required for browser monitor.', - payload: { type: 'browser', name: 'simple journey' }, - }, - }); - }); - - it('base browser monitor', async () => { - const monitor = { - type: 'browser', - private_locations: [privateLocation.id], - name: 'simple journey', - 'source.inline.script': 'step("simple journey", async () => {});', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - spaces: ['default'], - }) - ); - }); - - it('base browser monitor with inline_script', async () => { - const monitor = { - type: 'browser', - private_locations: [privateLocation.id], - name: 'simple journey inline_script', - inline_script: 'step("simple journey", async () => {});', - }; - const { body: result } = await addMonitorAPI(monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation], - spaces: ['default'], - }) - ); - }); - }); - }); -} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api.ts deleted file mode 100644 index 1aeaba353b5f0..0000000000000 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api.ts +++ /dev/null @@ -1,215 +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 { v4 as uuidv4 } from 'uuid'; -import { omit } from 'lodash'; -import moment from 'moment'; -import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import type { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; -import { - PrivateLocationTestService, - cleanSyntheticsTestData, -} from '../../services/synthetics_private_location'; -import { LOCAL_PUBLIC_LOCATION } from './helpers/location'; - -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('EditMonitorsPublicAPI - Public location', function () { - this.tags(['skipCloud', 'skipMKI']); - const supertestAPI = getService('supertestWithoutAuth'); - const kibanaServer = getService('kibanaServer'); - const samlAuth = getService('samlAuth'); - const testPrivateLocations = new PrivateLocationTestService(getService); - let editorUser: RoleCredentials; - - async function addMonitorAPI(monitor: any, statusCode: number = 200) { - return await addMonitorAPIHelper(supertestAPI, monitor, statusCode, editorUser, samlAuth); - } - - async function editMonitorAPI(id: string, monitor: any, statusCode: number = 200) { - return await editMonitorAPIHelper(id, monitor, statusCode); - } - - async function editMonitorAPIHelper(monitorId: string, monitor: any, statusCode = 200) { - const result = await supertestAPI - .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(monitor); - - expect(result.status).eql(statusCode, JSON.stringify(result.body)); - - if (statusCode === 200) { - const { - created_at: createdAt, - updated_at: updatedAt, - id, - config_id: configId, - } = result.body; - expect(id).not.empty(); - expect(configId).not.empty(); - expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - return { - rawBody: result.body, - body: { - ...omit(result.body, [ - 'created_at', - 'updated_at', - 'id', - 'config_id', - 'form_monitor_type', - ]), - }, - }; - } - return result.body; - } - - before(async () => { - await cleanSyntheticsTestData(kibanaServer); - editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - await testPrivateLocations.installSyntheticsPackage(); - }); - - after(async () => { - await cleanSyntheticsTestData(kibanaServer); - }); - let monitorId = 'test-id'; - - const defaultFields = DEFAULT_FIELDS.http; - - it('adds test monitor', async () => { - const monitor = { - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - url: 'https://www.google.com', - }; - const { body: result, rawBody } = await addMonitorAPI(monitor); - monitorId = rawBody.id; - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - const updates: any = {}; - - it('can change name of monitor', async () => { - updates.name = `updated name ${uuidv4()}`; - const monitor = { - name: updates.name, - }; - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - ...updates, - locations: [LOCAL_PUBLIC_LOCATION], - revision: 2, - url: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - it('prevents duplicate name of monitor', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - name, - type: 'http', - locations: [LOCAL_PUBLIC_LOCATION.id], - url: 'https://www.google.com', - }; - // create one monitor with one name - await addMonitorAPI(monitor); - // create another monitor with a different name - const { body: result, rawBody } = await addMonitorAPI({ - ...monitor, - name: 'test name', - }); - const newMonitorId = rawBody.id; - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [LOCAL_PUBLIC_LOCATION], - name: 'test name', - spaces: ['default'], - }) - ); - - const editResult = await editMonitorAPI( - newMonitorId, - { - name, - }, - 400 - ); - - expect(editResult).eql({ - statusCode: 400, - error: 'Bad Request', - message: `Monitor name must be unique, "${name}" already exists.`, - attributes: { - details: `Monitor name must be unique, "${name}" already exists.`, - }, - }); - }); - - it('can add a second public location to existing monitor', async () => { - const monitor = { - locations: [LOCAL_PUBLIC_LOCATION.id, 'dev2'], - }; - - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...updates, - revision: 3, - url: 'https://www.google.com', - locations: [ - LOCAL_PUBLIC_LOCATION, - { ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }, - ], - spaces: ['default'], - }) - ); - }); - - it('can remove public location from existing monitor', async () => { - const monitor = { - locations: ['dev2'], - }; - - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...updates, - revision: 4, - url: 'https://www.google.com', - locations: [{ ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }], - spaces: ['default'], - }) - ); - }); - }); -} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api_private_location.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api_private_location.ts deleted file mode 100644 index df5c757694e2e..0000000000000 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/edit_monitor_public_api_private_location.ts +++ /dev/null @@ -1,309 +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 rawExpect from 'expect'; -import { v4 as uuidv4 } from 'uuid'; -import { omit } from 'lodash'; -import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import moment from 'moment'; -import type { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; -import { LOCATION_REQUIRED_ERROR } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/monitor_validation'; -import type { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; -import { - PrivateLocationTestService, - cleanSyntheticsTestData, -} from '../../services/synthetics_private_location'; - -export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('EditMonitorsPublicAPI - Private Location', function () { - const supertestAPI = getService('supertestWithoutAuth'); - const kibanaServer = getService('kibanaServer'); - const samlAuth = getService('samlAuth'); - const testPrivateLocations = new PrivateLocationTestService(getService); - let editorUser: RoleCredentials; - let privateLocation1: PrivateLocation; - let privateLocation2: PrivateLocation; - - async function addMonitorAPI(monitor: any, statusCode: number = 200) { - return await addMonitorAPIHelper(supertestAPI, monitor, statusCode, editorUser, samlAuth); - } - - async function editMonitorAPI(id: string, monitor: any, statusCode: number = 200) { - return await editMonitorAPIHelper(id, monitor, statusCode); - } - - async function editMonitorAPIHelper(monitorId: string, monitor: any, statusCode = 200) { - const result = await supertestAPI - .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`) - .set(editorUser.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .send(monitor); - - expect(result.status).eql(statusCode, JSON.stringify(result.body)); - - if (statusCode === 200) { - const { - created_at: createdAt, - updated_at: updatedAt, - id, - config_id: configId, - } = result.body; - expect(id).not.empty(); - expect(configId).not.empty(); - expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - return { - rawBody: result.body, - body: { - ...omit(result.body, [ - 'created_at', - 'updated_at', - 'id', - 'config_id', - 'form_monitor_type', - ]), - }, - }; - } - return result.body; - } - - before(async () => { - await cleanSyntheticsTestData(kibanaServer); - editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); - await testPrivateLocations.installSyntheticsPackage(); - privateLocation1 = await testPrivateLocations.addTestPrivateLocation(); - privateLocation2 = await testPrivateLocations.addTestPrivateLocation(); - }); - - after(async () => { - await cleanSyntheticsTestData(kibanaServer); - }); - let monitorId = 'test-id'; - - const defaultFields = DEFAULT_FIELDS.http; - - it('adds test monitor', async () => { - const monitor = { - type: 'http', - private_locations: [privateLocation1.id], - url: 'https://www.google.com', - }; - const { body: result, rawBody } = await addMonitorAPI(monitor); - monitorId = rawBody.id; - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation1], - name: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - it('should return error for empty monitor', async function () { - const errMessage = 'Monitor must be a non-empty object'; - const testCases = [{}, null, undefined, false, [], '']; - for (const testCase of testCases) { - const { message } = await editMonitorAPI(monitorId, testCase, 400); - expect(message).eql(errMessage); - } - }); - - it('return error if type is being changed', async () => { - const { message } = await editMonitorAPI(monitorId, { type: 'tcp' }, 400); - expect(message).eql('Monitor type cannot be changed from http to tcp.'); - }); - - it('return error if monitor not found', async () => { - const { message } = await editMonitorAPI('invalid-monitor-id', { type: 'tcp' }, 404); - expect(message).eql('Monitor id invalid-monitor-id not found!'); - }); - - it('return error if invalid location specified', async () => { - const { message } = await editMonitorAPI( - monitorId, - { type: 'http', locations: ['mars'] }, - 400 - ); - rawExpect(message).toContain( - "Invalid locations specified. Elastic managed Location(s) 'mars' not found." - ); - }); - - it('return error if invalid private location specified', async () => { - const { message } = await editMonitorAPI( - monitorId, - { - type: 'http', - locations: ['mars'], - privateLocations: ['moon'], - }, - 400 - ); - expect(message).eql('Invalid monitor key(s) for http type: privateLocations'); - - const result = await editMonitorAPI( - monitorId, - { - type: 'http', - locations: ['mars'], - private_locations: ['moon'], - }, - 400 - ); - rawExpect(result.message).toContain("Private Location(s) 'moon' not found."); - }); - - it('throws an error if empty locations', async () => { - const monitor = { - locations: [], - private_locations: [], - }; - const { message } = await editMonitorAPI(monitorId, monitor, 400); - - expect(message).eql(LOCATION_REQUIRED_ERROR); - }); - - it('cannot change origin type', async () => { - const monitor = { - origin: 'project', - }; - const result = await editMonitorAPI(monitorId, monitor, 400); - - expect(result).eql({ - statusCode: 400, - error: 'Bad Request', - message: 'Unsupported origin type project, only ui type is supported via API.', - attributes: { - details: 'Unsupported origin type project, only ui type is supported via API.', - payload: { origin: 'project' }, - }, - }); - }); - - const updates: any = {}; - - it('can change name of monitor', async () => { - updates.name = `updated name ${uuidv4()}`; - const monitor = { - name: updates.name, - }; - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - ...updates, - locations: [privateLocation1], - revision: 2, - url: 'https://www.google.com', - spaces: ['default'], - }) - ); - }); - - it('prevents duplicate name of monitor', async () => { - const name = `test name ${uuidv4()}`; - const monitor = { - name, - type: 'http', - private_locations: [privateLocation1.id], - url: 'https://www.google.com', - }; - // create one monitor with one name - await addMonitorAPI(monitor); - // create another monitor with a different name - const { body: result, rawBody } = await addMonitorAPI({ - ...monitor, - name: 'test name', - }); - const newMonitorId = rawBody.id; - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...monitor, - locations: [privateLocation1], - name: 'test name', - spaces: ['default'], - }) - ); - - const editResult = await editMonitorAPI( - newMonitorId, - { - name, - }, - 400 - ); - - expect(editResult).eql({ - statusCode: 400, - error: 'Bad Request', - message: `Monitor name must be unique, "${name}" already exists.`, - attributes: { - details: `Monitor name must be unique, "${name}" already exists.`, - }, - }); - }); - - it('can add a second private location to existing monitor', async () => { - const monitor = { - private_locations: [privateLocation1.id, privateLocation2.id], - }; - - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...updates, - revision: 3, - url: 'https://www.google.com', - locations: [omit(privateLocation1, 'spaces'), omit(privateLocation2, 'spaces')], - spaces: ['default'], - }) - ); - }); - - it('can remove private location from existing monitor', async () => { - const monitor = { - private_locations: [privateLocation2.id], - }; - - const { body: result } = await editMonitorAPI(monitorId, monitor); - - expect(result).eql( - omitMonitorKeys({ - ...defaultFields, - ...updates, - revision: 4, - url: 'https://www.google.com', - locations: [omit(privateLocation2, 'spaces')], - spaces: ['default'], - }) - ); - }); - - it('can not remove all locations', async () => { - const monitor = { - locations: [], - private_locations: [], - }; - - const { message } = await editMonitorAPI(monitorId, monitor, 400); - - expect(message).eql(LOCATION_REQUIRED_ERROR); - }); - }); -} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/index.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/index.ts index 56de84a540121..41fdfd44e6a0f 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/index.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/index.ts @@ -26,15 +26,16 @@ export default function ({ loadTestFile, getService }: DeploymentAgnosticFtrProv loadTestFile(require.resolve('./create_monitor_project_private_location')); loadTestFile(require.resolve('./create_monitor_project')); loadTestFile(require.resolve('./create_monitor_project_multi_space.ts')); - loadTestFile(require.resolve('./create_monitor_public_api_private_location')); - loadTestFile(require.resolve('./create_monitor_public_api')); - loadTestFile(require.resolve('./create_monitor')); + // create_monitor_public_api_private_location migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api_private_location.spec.ts + // create_monitor_public_api migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor_public_api.spec.ts + // create_monitor test suite migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/create_monitor.spec.ts + // (create_monitor.ts is retained as a helpers-only module: addMonitorAPIHelper/omitMonitorKeys are still used by unmigrated FTR suites) loadTestFile(require.resolve('./create_update_delete_params')); loadTestFile(require.resolve('./delete_monitor_project')); loadTestFile(require.resolve('./delete_monitor')); loadTestFile(require.resolve('./edit_monitor_private_location')); - loadTestFile(require.resolve('./edit_monitor_public_api_private_location')); - loadTestFile(require.resolve('./edit_monitor_public_api')); + // edit_monitor_public_api_private_location migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api_private_location.spec.ts + // edit_monitor_public_api migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/edit_monitor_public_api.spec.ts loadTestFile(require.resolve('./edit_monitor')); loadTestFile(require.resolve('./enable_default_alerting')); // get_filters migrated to Scout: x-pack/solutions/observability/plugins/synthetics/test/scout/api/tests/get_filters.spec.ts From 2780b214e49554ae01746b12d7349679e9cce41a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 1 Jun 2026 10:51:39 +0200 Subject: [PATCH 2/2] fix CI: register synthetics dev public location on serverless Scout The serverless observability_complete Scout config only set xpack.uptime.service.manifestUrl, so no Elastic-managed `dev` location was created (getServiceLocations requires service.devUrl). The migrated monitor create/edit public-API specs failed on serverless with "Elastic managed Location(s) 'dev' not found". Add devUrl/username/password to match the stateful Scout base config and the FTR deployment-agnostic serverless base config. Co-authored-by: Cursor --- .../serverless/observability_complete.serverless.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/default/serverless/observability_complete.serverless.config.ts b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/default/serverless/observability_complete.serverless.config.ts index 2cdd56bc5305e..72e579709e6f1 100644 --- a/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/default/serverless/observability_complete.serverless.config.ts +++ b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/default/serverless/observability_complete.serverless.config.ts @@ -22,6 +22,11 @@ export const servers: ScoutServerConfig = { ...defaultConfig.kbnTestServer.serverArgs, '--serverless=oblt', '--coreApp.allowDynamicConfigOverrides=true', + // Synthetics service config — matches the FTR deployment-agnostic serverless base + // so the Elastic-managed `dev` public location is available for Synthetics API tests. + '--xpack.uptime.service.password=test', + '--xpack.uptime.service.username=localKibanaIntegrationTestsUser', + '--xpack.uptime.service.devUrl=mockDevUrl', '--xpack.uptime.service.manifestUrl=mockDevUrl', ], },