Skip to content

Commit

Permalink
[Synthetics] migrate first set of tests (elastic#198950)
Browse files Browse the repository at this point in the history
## Summary

Relates to elastic#196229 

Migrates Synthetics tests to deployment agnostic

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Shahzad <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2024
1 parent 0350618 commit b74b935
Show file tree
Hide file tree
Showing 42 changed files with 10,419 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,9 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/ @elastic/obs-ux-management-team
/x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team
/x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team
/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/ @elastic/obs-ux-management-team
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitors @elastic/obs-ux-management-team
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_private_location @elastic/obs-ux-management-team

# Elastic Stack Monitoring
/x-pack/test/monitoring_api_integration @elastic/stack-monitoring
Expand Down
24 changes: 24 additions & 0 deletions x-pack/plugins/observability_solution/synthetics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,27 @@ From the `~/x-pack` directory:
Start the server: `node scripts/functional_tests_server --config test/accessibility/config.ts`

Run the uptime `a11y` tests: `node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=uptime`


## Deployment agnostic API Integration Tests
The Synthetics tests are located under `x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics` folder. In order to run the SLO tests of your interest, you can grep accordingly. Use the commands below to run all SLO tests (`grep=SyntheticsAPITests`) on stateful or serverless.

### Stateful

```
# start server
node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
# run tests
node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep=SyntheticsAPITests
```

### Serverless

```
# start server
node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
# run tests
node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep=SyntheticsAPITests
```
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,10 @@ export class SyntheticsService {
return this.server.coreStart?.elasticsearch.client.asInternalUser;
}

async getOutput() {
async getOutput({ inspect }: { inspect: boolean } = { inspect: false }) {
const { apiKey, isValid } = await getAPIKeyForSyntheticsService({ server: this.server });
if (!isValid) {
// do not check for api key validity if inspecting
if (!isValid && !inspect) {
this.server.logger.error(
'API key is not valid. Cannot push monitor configuration to synthetics public testing locations'
);
Expand All @@ -332,7 +333,7 @@ export class SyntheticsService {
const monitors = this.formatConfigs(config);
const license = await this.getLicense();

const output = await this.getOutput();
const output = await this.getOutput({ inspect: true });
if (output) {
return await this.apiClient.inspect({
monitors,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/*
* 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 { RoleCredentials, SamlAuthProviderType } from '@kbn/ftr-common-functional-services';
import epct from 'expect';
import moment from 'moment/moment';
import { v4 as uuidv4 } from 'uuid';
import { omit, omitBy } from 'lodash';
import {
ConfigKey,
MonitorTypeEnum,
HTTPFields,
PrivateLocation,
} 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 { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults';
import {
removeMonitorEmptyValues,
transformPublicKeys,
} from '@kbn/synthetics-plugin/server/routes/monitor_cruds/formatters/saved_object_to_monitor';
import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helpers/get_fixture_json';
import { SyntheticsMonitorTestService } from '../../../services/synthetics_monitor';
import { PrivateLocationTestService } from '../../../services/synthetics_private_location';

export const addMonitorAPIHelper = async (
supertestAPI: any,
monitor: any,
statusCode = 200,
roleAuthc: RoleCredentials,
samlAuth: SamlAuthProviderType
) => {
const result = await supertestAPI
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send(monitor)
.expect(statusCode);

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;
};

export const keyToOmitList = [
'created_at',
'updated_at',
'id',
'config_id',
'form_monitor_type',
'spaceId',
'private_locations',
];

export const omitMonitorKeys = (monitor: any) => {
return omitBy(omit(transformPublicKeys(monitor), keyToOmitList), removeMonitorEmptyValues);
};

export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
describe('AddNewMonitorsUI', function () {
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 kibanaServer.savedObjects.cleanStandardList();
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('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': null };
await addMonitorAPI(newMonitor, 400);
});

it('returns bad request if monitor type is invalid', async () => {
const newMonitor = { ...httpMonitorJson, type: 'invalid-data-steam' };

const apiResponse = await addMonitorAPI(newMonitor, 400);

expect(apiResponse.message).eql('Invalid value "invalid-data-steam" supplied to "type"');
});

it('can create valid monitors without all defaults', async () => {
// Delete a required property to make payload invalid
const newMonitor = {
name: 'Sample name',
type: 'http',
urls: 'https://elastic.co',
locations: [privateLocation],
};

const { body: apiResponse } = await addMonitorAPI(newMonitor);

expect(apiResponse).eql(
omitMonitorKeys({
...DEFAULT_FIELDS[MonitorTypeEnum.HTTP],
...newMonitor,
})
);
});

it('can disable retries', async () => {
const maxAttempts = 1;
const newMonitor = {
max_attempts: maxAttempts,
urls: 'https://elastic.co',
name: `Sample name ${uuidv4()}`,
type: 'http',
locations: [privateLocation],
};

const { body: apiResponse } = await addMonitorAPI(newMonitor);

epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false }));
});

it('can enable retries with max attempts', async () => {
const maxAttempts = 2;
const newMonitor = {
max_attempts: maxAttempts,
urls: 'https://elastic.co',
name: `Sample name ${uuidv4()}`,
type: 'http',
locations: [privateLocation],
};

const { body: apiResponse } = await addMonitorAPI(newMonitor);

epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: true }));
});

it('can enable retries', async () => {
const newMonitor = {
retest_on_failure: false,
urls: 'https://elastic.co',
name: `Sample name ${uuidv4()}`,
type: 'http',
locations: [privateLocation],
};

const { body: apiResponse } = await addMonitorAPI(newMonitor);

epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false }));
});

it('cannot create a invalid monitor without a monitor type', async () => {
// Delete a required property to make payload invalid
const newMonitor = {
name: 'Sample name',
url: 'https://elastic.co',
locations: [privateLocation],
};
await addMonitorAPI(newMonitor, 400);
});

it('omits unknown keys', async () => {
// Delete a required property to make payload invalid
const newMonitor = {
name: 'Sample name',
url: 'https://elastic.co',
unknownKey: 'unknownValue',
type: 'http',
locations: [privateLocation],
};
const apiResponse = await addMonitorAPI(newMonitor, 400);
expect(apiResponse.message).not.to.have.keys(
'Invalid monitor key(s) for http type: unknownKey","attributes":{"details":"Invalid monitor key(s) for http type: unknownKey'
);
});

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)
.expect(200);
monitorId = apiResponse.body.id;
expect(apiResponse.body[ConfigKey.NAMESPACE]).eql(EXPECTED_NAMESPACE);
} finally {
await deleteMonitor(monitorId, 200, SPACE_ID);
}
});

it('preserves the passed namespace when preserve_namespace is passed', async () => {
const SPACE_ID = `test-space-${uuidv4()}`;
const SPACE_NAME = `test-space-name ${uuidv4()}`;
privateLocation = await privateLocationsService.addTestPrivateLocation(SPACE_ID);
const monitor = {
...httpMonitorJson,
[ConfigKey.NAMESPACE]: 'default',
locations: [privateLocation],
};
let monitorId = '';
await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME });

try {
const apiResponse = await supertestAPI
.post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`)
.query({ preserve_namespace: true })
.set(editorRoleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send(monitor)
.expect(200);
monitorId = apiResponse.body.id;
expect(apiResponse.body[ConfigKey.NAMESPACE]).eql('default');
} finally {
await deleteMonitor(monitorId, 200, SPACE_ID);
}
});

it('sets namespace to custom namespace when set', async () => {
const SPACE_ID = `test-space-${uuidv4()}`;
const SPACE_NAME = `test-space-name ${uuidv4()}`;
privateLocation = await privateLocationsService.addTestPrivateLocation(SPACE_ID);
const monitor = {
...httpMonitorJson,
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)
.expect(200);
monitorId = apiResponse.body.id;
expect(apiResponse.body[ConfigKey.NAMESPACE]).eql(monitor[ConfigKey.NAMESPACE]);
} finally {
await deleteMonitor(monitorId, 200, SPACE_ID);
}
});
});
}
Loading

0 comments on commit b74b935

Please sign in to comment.