[Profiling] creating API tests (#159984)

As part of the actions for making Profiling production ready, this PR
adds basic API tests on the Profiling APIs checking if only users with
`access:profiling` are allowed to call our APIs, other users must be
forbidden.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-06-24 09:47:28 +01:00 committed by GitHub
parent eaa9eb4b1f
commit 4ddb96f9e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 34144 additions and 2 deletions

View file

@ -399,3 +399,4 @@ enabled:
- x-pack/performance/journeys/dashboard_listing_page.ts
- x-pack/performance/journeys/cloud_security_dashboard.ts
- x-pack/test/custom_branding/config.ts
- x-pack/test/profiling_api_integration/cloud/config.ts

View file

@ -604,6 +604,7 @@ module.exports = {
'**/cypress.config.{js,ts}',
'x-pack/test_serverless/**/config*.ts',
'x-pack/test_serverless/*/test_suites/**/*',
'x-pack/test/profiling_api_integration/**/*.ts',
],
rules: {
'import/no-default-export': 'off',

3
.github/CODEOWNERS vendored
View file

@ -1262,6 +1262,9 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience
# Changes to translation files should not ping code reviewers
x-pack/plugins/translations/translations
# Profiling api integration testing
x-pack/test/profiling_api_integration @elastic/profiling-ui
####
## These rules are always last so they take ultimate priority over everything else
####

View file

@ -28,7 +28,6 @@ export function getRoutePaths() {
TopNTraces: `${BASE_ROUTE_PATH}/topn/traces`,
Flamechart: `${BASE_ROUTE_PATH}/flamechart`,
HasSetupESResources: `${BASE_ROUTE_PATH}/setup/es_resources`,
HasSetupDataCollection: `${BASE_ROUTE_PATH}/setup/has_data`,
SetupDataCollectionInstructions: `${BASE_ROUTE_PATH}/setup/instructions`,
};
}

View file

@ -0,0 +1,111 @@
/*
* 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.
*/
/* eslint-disable no-console */
const { times } = require('lodash');
const yargs = require('yargs');
const path = require('path');
const childProcess = require('child_process');
const { argv } = yargs(process.argv.slice(2))
.option('server', {
default: false,
type: 'boolean',
description: 'Only start ES and Kibana',
})
.option('runner', {
default: false,
type: 'boolean',
description: 'Only run tests',
})
.option('grep', {
alias: 'spec',
type: 'string',
description: 'Specify the specs to run',
})
.option('grep-files', {
alias: 'files',
type: 'array',
string: true,
description: 'Specify the files to run',
})
.option('inspect', {
default: false,
type: 'boolean',
description: 'Add --inspect-brk flag to the ftr for debugging',
})
.option('times', {
type: 'number',
description: 'Repeat the test n number of times',
})
.option('updateSnapshots', {
default: false,
type: 'boolean',
description: 'Update snapshots',
})
.check((argv) => {
const { inspect, runner } = argv;
if (inspect && !runner) {
throw new Error('--inspect can only be used with --runner');
} else {
return true;
}
})
.help();
const { server, runner, grep, grepFiles, inspect, updateSnapshots } = argv;
const license = 'cloud';
console.log(`License: ${license}`);
let ftrScript = 'functional_tests';
if (server) {
ftrScript = 'functional_tests_server';
} else if (runner) {
ftrScript = 'functional_test_runner';
}
const cmd = [
'node',
...(inspect ? ['--inspect-brk'] : []),
`../../../../../scripts/${ftrScript}`,
...(grep ? [`--grep "${grep}"`] : []),
...(updateSnapshots ? [`--updateSnapshots`] : []),
`--config ../../../../test/profiling_api_integration/${license}/config.ts`,
].join(' ');
console.log(`Running: "${cmd}"`);
function runTests() {
childProcess.execSync(cmd, {
cwd: path.join(__dirname),
stdio: 'inherit',
env: { ...process.env, PROFILING_TEST_GREP_FILES: JSON.stringify(grepFiles) },
});
}
if (argv.times) {
const runCounter = { succeeded: 0, failed: 0, remaining: argv.times };
let exitStatus = 0;
times(argv.times, () => {
try {
runTests();
runCounter.succeeded++;
} catch (e) {
exitStatus = 1;
runCounter.failed++;
}
runCounter.remaining--;
if (argv.times > 1) {
console.log(runCounter);
}
});
process.exit(exitStatus);
} else {
runTests();
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { configs } from '../configs';
export default configs.cloud;

View file

@ -0,0 +1,74 @@
/*
* 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 { formatRequest } from '@kbn/server-route-repository';
import request from 'superagent';
import supertest from 'supertest';
import { format } from 'url';
export function createProfilingApiClient(st: supertest.SuperTest<supertest.Test>) {
return async (options: {
endpoint: string;
params?: {
query?: any;
path?: any;
};
}) => {
const { endpoint } = options;
const params = 'params' in options ? (options.params as Record<string, any>) : {};
const { method, pathname, version } = formatRequest(endpoint, params.path);
const url = format({ pathname, query: params?.query });
const headers: Record<string, string> = { 'kbn-xsrf': 'foo' };
if (version) {
headers['Elastic-Api-Version'] = version;
}
let res: request.Response;
if (params.body) {
res = await st[method](url).send(params.body).set(headers);
} else {
res = await st[method](url).set(headers);
}
// supertest doesn't throw on http errors
if (res?.status !== 200 && res?.status !== 202) {
throw new ProfilingApiError(res, endpoint);
}
return res;
};
}
type ApiErrorResponse = Omit<request.Response, 'body'> & {
body: {
statusCode: number;
error: string;
message: string;
attributes: object;
};
};
export type ProfilingApiSupertest = ReturnType<typeof createProfilingApiClient>;
export class ProfilingApiError extends Error {
res: ApiErrorResponse;
constructor(res: request.Response, endpoint: string) {
super(
`Unhandled ProfilingApiError.
Status: "${res.status}"
Endpoint: "${endpoint}"
Body: ${JSON.stringify(res.body)}`
);
this.res = res;
}
}

View file

@ -0,0 +1,163 @@
/*
* 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 { format, UrlObject } from 'url';
import { FtrConfigProviderContext } from '@kbn/test';
import supertest from 'supertest';
import { getRoutePaths } from '@kbn/profiling-plugin/common';
import { ProfilingFtrConfigName } from '../configs';
import {
FtrProviderContext,
InheritedFtrProviderContext,
InheritedServices,
} from './ftr_provider_context';
import { RegistryProvider } from './registry';
import { createProfilingApiClient } from './api_supertest';
import {
ProfilingUsername,
PROFILING_TEST_PASSWORD,
} from './create_profiling_users/authentication';
import { createProfilingUsers } from './create_profiling_users';
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
const profilingRoutePaths = getRoutePaths();
export async function getProfilingApiClient({
kibanaServer,
username,
}: {
kibanaServer: UrlObject;
username: ProfilingUsername | 'elastic';
}) {
const url = format({
...kibanaServer,
auth: `${username}:${PROFILING_TEST_PASSWORD}`,
});
return createProfilingApiClient(supertest(url));
}
type ProfilingApiClientKey = 'noAccessUser' | 'readUser' | 'adminUser';
export type ProfilingApiClient = Record<
ProfilingApiClientKey,
Awaited<ReturnType<typeof getProfilingApiClient>>
>;
export interface ProfilingFtrConfig {
name: ProfilingFtrConfigName;
license: 'basic' | 'trial';
kibanaConfig?: Record<string, any>;
}
export interface CreateTest {
testFiles: string[];
servers: any;
servicesRequiredForTestAnalysis: string[];
services: InheritedServices & {
profilingFtrConfig: () => ProfilingFtrConfig;
registry: ({ getService }: FtrProviderContext) => ReturnType<typeof RegistryProvider>;
profilingApiClient: (context: InheritedFtrProviderContext) => ProfilingApiClient;
};
junit: { reportName: string };
esTestCluster: any;
kbnTestServer: any;
}
export function createTestConfig(
config: ProfilingFtrConfig
): ({ readConfigFile }: FtrConfigProviderContext) => Promise<CreateTest> {
const { license, name, kibanaConfig } = config;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.ts')
);
const services = xPackAPITestsConfig.get('services');
const servers = xPackAPITestsConfig.get('servers');
const kibanaServer = servers.kibana as UrlObject;
const kibanaServerUrl = format(kibanaServer);
const esServer = servers.elasticsearch as UrlObject;
return {
testFiles: [require.resolve('../tests')],
servers,
servicesRequiredForTestAnalysis: ['profilingFtrConfig', 'registry'],
services: {
...services,
profilingFtrConfig: () => config,
registry: RegistryProvider,
profilingApiClient: async (context: InheritedFtrProviderContext) => {
const security = context.getService('security');
const securityService = await security.init();
const { username, password } = servers.kibana;
const esUrl = format(esServer);
await createProfilingUsers({
securityService,
elasticsearch: { node: esUrl, username, password },
kibana: { hostname: kibanaServerUrl },
});
const adminUser = await getProfilingApiClient({
kibanaServer,
username: 'elastic',
});
await supertest(kibanaServerUrl).post('/api/fleet/setup').set('kbn-xsrf', 'foo');
const result = await adminUser({
endpoint: `GET ${profilingRoutePaths.HasSetupESResources}`,
});
if (!result.body.has_setup) {
// eslint-disable-next-line no-console
console.log('Setting up Universal Profiling');
await adminUser({
endpoint: `POST ${profilingRoutePaths.HasSetupESResources}`,
});
// eslint-disable-next-line no-console
console.log('Universal Profiling set up');
}
return {
noAccessUser: await getProfilingApiClient({
kibanaServer,
username: ProfilingUsername.noAccessUser,
}),
readUser: await getProfilingApiClient({
kibanaServer,
username: ProfilingUsername.viewerUser,
}),
adminUser,
};
},
},
junit: {
reportName: `Profiling API Integration tests (${name})`,
},
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
license,
},
kbnTestServer: {
...xPackAPITestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
...(kibanaConfig
? Object.entries(kibanaConfig).map(([key, value]) =>
Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}`
)
: []),
],
},
};
};
}
export type ProfilingServices = Awaited<ReturnType<CreateTestConfig>>['services'];

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export enum ProfilingUsername {
noAccessUser = 'no_access_user',
viewerUser = 'viewer',
editorUser = 'editor',
}
export const PROFILING_TEST_PASSWORD = 'changeme';
export const profilingUsers: Record<ProfilingUsername, { builtInRoleNames?: string[] }> = {
[ProfilingUsername.noAccessUser]: {},
[ProfilingUsername.viewerUser]: {
builtInRoleNames: ['viewer'],
},
[ProfilingUsername.editorUser]: {
builtInRoleNames: ['editor'],
},
};

View file

@ -0,0 +1,56 @@
/*
* 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 axios, { AxiosRequestConfig, AxiosError } from 'axios';
import { once } from 'lodash';
import { Elasticsearch, Kibana } from '..';
export async function callKibana<T>({
elasticsearch,
kibana,
options,
}: {
elasticsearch: Omit<Elasticsearch, 'node'>;
kibana: Kibana;
options: AxiosRequestConfig;
}): Promise<T> {
const baseUrl = await getBaseUrl(kibana.hostname);
const { username, password } = elasticsearch;
const { data } = await axios.request({
...options,
baseURL: baseUrl,
auth: { username, password },
headers: { 'kbn-xsrf': 'true', ...options.headers },
});
return data;
}
const getBaseUrl = once(async (kibanaHostname: string) => {
try {
await axios.request({ url: kibanaHostname, maxRedirects: 0 });
} catch (e) {
if (isAxiosError(e)) {
const location = e.response?.headers?.location ?? '';
const hasBasePath = RegExp(/^\/\w{3}$/).test(location);
const basePath = hasBasePath ? location : '';
return `${kibanaHostname}${basePath}`;
}
throw e;
}
return kibanaHostname;
});
export function isAxiosError(e: AxiosError | Error): e is AxiosError {
return 'isAxiosError' in e;
}
export class AbortError extends Error {
constructor(message: string) {
super(message);
}
}

View file

@ -0,0 +1,114 @@
/*
* 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.
*/
/* eslint-disable no-console */
import { difference, union } from 'lodash';
import { Elasticsearch, Kibana } from '..';
import { callKibana, isAxiosError } from './call_kibana';
import { SecurityService } from '../../../../../../test/common/services/security/security';
interface User {
username: string;
roles: string[];
full_name?: string;
email?: string;
enabled?: boolean;
}
export async function createOrUpdateUser({
elasticsearch,
kibana,
user,
securityService,
}: {
elasticsearch: Elasticsearch;
kibana: Kibana;
user: User;
securityService: SecurityService;
}) {
const existingUser = await getUser({
elasticsearch,
kibana,
username: user.username,
});
if (!existingUser) {
createUser({ elasticsearch, newUser: user, securityService });
return;
}
updateUser({
existingUser,
newUser: user,
securityService,
});
}
async function createUser({
elasticsearch,
newUser,
securityService,
}: {
elasticsearch: Elasticsearch;
newUser: User;
securityService: SecurityService;
}) {
const { username, ...options } = newUser;
await securityService.user.create(username, {
...options,
enabled: true,
password: elasticsearch.password,
});
}
async function updateUser({
existingUser,
newUser,
securityService,
}: {
existingUser: User;
newUser: User;
securityService: SecurityService;
}) {
const { username } = newUser;
const allRoles = union(existingUser.roles, newUser.roles);
const hasAllRoles = difference(allRoles, existingUser.roles).length === 0;
if (hasAllRoles) {
console.log(`Skipping: User "${username}" already has necessary roles: "${newUser.roles}"`);
return;
}
const { username: _, ...options } = existingUser;
await securityService.user.create(username, { ...options, roles: allRoles });
}
async function getUser({
elasticsearch,
kibana,
username,
}: {
elasticsearch: Elasticsearch;
kibana: Kibana;
username: string;
}) {
try {
return await callKibana<User>({
elasticsearch,
kibana,
options: {
url: `/internal/security/users/${username}`,
},
});
} catch (e) {
// return empty if user doesn't exist
if (isAxiosError(e) && e.response?.status === 404) {
return null;
}
throw e;
}
}

View file

@ -0,0 +1,108 @@
/*
* 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 { asyncForEach } from '@kbn/std';
import { ProfilingUsername, profilingUsers } from './authentication';
import { AbortError, callKibana } from './helpers/call_kibana';
import { createOrUpdateUser } from './helpers/create_or_update_user';
import { SecurityService } from '../../../../../test/common/services/security/security';
export interface Elasticsearch {
node: string;
username: string;
password: string;
}
export interface Kibana {
hostname: string;
}
export async function createProfilingUsers({
kibana,
elasticsearch,
securityService,
}: {
kibana: Kibana;
elasticsearch: Elasticsearch;
securityService: SecurityService;
}) {
const isCredentialsValid = await getIsCredentialsValid({
elasticsearch,
kibana,
});
if (!isCredentialsValid) {
throw new AbortError('Invalid username/password');
}
const isSecurityEnabled = await getIsSecurityEnabled({
elasticsearch,
kibana,
});
if (!isSecurityEnabled) {
throw new AbortError('Security must be enabled!');
}
const userNames = Object.values(ProfilingUsername);
await asyncForEach(userNames, async (username) => {
const user = profilingUsers[username];
const { builtInRoleNames = [] } = user;
// create user
await createOrUpdateUser({
securityService,
elasticsearch,
kibana,
user: { username, roles: builtInRoleNames },
});
});
return userNames;
}
async function getIsSecurityEnabled({
elasticsearch,
kibana,
}: {
elasticsearch: Elasticsearch;
kibana: Kibana;
}) {
try {
await callKibana({
elasticsearch,
kibana,
options: {
url: `/internal/security/me`,
},
});
return true;
} catch (err) {
return false;
}
}
async function getIsCredentialsValid({
elasticsearch,
kibana,
}: {
elasticsearch: Elasticsearch;
kibana: Kibana;
}) {
try {
await callKibana({
elasticsearch,
kibana,
options: {
validateStatus: (status) => status >= 200 && status < 400,
url: `/`,
},
});
return true;
} catch (err) {
return false;
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,20 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { ProfilingServices } from './config';
import { FtrProviderContext as InheritedFtrProviderContext } from '../../api_integration/ftr_provider_context';
export type InheritedServices = InheritedFtrProviderContext extends GenericFtrProviderContext<
infer TServices,
{}
>
? TServices
: {};
export type { InheritedFtrProviderContext };
export type FtrProviderContext = GenericFtrProviderContext<ProfilingServices, {}>;

View file

@ -0,0 +1,174 @@
/*
* 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 { joinByKey } from '@kbn/apm-plugin/common/utils/join_by_key';
import { maybe } from '@kbn/apm-plugin/common/utils/maybe';
import callsites from 'callsites';
import { castArray, groupBy } from 'lodash';
import Path from 'path';
import fs from 'fs';
import { ProfilingFtrConfigName } from '../configs';
import { FtrProviderContext } from './ftr_provider_context';
const esArchiversPath = Path.posix.join(__dirname, 'fixtures', 'es_archiver', 'profiling');
interface RunCondition {
config: ProfilingFtrConfigName;
}
export function RegistryProvider({ getService }: FtrProviderContext) {
const profilingFtrConfig = getService('profilingFtrConfig');
const es = getService('es');
const callbacks: Array<
RunCondition & {
runs: Array<{
cb: () => void;
}>;
}
> = [];
let running: boolean = false;
function when(
title: string,
conditions: RunCondition | RunCondition[],
callback: (condition: RunCondition) => void,
skip?: boolean
) {
const allConditions = castArray(conditions);
if (!allConditions.length) {
throw new Error('At least one condition should be defined');
}
if (running) {
throw new Error("Can't add tests when running");
}
const frame = maybe(callsites()[1]);
const file = frame?.getFileName();
if (!file) {
throw new Error('Could not infer file for suite');
}
allConditions.forEach((matchedCondition) => {
callbacks.push({
...matchedCondition,
runs: [
{
cb: () => {
const suite: ReturnType<typeof describe> = (skip ? describe.skip : describe)(
title,
() => {
callback(matchedCondition);
}
) as any;
suite.file = file;
suite.eachTest((test) => {
test.file = file;
});
},
},
],
});
});
}
when.skip = (
title: string,
conditions: RunCondition | RunCondition[],
callback: (condition: RunCondition) => void
) => {
when(title, conditions, callback, true);
};
const registry = {
when,
run: () => {
running = true;
const logger = getService('log');
const logWithTimer = () => {
const start = process.hrtime();
return (message: string) => {
const diff = process.hrtime(start);
const time = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`;
logger.info(`(${time}) ${message}`);
};
};
const groups = joinByKey(callbacks, ['config'], (a, b) => ({
...a,
...b,
runs: a.runs.concat(b.runs),
}));
callbacks.length = 0;
const byConfig = groupBy(groups, 'config');
Object.keys(byConfig).forEach((config) => {
const groupsForConfig = byConfig[config];
// register suites for other configs, but skip them so tests are marked as such
// and their snapshots are not marked as obsolete
(config === profilingFtrConfig.name ? describe : describe.skip)(config, () => {
groupsForConfig.forEach((group) => {
const { runs } = group;
const runBefore = async () => {
const log = logWithTimer();
const content = fs.readFileSync(`${esArchiversPath}/data.json`, 'utf8');
log(`Loading profiling data`);
await es.bulk({ operations: content.split('\n'), refresh: 'wait_for' });
log('Loaded profiling data');
};
const runAfter = async () => {
const log = logWithTimer();
log(`Unloading Profiling data`);
const indices = await es.cat.indices({ format: 'json' });
const profilingIndices = indices
.filter((index) => index.index !== undefined)
.map((index) => index.index)
.filter((index) => {
return index!.startsWith('profiling') || index!.startsWith('.profiling');
}) as string[];
await Promise.all([
...profilingIndices.map((index) => es.indices.delete({ index })),
es.indices.deleteDataStream({
name: 'profiling-events*',
}),
]);
log('Unloaded Profiling data');
};
describe('Loading profiling data', () => {
before(runBefore);
runs.forEach((run) => {
run.cb();
});
after(runAfter);
});
});
});
});
running = false;
},
};
return registry;
}

View file

@ -0,0 +1,30 @@
xpack.profiling.enabled: true
xpack.cloud.id: 'foo'
xpack.fleet.packages:
- name: apm
version: latest
xpack.fleet.agentPolicies:
- name: Elastic APM
id: policy-elastic-agent-on-cloud
package_policies:
- name: Elastic APM
id: elastic-cloud-apm
package:
name: apm
inputs:
- type: apm
keep_enabled: true
vars:
- name: api_key_enabled
value: true
- name: host
value: '0.0.0.0:8200'
frozen: true
- name: secret_token
value: 'foo'
- name: profiling.enabled
value: true
- name: profiling.metrics.elasticsearch
value: ['https://elasticsearch:9200']

View file

@ -0,0 +1,40 @@
/*
* 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 { mapValues } from 'lodash';
import path from 'path';
import { createTestConfig, CreateTestConfig } from '../common/config';
const kibanaYamlFilePath = path.join(__dirname, './ftr_kibana.yml');
const profilingDebugLogger = {
name: 'plugins.profiling',
level: 'debug',
appenders: ['console'],
};
const profilingFtrConfigs = {
cloud: {
license: 'trial' as const,
kibanaConfig: {
'logging.loggers': [profilingDebugLogger],
config: kibanaYamlFilePath,
},
},
};
export type ProfilingFtrConfigName = keyof typeof profilingFtrConfigs;
export const configs: Record<ProfilingFtrConfigName, CreateTestConfig> = mapValues(
profilingFtrConfigs,
(value, key) => {
return createTestConfig({
name: key as ProfilingFtrConfigName,
...value,
});
}
);

View file

@ -0,0 +1,127 @@
/*
* 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 { getRoutePaths } from '@kbn/profiling-plugin/common';
import { ProfilingApiError } from '../common/api_supertest';
import { getProfilingApiClient } from '../common/config';
import { FtrProviderContext } from '../common/ftr_provider_context';
const profilingRoutePaths = getRoutePaths();
export default function featureControlsTests({ getService }: FtrProviderContext) {
const registry = getService('registry');
const profilingApiClient = getService('profilingApiClient');
const log = getService('log');
const start = encodeURIComponent(new Date(Date.now() - 10000).valueOf());
const end = encodeURIComponent(new Date().valueOf());
const expect403 = (status: number) => {
expect(status).to.be(403);
};
const expect200 = (status: number) => {
expect(status).to.be(200);
};
interface Endpoint {
url: string;
method?: 'GET' | 'POST';
params?: { query: Record<string, any> };
body?: any;
}
const endpoints: Endpoint[] = [
{
url: profilingRoutePaths.TopNContainers,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{
url: profilingRoutePaths.TopNDeployments,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{
url: profilingRoutePaths.TopNHosts,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{
url: profilingRoutePaths.TopNTraces,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{
url: profilingRoutePaths.TopNThreads,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{
url: profilingRoutePaths.TopNFunctions,
params: { query: { timeFrom: start, timeTo: end, kuery: '', startIndex: 1, endIndex: 5 } },
},
{
url: profilingRoutePaths.Flamechart,
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
},
{ url: profilingRoutePaths.SetupDataCollectionInstructions },
{ url: profilingRoutePaths.HasSetupESResources },
];
async function executeRequests({
expectation,
runAsUser,
}: {
expectation: 'forbidden' | 'response';
runAsUser: Awaited<ReturnType<typeof getProfilingApiClient>>;
}) {
for (const endpoint of endpoints) {
const method = endpoint.method || 'GET';
const endpointPath = `${method} ${endpoint.url}`;
try {
log.info(`Requesting: ${endpointPath}. Expecting: ${expectation}`);
const result = await runAsUser({
endpoint: endpointPath,
params: endpoint.params || {},
});
if (expectation === 'forbidden') {
throw new Error(
`Endpoint: ${endpointPath}
Status code: ${result.status}
Response: ${result.body}`
);
}
expect200(result.status);
} catch (e) {
if (e instanceof ProfilingApiError && expectation === 'forbidden') {
expect403(e.res.status);
} else {
throw new Error(
`Endpoint: ${endpointPath}
Status code: ${e.res.status}
Response: ${e.res}
${e.message}`
);
}
}
}
}
registry.when('Profiling feature controls', { config: 'cloud' }, () => {
before(async () => {});
it(`returns forbidden for users with no access to profiling APIs`, async () => {
await executeRequests({
runAsUser: profilingApiClient.noAccessUser,
expectation: 'forbidden',
});
});
it(`returns ok for users with access to profiling APIs`, async () => {
await executeRequests({ runAsUser: profilingApiClient.readUser, expectation: 'response' });
});
});
}

View file

@ -0,0 +1,50 @@
/*
* 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 globby from 'globby';
import path from 'path';
import { FtrProviderContext } from '../common/ftr_provider_context';
const cwd = path.join(__dirname);
const envGrepFiles = process.env.profiling_TEST_GREP_FILES as string;
function getGlobPattern() {
try {
const envGrepFilesParsed = JSON.parse(envGrepFiles as string) as string[];
return envGrepFilesParsed.map((pattern) => `**/${pattern}**`);
} catch (e) {
// ignore
}
return '**/*.spec.ts';
}
export default function profilingApiIntegrationTests({
getService,
loadTestFile,
}: FtrProviderContext) {
const registry = getService('registry');
describe('Profiling API tests', function () {
const filePattern = getGlobPattern();
const tests = globby.sync(filePattern, { cwd });
if (envGrepFiles) {
// eslint-disable-next-line no-console
console.log(
`\nCommand "--grep-files=${filePattern}" matched ${tests.length} file(s):\n${tests
.map((name) => ` - ${name}`)
.join('\n')}\n`
);
}
tests.forEach((testName) => {
describe(testName, () => {
loadTestFile(require.resolve(`./${testName}`));
registry.run();
});
});
});
}

View file

@ -129,6 +129,7 @@
"@kbn/core-http-common",
"@kbn/slo-schema",
"@kbn/lens-plugin",
"@kbn/telemetry-tools"
"@kbn/telemetry-tools",
"@kbn/profiling-plugin"
]
}