[Uptime] Monitor crud routes data validation (#120389) (elastic/uptime#410)

* Monitor crud route data validation (io-ts typing).
* Added unit tests to add, edit, delete and get monitor REST endpoints.
This commit is contained in:
Abdul Wahab Zahid 2021-12-17 21:12:03 +01:00 committed by GitHub
parent a2d7a98353
commit c4ffd316a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1155 additions and 136 deletions

View file

@ -30,6 +30,11 @@ export const ServiceLocationCodec = t.interface({
export const ServiceLocationsCodec = t.array(ServiceLocationCodec);
export const ServiceLocationsApiResponseCodec = t.interface({
locations: ServiceLocationsCodec,
});
export type ManifestLocation = t.TypeOf<typeof ManifestLocationCodec>;
export type ServiceLocation = t.TypeOf<typeof ServiceLocationCodec>;
export type ServiceLocations = t.TypeOf<typeof ServiceLocationsCodec>;
export type ManifestLocation = t.TypeOf<typeof ManifestLocationCodec>;
export type ServiceLocationsApiResponse = t.TypeOf<typeof ServiceLocationsApiResponseCodec>;

View file

@ -7,15 +7,16 @@
import * as t from 'io-ts';
import { ConfigKey } from './config_key';
import { ServiceLocationsCodec } from './locations';
import {
DataStreamCodec,
ModeCodec,
ResponseBodyIndexPolicyCodec,
ScheduleUnitCodec,
TLSVersionCodec,
VerificationModeCodec,
} from './monitor_configs';
import { MetadataCodec } from './monitor_meta_data';
import { TLSVersionCodec, VerificationModeCodec } from './monitor_configs';
import { ServiceLocationsCodec } from './locations';
const Schedule = t.interface({
number: t.string,
@ -187,6 +188,7 @@ export type BrowserFields = t.TypeOf<typeof BrowserFieldsCodec>;
export type BrowserSimpleFields = t.TypeOf<typeof BrowserSimpleFieldsCodec>;
export type BrowserAdvancedFields = t.TypeOf<typeof BrowserAdvancedFieldsCodec>;
// MonitorFields, represents any possible monitor type
export const MonitorFieldsCodec = t.intersection([
HTTPFieldsCodec,
TCPFieldsCodec,
@ -196,19 +198,27 @@ export const MonitorFieldsCodec = t.intersection([
export type MonitorFields = t.TypeOf<typeof MonitorFieldsCodec>;
// Monitor, represents one of (Icmp | Tcp | Http | Browser)
export const SyntheticsMonitorCodec = t.union([
HTTPFieldsCodec,
TCPFieldsCodec,
ICMPSimpleFieldsCodec,
BrowserFieldsCodec,
]);
export type SyntheticsMonitor = t.TypeOf<typeof SyntheticsMonitorCodec>;
export const SyntheticsMonitorWithIdCodec = t.intersection([
SyntheticsMonitorCodec,
t.interface({ id: t.string }),
]);
export type SyntheticsMonitorWithId = t.TypeOf<typeof SyntheticsMonitorWithIdCodec>;
export const MonitorManagementListResultCodec = t.type({
monitors: t.array(t.interface({ id: t.string, attributes: MonitorFieldsCodec })),
monitors: t.array(t.interface({ id: t.string, attributes: SyntheticsMonitorCodec })),
page: t.number,
perPage: t.number,
total: t.union([t.number, t.null]),
});
export type MonitorManagementListResult = Omit<
t.TypeOf<typeof MonitorManagementListResultCodec>,
'monitors'
> & {
monitors: Array<{
id: string;
attributes: Partial<MonitorFields>;
}>;
};
export type MonitorManagementListResult = t.TypeOf<typeof MonitorManagementListResultCodec>;

View file

@ -6,7 +6,7 @@
*/
import { SimpleSavedObject } from 'kibana/public';
import { MonitorFields } from '../runtime_types/monitor_management';
import { SyntheticsMonitor } from '../runtime_types';
/** Represents the average monitor duration ms at a point in time. */
export interface MonitorDurationAveragePoint {
@ -32,4 +32,4 @@ export interface MonitorIdParam {
monitorId: string;
}
export type SyntheticsMonitorSavedObject = SimpleSavedObject<MonitorFields>;
export type SyntheticsMonitorSavedObject = SimpleSavedObject<SyntheticsMonitor>;

View file

@ -17,11 +17,9 @@ import {
ThrottlingConfigKey,
ThrottlingSuffix,
ThrottlingSuffixType,
} from '../../../common/runtime_types/monitor_management';
} from '../../../common/runtime_types';
export * from '../../../common/runtime_types/monitor_management';
export type Monitor = Partial<MonitorFields>;
export interface PolicyConfig {
[DataStream.HTTP]: HTTPFields;
[DataStream.TCP]: TCPFields;

View file

@ -10,20 +10,20 @@ import { screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import * as fetchers from '../../../state/api/monitor_management';
import { DataStream, ScheduleUnit } from '../../fleet_package/types';
import { DataStream, HTTPFields, ScheduleUnit, SyntheticsMonitor } from '../../fleet_package/types';
import { ActionBar } from './action_bar';
describe('<ActionBar />', () => {
const setMonitor = jest.spyOn(fetchers, 'setMonitor');
const monitor = {
const monitor: SyntheticsMonitor = {
name: 'test-monitor',
schedule: {
unit: ScheduleUnit.MINUTES,
number: '2',
},
urls: 'https://elastic.co',
type: DataStream.BROWSER,
};
type: DataStream.HTTP,
} as unknown as HTTPFields;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -16,10 +16,10 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { MONITOR_MANAGEMENT } from '../../../../common/constants';
import { setMonitor } from '../../../state/api';
import { Monitor } from '../../fleet_package/types';
import { SyntheticsMonitor } from '../../fleet_package/types';
interface Props {
monitor: Monitor;
monitor: SyntheticsMonitor;
isValid: boolean;
onSave?: () => void;
}

View file

@ -9,8 +9,9 @@ import React from 'react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import { DataStream, ScheduleUnit } from '../../fleet_package/types';
import { DataStream, HTTPFields, ScheduleUnit } from '../../fleet_package/types';
import { MonitorManagementList } from './monitor_list';
import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management';
describe('<ActionBar />', () => {
const setRefresh = jest.fn();
@ -29,7 +30,7 @@ describe('<ActionBar />', () => {
urls: `https://test-${i}.co`,
type: DataStream.HTTP,
tags: [`tag-${i}`],
},
} as HTTPFields,
});
}
const state = {
@ -49,7 +50,7 @@ describe('<ActionBar />', () => {
monitorList: true,
serviceLocations: false,
},
},
} as MonitorManagementListState,
};
it.each(monitors)('navigates to edit monitor flow on edit pencil', (monitor) => {

View file

@ -7,7 +7,7 @@
import React, { useContext, useMemo, useCallback } from 'react';
import { EuiBasicTable, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui';
import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management';
import { MonitorFields } from '../../../../common/runtime_types/monitor_management';
import { MonitorFields } from '../../../../common/runtime_types';
import { UptimeSettingsContext } from '../../../contexts';
import { Actions } from './actions';
import { MonitorTags } from './tags';

View file

@ -9,6 +9,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { useParams } from 'react-router-dom';
import { useTrackPageview, FETCH_STATUS, useFetcher } from '../../../observability/public';
import { MonitorFields } from '../../common/runtime_types';
import { EditMonitorConfig } from '../components/monitor_management/edit_monitor_config';
import { Loader } from '../components/monitor_management/loader/loader';
import { getMonitor } from '../state/api';
@ -24,7 +25,7 @@ export const EditMonitorPage: React.FC = () => {
return getMonitor({ id: Buffer.from(monitorId, 'base64').toString('utf8') });
}, [monitorId]);
const monitor = data?.attributes;
const monitor = data?.attributes as MonitorFields;
const { error: locationsError, loading: locationsLoading } = useLocations();
return (

View file

@ -11,13 +11,20 @@ import {
MonitorManagementListResultCodec,
MonitorManagementListResult,
ServiceLocations,
ServiceLocationsCodec,
SyntheticsMonitor,
ServiceLocationsApiResponseCodec,
} from '../../../common/runtime_types';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { apiService } from './utils';
// TODO, change to monitor runtime type
export const setMonitor = async ({ monitor, id }: { monitor: any; id?: string }): Promise<void> => {
// TODO: Type the return type from runtime types
export const setMonitor = async ({
monitor,
id,
}: {
monitor: SyntheticsMonitor;
id?: string;
}): Promise<void> => {
if (id) {
return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor);
} else {
@ -48,7 +55,7 @@ export const fetchServiceLocations = async (): Promise<ServiceLocations> => {
const { locations } = await apiService.get(
API_URLS.SERVICE_LOCATIONS,
undefined,
ServiceLocationsCodec
ServiceLocationsApiResponseCodec
);
return locations;
};

View file

@ -10,7 +10,7 @@ import { tcpFormatters, TCPFormatMap } from './tcp';
import { icmpFormatters, ICMPFormatMap } from './icmp';
import { browserFormatters, BrowserFormatMap } from './browser';
import { commonFormatters, CommonFormatMap } from './common';
import { DataStream } from '../../../../common/runtime_types/monitor_management';
import { DataStream } from '../../../../common/runtime_types';
type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap;

View file

@ -9,7 +9,8 @@ import axios from 'axios';
import {
ManifestLocation,
ServiceLocations,
} from '../../../common/runtime_types/monitor_management';
ServiceLocationsApiResponse,
} from '../../../common/runtime_types';
export async function getServiceLocations({ manifestUrl }: { manifestUrl: string }) {
const locations: ServiceLocations = [];
@ -25,10 +26,10 @@ export async function getServiceLocations({ manifestUrl }: { manifestUrl: string
});
});
return { locations };
return { locations } as ServiceLocationsApiResponse;
} catch (e) {
return {
locations: [],
};
} as ServiceLocationsApiResponse;
}
}

View file

@ -10,7 +10,7 @@ import { forkJoin, from as rxjsFrom, Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { getServiceLocations } from './get_service_locations';
import { Logger } from '../../../../../../src/core/server';
import { MonitorFields, ServiceLocations } from '../../../common/runtime_types/monitor_management';
import { MonitorFields, ServiceLocations } from '../../../common/runtime_types';
const TEST_SERVICE_USERNAME = 'localKibanaIntegrationTestsUser';

View file

@ -7,8 +7,6 @@
/* eslint-disable max-classes-per-file */
import { ValuesType } from 'utility-types';
import {
CoreStart,
KibanaRequest,
@ -24,15 +22,17 @@ import { UptimeServerSetup } from '../adapters';
import { installSyntheticsIndexTemplates } from '../../rest_api/synthetics_service/install_index_templates';
import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key';
import { getAPIKeyForSyntheticsService } from './get_api_key';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { syntheticsMonitorType } from '../saved_objects/synthetics_monitor';
import { getEsHosts } from './get_es_hosts';
import { UptimeConfig } from '../../../common/config';
import { ServiceAPIClient } from './service_api_client';
import { formatMonitorConfig } from './formatters/format_configs';
import { ConfigKey, MonitorFields } from '../../../common/runtime_types/monitor_management';
export type MonitorFieldsWithID = MonitorFields & { id: string };
import {
ConfigKey,
MonitorFields,
SyntheticsMonitor,
SyntheticsMonitorWithId,
} from '../../../common/runtime_types';
const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE =
'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects';
@ -169,7 +169,7 @@ export class SyntheticsService {
};
}
async pushConfigs(request?: KibanaRequest, configs?: MonitorFieldsWithID[]) {
async pushConfigs(request?: KibanaRequest, configs?: SyntheticsMonitorWithId[]) {
const monitors = this.formatConfigs(configs || (await this.getMonitorConfigs()));
if (monitors.length === 0) {
return;
@ -187,7 +187,7 @@ export class SyntheticsService {
}
}
async deleteConfigs(request: KibanaRequest, configs: MonitorFieldsWithID[]) {
async deleteConfigs(request: KibanaRequest, configs: SyntheticsMonitorWithId[]) {
const data = {
monitors: this.formatConfigs(configs),
output: await this.getOutput(request),
@ -197,20 +197,22 @@ export class SyntheticsService {
async getMonitorConfigs() {
const savedObjectsClient = this.server.savedObjectsClient;
const monitorsSavedObjects = await savedObjectsClient?.find<
SyntheticsMonitorSavedObject['attributes']
>({
if (!savedObjectsClient?.find) {
return [] as SyntheticsMonitorWithId[];
}
const findResult = await savedObjectsClient.find<SyntheticsMonitor>({
type: syntheticsMonitorType,
});
const savedObjectsList = monitorsSavedObjects?.saved_objects ?? [];
return savedObjectsList.map<ValuesType<MonitorFields[]>>(({ attributes, id }) => ({
return (findResult.saved_objects ?? []).map(({ attributes, id }) => ({
...attributes,
id,
}));
})) as SyntheticsMonitorWithId[];
}
formatConfigs(configs: MonitorFields[]) {
formatConfigs(configs: SyntheticsMonitorWithId[]) {
return configs.map((config: Partial<MonitorFields>) =>
formatMonitorConfig(Object.keys(config) as ConfigKey[], config)
);

View file

@ -32,7 +32,7 @@ import { getServiceLocationsRoute } from './synthetics_service/get_service_locat
import {
getAllSyntheticsMonitorRoute,
getSyntheticsMonitorRoute,
} from './synthetics_service/get_monitors';
} from './synthetics_service/get_monitor';
import { addSyntheticsMonitorRoute } from './synthetics_service/add_monitor';
import { editSyntheticsMonitorRoute } from './synthetics_service/edit_monitor';
import { deleteSyntheticsMonitorRoute } from './synthetics_service/delete_monitor';

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { MonitorFields, SyntheticsMonitor } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor';
import { validateMonitor } from './monitor_validation';
export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
method: 'POST',
@ -16,10 +17,20 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
validate: {
body: schema.any(),
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
const monitor = request.body as SyntheticsMonitorSavedObject['attributes'];
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
const monitor: SyntheticsMonitor = request.body as SyntheticsMonitor;
const newMonitor = await savedObjectsClient.create(syntheticsMonitorType, monitor);
const validationResult = validateMonitor(monitor as MonitorFields);
if (!validationResult.valid) {
const { reason: message, details, payload } = validationResult;
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
const newMonitor = await savedObjectsClient.create<SyntheticsMonitor>(
syntheticsMonitorType,
monitor
);
const { syntheticsService } = server;

View file

@ -6,26 +6,27 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { SyntheticsMonitor } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { getMonitorNotFoundResponse } from './service_errors';
export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
method: 'DELETE',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
validate: {
params: schema.object({
monitorId: schema.string(),
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
const { monitorId } = request.params;
const { syntheticsService } = server;
try {
const monitor = await savedObjectsClient.get<SyntheticsMonitorSavedObject['attributes']>(
const monitor = await savedObjectsClient.get<SyntheticsMonitor>(
syntheticsMonitorType,
monitorId
);
@ -34,14 +35,17 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
const errors = await syntheticsService.deleteConfigs(request, [
{ ...monitor.attributes, id: monitorId },
]);
if (errors) {
return errors;
}
return monitorId;
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return 'Not found';
return getMonitorNotFoundResponse(response, monitorId);
}
throw getErr;
}
},

View file

@ -4,12 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsUpdateResponse } from 'kibana/server';
import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { MonitorFields, SyntheticsMonitor } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor';
import { validateMonitor } from './monitor_validation';
import { getMonitorNotFoundResponse } from './service_errors';
// Simplify return promise type and type it with runtime_types
export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
method: 'PUT',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
@ -19,26 +25,43 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
}),
body: schema.any(),
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
const monitor = request.body as SyntheticsMonitorSavedObject['attributes'];
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
const monitor = request.body as SyntheticsMonitor;
const validationResult = validateMonitor(monitor as MonitorFields);
if (!validationResult.valid) {
const { reason: message, details, payload } = validationResult;
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
const { monitorId } = request.params;
const { syntheticsService } = server;
const editMonitor = await savedObjectsClient.update(syntheticsMonitorType, monitorId, monitor);
try {
const editMonitor: SavedObjectsUpdateResponse<MonitorFields> =
await savedObjectsClient.update(syntheticsMonitorType, monitorId, monitor);
const errors = await syntheticsService.pushConfigs(request, [
{
...(editMonitor.attributes as SyntheticsMonitorSavedObject['attributes']),
id: editMonitor.id,
},
]);
const errors = await syntheticsService.pushConfigs(request, [
{
...(editMonitor.attributes as SyntheticsMonitor),
id: editMonitor.id,
},
]);
if (errors) {
return errors;
// Return service sync errors in OK response
if (errors) {
return errors;
}
return editMonitor;
} catch (updateErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(updateErr)) {
return getMonitorNotFoundResponse(response, monitorId);
}
throw updateErr;
}
return editMonitor;
},
});

View file

@ -4,22 +4,33 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor';
import { getMonitorNotFoundResponse } from './service_errors';
export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
validate: {
params: schema.object({
monitorId: schema.string(),
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, savedObjectsClient }): Promise<any> => {
handler: async ({ request, response, savedObjectsClient }): Promise<any> => {
const { monitorId } = request.params;
return await savedObjectsClient.get(syntheticsMonitorType, monitorId);
try {
return await savedObjectsClient.get(syntheticsMonitorType, monitorId);
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return getMonitorNotFoundResponse(response, monitorId);
}
throw getErr;
}
},
});

View file

@ -0,0 +1,440 @@
/*
* 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 {
BrowserAdvancedFields,
BrowserFields,
BrowserSimpleFields,
CommonFields,
ConfigKey,
DataStream,
HTTPAdvancedFields,
HTTPFields,
HTTPSimpleFields,
ICMPSimpleFields,
Metadata,
Mode,
MonitorFields,
ResponseBodyIndexPolicy,
ScheduleUnit,
TCPAdvancedFields,
TCPFields,
TCPSimpleFields,
TLSFields,
TLSVersion,
VerificationMode,
ZipUrlTLSFields,
} from '../../../common/runtime_types';
import { validateMonitor } from './monitor_validation';
describe('validateMonitor', () => {
let testSchedule;
let testTags: string[];
let testCommonFields: CommonFields;
let testMetaData: Metadata;
let testICMPFields: ICMPSimpleFields;
let testTCPSimpleFields: TCPSimpleFields;
let testTCPAdvancedFields: TCPAdvancedFields;
let testTCPFields: TCPFields;
let testTLSFields: TLSFields;
let testHTTPSimpleFields: HTTPSimpleFields;
let testHTTPAdvancedFields: HTTPAdvancedFields;
let testHTTPFields: HTTPFields;
let testZipUrlTLSFields: ZipUrlTLSFields;
let testBrowserSimpleFields: BrowserSimpleFields;
let testBrowserAdvancedFields: BrowserAdvancedFields;
let testBrowserFields: BrowserFields;
beforeEach(() => {
testSchedule = { number: '5', unit: ScheduleUnit.MINUTES };
testTags = ['tag1', 'tag2'];
testCommonFields = {
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
[ConfigKey.NAME]: 'test-monitor-name',
[ConfigKey.ENABLED]: true,
[ConfigKey.TAGS]: testTags,
[ConfigKey.SCHEDULE]: testSchedule,
[ConfigKey.APM_SERVICE_NAME]: '',
[ConfigKey.TIMEOUT]: '3m',
[ConfigKey.LOCATIONS]: [
{
id: 'eu-west-1',
label: 'EU West',
geo: {
lat: 33.4354332,
lon: 73.4453553,
},
url: 'https://test-url.com',
},
],
};
testMetaData = {
is_tls_enabled: false,
is_zip_url_tls_enabled: false,
script_source: {
is_generated_script: false,
file_name: 'test-file.name',
},
};
testICMPFields = {
...testCommonFields,
[ConfigKey.HOSTS]: 'test-hosts',
[ConfigKey.WAIT]: '',
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
};
testTLSFields = {
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: 't.string',
[ConfigKey.TLS_CERTIFICATE]: 't.string',
[ConfigKey.TLS_KEY]: 't.string',
[ConfigKey.TLS_KEY_PASSPHRASE]: 't.string',
[ConfigKey.TLS_VERIFICATION_MODE]: VerificationMode.CERTIFICATE,
[ConfigKey.TLS_VERSION]: [TLSVersion.ONE_ONE, TLSVersion.ONE_TWO],
};
testTCPSimpleFields = {
...testCommonFields,
[ConfigKey.METADATA]: testMetaData,
[ConfigKey.HOSTS]: 'https://host1.com',
};
testTCPAdvancedFields = {
[ConfigKey.PROXY_URL]: 'http://proxy-url.com',
[ConfigKey.PROXY_USE_LOCAL_RESOLVER]: false,
[ConfigKey.RESPONSE_RECEIVE_CHECK]: '',
[ConfigKey.REQUEST_SEND_CHECK]: '',
};
testTCPFields = {
...testTCPSimpleFields,
...testTCPAdvancedFields,
...testTLSFields,
[ConfigKey.MONITOR_TYPE]: DataStream.TCP,
};
testHTTPSimpleFields = {
...testCommonFields,
[ConfigKey.METADATA]: testMetaData,
[ConfigKey.MAX_REDIRECTS]: '3',
[ConfigKey.URLS]: 'https://example.com',
};
testHTTPAdvancedFields = {
[ConfigKey.PASSWORD]: 'test',
[ConfigKey.PROXY_URL]: 'http://proxy.com',
[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: [],
[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: [],
[ConfigKey.RESPONSE_BODY_INDEX]: ResponseBodyIndexPolicy.NEVER,
[ConfigKey.RESPONSE_HEADERS_CHECK]: {},
[ConfigKey.RESPONSE_HEADERS_INDEX]: true,
[ConfigKey.RESPONSE_STATUS_CHECK]: ['200', '201'],
[ConfigKey.REQUEST_BODY_CHECK]: { value: 'testValue', type: Mode.JSON },
[ConfigKey.REQUEST_HEADERS_CHECK]: {},
[ConfigKey.REQUEST_METHOD_CHECK]: '',
[ConfigKey.USERNAME]: 'test-username',
};
testHTTPFields = {
...testHTTPSimpleFields,
...testHTTPAdvancedFields,
...testTLSFields,
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
};
testZipUrlTLSFields = {
[ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: 'test',
[ConfigKey.ZIP_URL_TLS_CERTIFICATE]: 'test',
[ConfigKey.ZIP_URL_TLS_KEY]: 'key',
[ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: 'passphrase',
[ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: VerificationMode.STRICT,
[ConfigKey.ZIP_URL_TLS_VERSION]: [TLSVersion.ONE_ONE, TLSVersion.ONE_TWO],
};
testBrowserSimpleFields = {
...testZipUrlTLSFields,
...testCommonFields,
[ConfigKey.METADATA]: testMetaData,
[ConfigKey.SOURCE_INLINE]: '',
[ConfigKey.SOURCE_ZIP_URL]: '',
[ConfigKey.SOURCE_ZIP_FOLDER]: '',
[ConfigKey.SOURCE_ZIP_USERNAME]: 'test-username',
[ConfigKey.SOURCE_ZIP_PASSWORD]: 'password',
[ConfigKey.SOURCE_ZIP_PROXY_URL]: 'http://proxy-url.com',
[ConfigKey.PARAMS]: '',
};
testBrowserAdvancedFields = {
[ConfigKey.SYNTHETICS_ARGS]: ['arg1', 'arg2'],
[ConfigKey.SCREENSHOTS]: 'false',
[ConfigKey.JOURNEY_FILTERS_MATCH]: 'false',
[ConfigKey.JOURNEY_FILTERS_TAGS]: testTags,
[ConfigKey.IGNORE_HTTPS_ERRORS]: false,
[ConfigKey.IS_THROTTLING_ENABLED]: true,
[ConfigKey.DOWNLOAD_SPEED]: '5',
[ConfigKey.UPLOAD_SPEED]: '3',
[ConfigKey.LATENCY]: '20',
[ConfigKey.THROTTLING_CONFIG]: '5d/3u/20l',
};
testBrowserFields = {
...testBrowserSimpleFields,
...testBrowserAdvancedFields,
[ConfigKey.MONITOR_TYPE]: DataStream.BROWSER,
};
});
describe('should invalidate', () => {
it(`when 'type' is null or undefined`, () => {
const testMonitor = { type: undefined } as unknown as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: false,
reason: 'Monitor type is invalid',
details: expect.stringMatching(/(?=.*invalid)(?=.*DataStream)/i),
});
});
it(`when 'type' is not an acceptable monitor type (DataStream)`, () => {
const monitor = { type: 'non-HTTP' } as unknown as MonitorFields;
const result = validateMonitor(monitor);
expect(result).toMatchObject({
valid: false,
reason: 'Monitor type is invalid',
details: expect.stringMatching(/(?=.*invalid)(?=.*non-HTTP)(?=.*DataStream)/i),
});
});
});
describe('should validate', () => {
it('when payload is a correct ICMP monitor', () => {
const testMonitor = testICMPFields as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
it('when payload is a correct TCP monitor', () => {
const testMonitor = testTCPFields as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
it('when payload is a correct HTTP monitor', () => {
const testMonitor = testHTTPFields as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
it('when payload is a correct Browser monitor', () => {
const testMonitor = testBrowserFields as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
});
describe('should invalidate when incomplete properties are received', () => {
it('for ICMP monitor', () => {
const testMonitor = {
...testICMPFields,
...({
[ConfigKey.HOSTS]: undefined,
} as unknown as Partial<ICMPSimpleFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.HOSTS));
expect(result).toMatchObject({
valid: false,
reason: `Monitor is not a valid monitor of type ${DataStream.ICMP}`,
payload: testMonitor,
});
});
it('for TCP monitor', () => {
const testMonitor = {
...testTCPFields,
...({
[ConfigKey.TIMEOUT]: undefined,
} as unknown as Partial<TCPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.TIMEOUT));
expect(result).toMatchObject({
valid: false,
reason: `Monitor is not a valid monitor of type ${DataStream.TCP}`,
payload: testMonitor,
});
});
it('for HTTP monitor', () => {
const testMonitor = {
...testHTTPFields,
...({
[ConfigKey.URLS]: undefined,
} as unknown as Partial<HTTPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.URLS));
expect(result).toMatchObject({
valid: false,
reason: `Monitor is not a valid monitor of type ${DataStream.HTTP}`,
payload: testMonitor,
});
});
it('for Browser monitor', () => {
const testMonitor = {
...testBrowserFields,
...({
[ConfigKey.SOURCE_INLINE]: undefined,
} as unknown as Partial<BrowserFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.SOURCE_INLINE));
expect(result).toMatchObject({
valid: false,
reason: `Monitor is not a valid monitor of type ${DataStream.BROWSER}`,
payload: testMonitor,
});
});
});
// The following should fail when strict typing/validation is in place
describe('should pass validation when mixed props', () => {
it('of HTTP is provided into TCP', () => {
const testMonitor = {
...testTCPFields,
...({
[ConfigKey.RESPONSE_HEADERS_CHECK]: undefined,
} as unknown as Partial<TCPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
});
describe('should validate payload', () => {
it('when parsed from serialized JSON', () => {
const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor(testMonitor);
expect(result).toMatchObject({
valid: true,
reason: '',
details: '',
payload: testMonitor,
});
});
});
});
function getJsonPayload() {
const json =
'{' +
' "type": "http",' +
' "enabled": true, ' +
' "tags": [' +
' "tag1",' +
' "tag2"' +
' ],' +
' "schedule": {' +
' "number": "5",' +
' "unit": "m"' +
' },' +
' "service.name": "",' +
' "timeout": "3m",' +
' "__ui": {' +
' "is_tls_enabled": false,' +
' "is_zip_url_tls_enabled": false,' +
' "script_source": {' +
' "is_generated_script": false,' +
' "file_name": "test-file.name"' +
' }' +
' },' +
' "max_redirects": "3",' +
' "password": "test",' +
' "urls": "https://nextjs-test-synthetics.vercel.app/api/users",' +
' "proxy_url": "http://proxy.com",' +
' "check.response.body.negative": [],' +
' "check.response.body.positive": [],' +
' "response.include_body": "never",' +
' "check.response.headers": {},' +
' "response.include_headers": true,' +
' "check.response.status": [' +
' "200",' +
' "201"' +
' ],' +
' "check.request.body": {' +
' "value": "testValue",' +
' "type": "json"' +
' },' +
' "check.request.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": "eu-west-01",' +
' "label": "Europe West",' +
' "geo": {' +
' "lat": 33.2343132435,' +
' "lon": 73.2342343434' +
' },' +
' "url": "https://example-url.com"' +
' }]' +
'}';
return JSON.parse(json);
}

View file

@ -0,0 +1,81 @@
/*
* 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 { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import {
BrowserFieldsCodec,
ConfigKey,
DataStream,
DataStreamCodec,
HTTPFieldsCodec,
ICMPSimpleFieldsCodec,
MonitorFields,
TCPFieldsCodec,
} from '../../../common/runtime_types';
type MonitorCodecType =
| typeof ICMPSimpleFieldsCodec
| typeof TCPFieldsCodec
| typeof HTTPFieldsCodec
| typeof BrowserFieldsCodec;
const monitorTypeToCodecMap: Record<DataStream, MonitorCodecType> = {
[DataStream.ICMP]: ICMPSimpleFieldsCodec,
[DataStream.TCP]: TCPFieldsCodec,
[DataStream.HTTP]: HTTPFieldsCodec,
[DataStream.BROWSER]: BrowserFieldsCodec,
};
/**
* Validates monitor fields with respect to the relevant Codec identified by object's 'type' property.
* @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI.
*/
export function validateMonitor(monitorFields: MonitorFields): {
valid: boolean;
reason: string;
details: string;
payload: object;
} {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
const decodedType = DataStreamCodec.decode(monitorType);
if (isLeft(decodedType)) {
return {
valid: false,
reason: `Monitor type is invalid`,
details: PathReporter.report(decodedType).join(' | '),
payload: monitorFields,
};
}
const codec = monitorTypeToCodecMap[monitorType];
if (!codec) {
return {
valid: false,
reason: `Payload is not a valid monitor object`,
details: '',
payload: monitorFields,
};
}
// Cast it to ICMPCodec to satisfy typing. During runtime, correct codec will be used to decode.
const decodedMonitor = (codec as typeof ICMPSimpleFieldsCodec).decode(monitorFields);
if (isLeft(decodedMonitor)) {
return {
valid: false,
reason: `Monitor is not a valid monitor of type ${monitorType}`,
details: PathReporter.report(decodedMonitor).join(' | '),
payload: monitorFields,
};
}
return { valid: true, reason: '', details: '', payload: monitorFields };
}

View file

@ -0,0 +1,12 @@
/*
* 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 { KibanaResponseFactory } from '../../../../../../src/core/server';
export function getMonitorNotFoundResponse(response: KibanaResponseFactory, monitorId: string) {
return response.notFound({ body: { message: `Monitor id ${monitorId} not found!` } });
}

View file

@ -5,19 +5,29 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { HTTPFields } from '../../../../../plugins/uptime/common/runtime_types';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('add synthetics monitor', () => {
describe('[POST] /internal/uptime/service/monitors', () => {
const supertest = getService('supertest');
const newMonitor = {
type: 'http',
name: 'Test monitor',
urls: 'https://www.elastic.co',
};
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
before(() => {
_httpMonitorJson = getFixtureJson('http_monitor');
});
beforeEach(() => {
httpMonitorJson = _httpMonitorJson;
});
it('returns the newly added monitor', async () => {
const newMonitor = httpMonitorJson;
const apiResponse = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
@ -25,5 +35,29 @@ export default function ({ getService }: FtrProviderContext) {
expect(apiResponse.body.attributes).eql(newMonitor);
});
it('returns bad request if payload is invalid for HTTP monitor', async () => {
// Delete a required property to make payload invalid
const newMonitor = { ...httpMonitorJson, 'check.request.headers': undefined };
const apiResponse = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor);
expect(apiResponse.status).eql(400);
});
it('returns bad request if monitor type is invalid', async () => {
const newMonitor = { ...httpMonitorJson, type: 'invalid-data-steam' };
const apiResponse = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor);
expect(apiResponse.status).eql(400);
expect(apiResponse.body.message).eql('Monitor type is invalid');
});
});
}

View file

@ -6,31 +6,77 @@
*/
import expect from '@kbn/expect';
import { HTTPFields, MonitorFields } from '../../../../../plugins/uptime/common/runtime_types';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('delete synthetics monitor', () => {
describe('[DELETE] /internal/uptime/service/monitors', () => {
const supertest = getService('supertest');
const newMonitor = {
type: 'http',
name: 'Test monitor',
urls: 'https://www.elastic.co',
};
it('deleted monitor by id', async () => {
const apiResponse = await supertest
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
const saveMonitor = async (monitor: MonitorFields) => {
const res = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor);
.send(monitor);
const monitorId = apiResponse.body.id;
return res.body;
};
before(() => {
_httpMonitorJson = getFixtureJson('http_monitor');
});
beforeEach(() => {
httpMonitorJson = _httpMonitorJson;
});
it('deletes monitor by id', async () => {
const { id: monitorId } = await saveMonitor(httpMonitorJson as MonitorFields);
const deleteResponse = await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true');
//
expect(deleteResponse.body).eql(monitorId);
// Hit get endpoint and expect 404 as well
await supertest.get(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId).expect(404);
});
it('returns 404 if monitor id is not found', async () => {
const invalidMonitorId = 'invalid-id';
const expected404Message = `Monitor id ${invalidMonitorId} not found!`;
const deleteResponse = await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + invalidMonitorId)
.set('kbn-xsrf', 'true');
expect(deleteResponse.status).eql(404);
expect(deleteResponse.body.message).eql(expected404Message);
});
it('validates empty monitor id', async () => {
const emptyMonitorId = '';
// Route DELETE '/${SYNTHETICS_MONITORS}' should not exist
await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + emptyMonitorId)
.set('kbn-xsrf', 'true')
.expect(404);
});
it('validates param length for sanity', async () => {
const veryLargeMonId = new Array(1050).fill('1').join('');
await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + veryLargeMonId)
.set('kbn-xsrf', 'true')
.expect(400);
});
});
}

View file

@ -5,33 +5,108 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { SimpleSavedObject } from 'kibana/public';
import {
ConfigKey,
HTTPFields,
MonitorFields,
} from '../../../../../plugins/uptime/common/runtime_types';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('edit synthetics monitor', () => {
describe('[PUT] /internal/uptime/service/monitors', () => {
const supertest = getService('supertest');
const newMonitor = {
type: 'http',
name: 'Test monitor',
urls: 'https://www.elastic.co',
};
it('edits the monitor', async () => {
const apiResponse = await supertest
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
const saveMonitor = async (monitor: MonitorFields) => {
const res = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor);
.send(monitor)
.expect(200);
const monitorId = apiResponse.body.id;
return res.body as SimpleSavedObject<MonitorFields>;
};
expect(apiResponse.body.attributes).eql(newMonitor);
before(() => {
_httpMonitorJson = getFixtureJson('http_monitor');
});
beforeEach(() => {
httpMonitorJson = { ..._httpMonitorJson };
});
it('edits the monitor', async () => {
const newMonitor = httpMonitorJson;
const { id: monitorId, attributes: savedMonitor } = await saveMonitor(
newMonitor as MonitorFields
);
expect(savedMonitor).eql(newMonitor);
const updates: Partial<HTTPFields> = {
[ConfigKey.URLS]: 'https://modified-host.com',
[ConfigKey.NAME]: 'Modified name',
};
const modifiedMonitor = { ...savedMonitor, ...updates };
const editResponse = await supertest
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.send({ ...newMonitor, name: 'New name' });
.send(modifiedMonitor)
.expect(200);
expect(editResponse.body.attributes.name).eql('New name');
expect(editResponse.body.attributes).eql(modifiedMonitor);
});
it('returns 404 if monitor id is not present', async () => {
const invalidMonitorId = 'invalid-id';
const expected404Message = `Monitor id ${invalidMonitorId} not found!`;
const editResponse = await supertest
.put(API_URLS.SYNTHETICS_MONITORS + '/' + invalidMonitorId)
.set('kbn-xsrf', 'true')
.send(httpMonitorJson)
.expect(404);
expect(editResponse.body.message).eql(expected404Message);
});
it('returns bad request if payload is invalid for HTTP monitor', async () => {
const { id: monitorId, attributes: savedMonitor } = await saveMonitor(
httpMonitorJson as MonitorFields
);
// Delete a required property to make payload invalid
const toUpdate = { ...savedMonitor, 'check.request.headers': undefined };
const apiResponse = await supertest
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.send(toUpdate);
expect(apiResponse.status).eql(400);
});
it('returns bad request if monitor type is invalid', async () => {
const { id: monitorId, attributes: savedMonitor } = await saveMonitor(
httpMonitorJson as MonitorFields
);
const toUpdate = { ...savedMonitor, type: 'invalid-data-steam' };
const apiResponse = await supertest
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.send(toUpdate);
expect(apiResponse.status).eql(400);
expect(apiResponse.body.message).eql('Monitor type is invalid');
});
});
}

View file

@ -0,0 +1,41 @@
{
"type": "browser",
"enabled": true,
"schedule": {
"number": "3",
"unit": "m"
},
"service.name": "",
"tags": [
"cookie-test",
"browser"
],
"timeout": "16",
"__ui": {
"script_source": {
"is_generated_script": false,
"file_name": ""
},
"is_zip_url_tls_enabled": false,
"is_tls_enabled": false
},
"source.zip_url.url": "",
"source.zip_url.username": "",
"source.zip_url.password": "",
"source.zip_url.folder": "",
"source.zip_url.proxy_url": "",
"source.inline.script": "step(\"Visit /users api route\", async () => {\\n const response = await page.goto('https://nextjs-test-synthetics.vercel.app/api/users');\\n expect(response.status()).toEqual(200);\\n});",
"params": "",
"screenshots": "on",
"synthetics_args": [],
"filter_journeys.match": "",
"filter_journeys.tags": [],
"ignore_https_errors": false,
"throttling.is_enabled": true,
"throttling.download_speed": "5",
"throttling.upload_speed": "3",
"throttling.latency": "20",
"throttling.config": "5d/3u/20l",
"locations": [],
"name": "Test HTTP Monitor 03"
}

View file

@ -0,0 +1,61 @@
{
"type": "http",
"enabled": true,
"tags": [
"tag1",
"tag2"
],
"schedule": {
"number": "5",
"unit": "m"
},
"service.name": "",
"timeout": "3m",
"__ui": {
"is_tls_enabled": false,
"is_zip_url_tls_enabled": false,
"script_source": {
"is_generated_script": false,
"file_name": "test-file.name"
}
},
"max_redirects": "3",
"password": "test",
"urls": "https://nextjs-test-synthetics.vercel.app/api/users",
"proxy_url": "http://proxy.com",
"check.response.body.negative": [],
"check.response.body.positive": [],
"response.include_body": "never",
"check.response.headers": {},
"response.include_headers": true,
"check.response.status": [
"200",
"201"
],
"check.request.body": {
"value": "testValue",
"type": "json"
},
"check.request.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": "eu-west-01",
"label": "Europe West",
"geo": {
"lat": 33.2343132435,
"lon": 73.2342343434
},
"url": "https://example-url.com"
}]
}

View file

@ -0,0 +1,35 @@
{
"type": "tcp",
"locations": [],
"enabled": true,
"schedule": {
"number": "3",
"unit": "m"
},
"service.name": "example-service-name",
"tags": [
"tagT1",
"tagT2"
],
"timeout": "16",
"__ui": {
"is_tls_enabled": true,
"is_zip_url_tls_enabled": false
},
"hosts": "192.33.22.111:3333",
"proxy_url": "",
"proxy_use_local_resolver": false,
"check.receive": "",
"check.send": "",
"ssl.certificate_authorities": "",
"ssl.certificate": "",
"ssl.key": "",
"ssl.key_passphrase": "",
"ssl.verification_mode": "full",
"ssl.supported_protocols": [
"TLSv1.1",
"TLSv1.2",
"TLSv1.3"
],
"name": "Test HTTP Monitor 04"
}

View file

@ -0,0 +1,31 @@
{
"type": "tcp",
"locations": [],
"enabled": true,
"schedule": {
"number": "3",
"unit": "m"
},
"service.name": "",
"tags": [],
"timeout": "16",
"__ui": {
"is_tls_enabled": true,
"is_zip_url_tls_enabled": false
},
"hosts": "example-host:40",
"proxy_url": "",
"proxy_use_local_resolver": false,
"check.receive": "",
"check.send": "",
"ssl.certificate_authorities": "",
"ssl.certificate": "",
"ssl.key": "",
"ssl.key_passphrase": "examplepassphrase",
"ssl.verification_mode": "full",
"ssl.supported_protocols": [
"TLSv1.1",
"TLSv1.3"
],
"name": "Test HTTP Monitor 04"
}

View file

@ -6,46 +6,114 @@
*/
import expect from '@kbn/expect';
import { SimpleSavedObject } from 'kibana/public';
import { MonitorFields } from '../../../../../plugins/uptime/common/runtime_types';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('get synthetics monitor', () => {
const newMonitor = {
type: 'http',
name: 'Test monitor',
urls: 'https://www.elastic.co',
};
describe('[GET] /internal/uptime/service/monitors', () => {
const supertest = getService('supertest');
const addMonitor = async () => {
let _monitors: MonitorFields[];
let monitors: MonitorFields[];
const saveMonitor = async (monitor: MonitorFields) => {
const res = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor);
return res.body.id;
.send(monitor)
.expect(200);
return res.body as SimpleSavedObject<MonitorFields>;
};
const supertest = getService('supertest');
it('get all monitors', async () => {
const id1 = await addMonitor();
const id2 = await addMonitor();
const apiResponse = await supertest.get(API_URLS.SYNTHETICS_MONITORS);
const monitor1 = apiResponse.body.monitors.find((obj: any) => obj.id === id1);
const monitor2 = apiResponse.body.monitors.find((obj: any) => obj.id === id2);
expect(monitor1.id).eql(id1);
expect(monitor2.id).eql(id2);
before(() => {
_monitors = [
getFixtureJson('icmp_monitor'),
getFixtureJson('tcp_monitor'),
getFixtureJson('http_monitor'),
getFixtureJson('browser_monitor'),
];
});
it('get monitor by id', async () => {
const monitorId = await addMonitor();
beforeEach(() => {
monitors = _monitors;
});
const apiResponse = await supertest.get(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId);
describe('get many monitors', () => {
it('without params', async () => {
const [{ id: id1, attributes: mon1 }, { id: id2, attributes: mon2 }] = await Promise.all(
monitors.map(saveMonitor)
);
expect(apiResponse.body.id).eql(monitorId);
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS + '?perPage=1000') // 1000 to sort of load all saved monitors
.expect(200);
const found: Array<SimpleSavedObject<MonitorFields>> = apiResponse.body.monitors.filter(
({ id }: SimpleSavedObject<MonitorFields>) => [id1, id2].includes(id)
);
found.sort(({ id: a }) => (a === id2 ? 1 : a === id1 ? -1 : 0));
const foundMonitors = found.map(
({ attributes }: SimpleSavedObject<MonitorFields>) => attributes
);
const expected = [mon1, mon2];
expect(foundMonitors).eql(expected);
});
it('with page params', async () => {
await Promise.all([...monitors, ...monitors].map(saveMonitor));
const firstPageResp = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}?page=1&perPage=2`)
.expect(200);
const secondPageResp = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}?page=2&perPage=3`)
.expect(200);
expect(firstPageResp.body.total).greaterThan(6);
expect(firstPageResp.body.monitors.length).eql(2);
expect(secondPageResp.body.monitors.length).eql(3);
expect(firstPageResp.body.monitors[0].id).not.eql(secondPageResp.body.monitors[0].id);
});
});
describe('get one monitor', () => {
it('should get by id', async () => {
const [{ id: id1, attributes: mon1 }] = await Promise.all(monitors.map(saveMonitor));
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS + '/' + id1)
.expect(200);
expect(apiResponse.body.attributes).eql(mon1);
});
it('returns 404 if monitor id is not found', async () => {
const invalidMonitorId = 'invalid-id';
const expected404Message = `Monitor id ${invalidMonitorId} not found!`;
const getResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS + '/' + invalidMonitorId)
.set('kbn-xsrf', 'true');
expect(getResponse.status).eql(404);
expect(getResponse.body.message).eql(expected404Message);
});
it('validates param length for sanity', async () => {
const veryLargeMonId = new Array(1050).fill('1').join('');
await supertest
.get(API_URLS.SYNTHETICS_MONITORS + '/' + veryLargeMonId)
.set('kbn-xsrf', 'true')
.expect(400);
});
});
});
}

View file

@ -0,0 +1,21 @@
/*
* 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 fs from 'fs';
import { join } from 'path';
const fixturesDir = join(__dirname, '..', 'fixtures');
export function getFixtureJson(fixtureName: string) {
try {
const fixturePath = join(fixturesDir, `${fixtureName}.json`);
const fileContents = fs.readFileSync(fixturePath, 'utf8');
return JSON.parse(fileContents);
} catch (e) {
return {};
}
}