[8.6] [RAM] Add integration tests for all-space global logs view (#145192) (#145596)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[RAM] Add integration tests for all-space global logs view
(#145192)](https://github.com/elastic/kibana/pull/145192)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Zacqary Adam
Xeper","email":"Zacqary@users.noreply.github.com"},"sourceCommit":{"committedDate":"2022-11-17T16:51:28Z","message":"[RAM]
Add integration tests for all-space global logs view (#145192)\n\n##
Summary\r\n\r\nCloses #144732\r\n\r\nAdds functional tests for:\r\n\r\n-
Global logs api with `namespace`\r\n- Global logs kpi with
`namespace`\r\n- Error log api with `namespace`\r\n- Showing/hiding the
logs switch based on space existence and
user\r\npermissions\r\n\r\nAlso:\r\n\r\n- Fixes the query backend for
`getActionErrorLogWithAuth` so that it\r\nactually successfully filters
down to just a single specified alert `id`\r\ninstead of returning all
action error logs from everything. Writing a\r\ntest for this
functionality revealed that it was broken.\r\n\r\n\r\n### Checklist\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"4956ea0b7af7375d62d8da64deb96c099c62e35c","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","Feature:Alerting/RulesManagement","v8.6.0","v8.7.0"],"number":145192,"url":"https://github.com/elastic/kibana/pull/145192","mergeCommit":{"message":"[RAM]
Add integration tests for all-space global logs view (#145192)\n\n##
Summary\r\n\r\nCloses #144732\r\n\r\nAdds functional tests for:\r\n\r\n-
Global logs api with `namespace`\r\n- Global logs kpi with
`namespace`\r\n- Error log api with `namespace`\r\n- Showing/hiding the
logs switch based on space existence and
user\r\npermissions\r\n\r\nAlso:\r\n\r\n- Fixes the query backend for
`getActionErrorLogWithAuth` so that it\r\nactually successfully filters
down to just a single specified alert `id`\r\ninstead of returning all
action error logs from everything. Writing a\r\ntest for this
functionality revealed that it was broken.\r\n\r\n\r\n### Checklist\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"4956ea0b7af7375d62d8da64deb96c099c62e35c"}},"sourceBranch":"main","suggestedTargetBranches":["8.6"],"targetPullRequestStates":[{"branch":"8.6","label":"v8.6.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/145192","number":145192,"mergeCommit":{"message":"[RAM]
Add integration tests for all-space global logs view (#145192)\n\n##
Summary\r\n\r\nCloses #144732\r\n\r\nAdds functional tests for:\r\n\r\n-
Global logs api with `namespace`\r\n- Global logs kpi with
`namespace`\r\n- Error log api with `namespace`\r\n- Showing/hiding the
logs switch based on space existence and
user\r\npermissions\r\n\r\nAlso:\r\n\r\n- Fixes the query backend for
`getActionErrorLogWithAuth` so that it\r\nactually successfully filters
down to just a single specified alert `id`\r\ninstead of returning all
action error logs from everything. Writing a\r\ntest for this
functionality revealed that it was broken.\r\n\r\n\r\n### Checklist\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"4956ea0b7af7375d62d8da64deb96c099c62e35c"}}]}]
BACKPORT-->

Co-authored-by: Zacqary Adam Xeper <Zacqary@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-11-17 13:37:07 -05:00 committed by GitHub
parent f2a3956e03
commit d9b030089c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 468 additions and 7 deletions

View file

@ -529,6 +529,7 @@ export function getQueryBodyWithAuthFilter(
) {
const { namespaces, type, authFilter } = opts;
const { start, end, filter } = queryOptions ?? {};
const ids = 'ids' in opts ? opts.ids : [];
const namespaceQuery = (namespaces ?? [undefined]).map((namespace) =>
getNamespaceQuery(namespace)
@ -586,6 +587,43 @@ export function getQueryBodyWithAuthFilter(
},
];
if (ids.length) {
musts.push({
bool: {
should: {
bool: {
must: [
{
nested: {
path: 'kibana.saved_objects',
query: {
bool: {
must: [
{
terms: {
// default maximum of 65,536 terms, configurable by index.max_terms_count
'kibana.saved_objects.id': ids,
},
},
],
},
},
},
},
{
range: {
'kibana.version': {
gte: LEGACY_ID_CUTOFF_VERSION,
},
},
},
],
},
},
},
});
}
if (start) {
musts.push({
range: {

View file

@ -428,7 +428,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
/>
</EuiFlexItem>
{hasAllSpaceSwitch && areMultipleSpacesAccessible && (
<EuiFlexItem>
<EuiFlexItem data-test-subj="showAllSpacesSwitch">
<EuiSwitch
label={ALL_SPACES_LABEL}
checked={showFromAllSpaces}

View file

@ -0,0 +1,120 @@
/*
* 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 { Spaces } from '../../../scenarios';
import {
getUrlPrefix,
ObjectRemover,
getTestRuleData,
getEventLog,
ESTestIndexTool,
} from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function createGetActionErrorLogTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const es = getService('es');
const esTestIndexTool = new ESTestIndexTool(es, retry);
const dateStart = new Date(Date.now() - 600000).toISOString();
describe('getActionErrorLog', () => {
const objectRemover = new ObjectRemover(supertest);
beforeEach(async () => {
await esTestIndexTool.destroy();
await esTestIndexTool.setup();
});
afterEach(async () => {
await objectRemover.removeAll();
});
it('gets action error logs from an alternate space', async () => {
const { body: createdConnector } = await supertest
.post(`${getUrlPrefix(Spaces[1].id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'connector that throws',
connector_type_id: 'test.throw',
config: {},
secrets: {},
})
.expect(200);
objectRemover.add(Spaces[1].id, createdConnector.id, 'action', 'actions');
// Create 2 rules, and then only pull the logs for one of them
let watchedRuleId;
for (let i = 0; i < 2; i++) {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces[1].id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.cumulative-firing',
actions: [
{
id: createdConnector.id,
group: 'default',
params: {},
},
],
})
)
.expect(200);
objectRemover.add(Spaces[1].id, createdRule.id, 'rule', 'alerting');
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
await waitForEvents(createdRule.id, 'actions', new Map([['execute', { gte: 1 }]]));
if (i === 0) watchedRuleId = createdRule.id;
}
const response = await supertest.get(
`${getUrlPrefix(
Spaces[0].id
)}/internal/alerting/rule/${watchedRuleId}/_action_error_log?date_start=${dateStart}&with_auth=true&namespace=${
Spaces[1].id
}`
);
expect(response.body.totalErrors).to.eql(1);
expect(response.body.errors.length).to.eql(1);
for (const errors of response.body.errors) {
expect(errors.type).to.equal('actions');
expect(errors.message).to.equal(
`action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail; retry: true`
);
}
});
});
async function waitForEvents(
id: string,
provider: string,
actions: Map<
string,
{
gte: number;
}
>
) {
await retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces[1].id,
type: 'alert',
id,
provider,
actions,
});
});
}
}

View file

@ -20,7 +20,7 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
describe('getGlobalExecutionKpi', () => {
const objectRemover = new ObjectRemover(supertest);
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
it('should return KPI only from the current space', async () => {
const startTime = new Date().toISOString();
@ -42,8 +42,9 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
const ruleId = response.body.id;
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
const spaceId2 = UserAtSpaceScenarios[4].space.id;
const response2 = await supertest
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
@ -55,12 +56,12 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
expect(response2.status).to.eql(200);
const ruleId2 = response2.body.id;
objectRemover.add(spaceId, ruleId2, 'rule', 'alerting');
objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting');
await retry.try(async () => {
// break AAD
await supertest
.put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
.put(`${getUrlPrefix(spaceId2)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
.set('kbn-xsrf', 'foo')
.send({
attributes: {
@ -74,7 +75,7 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
// there can be a successful execute before the error one
const someEvents = await getEventLog({
getService,
spaceId,
spaceId: spaceId2,
type: 'alert',
id: ruleId2,
provider: 'alerting',
@ -148,15 +149,155 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
'success',
'unknown',
'failure',
'warning',
'activeAlerts',
'newAlerts',
'recoveredAlerts',
'erroredActions',
'triggeredActions',
]);
// it should be above 1 since we have two rule running
expect(kpiLogs.success).to.be.above(1);
expect(kpiLogs.failure).to.be.above(0);
});
it('should return KPI from all spaces in the namespaces param', async () => {
const startTime = new Date().toISOString();
const spaceId = UserAtSpaceScenarios[1].space.id;
const user = UserAtSpaceScenarios[1].user;
const response = await supertest
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response.status).to.eql(200);
const ruleId = response.body.id;
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
const spaceId2 = UserAtSpaceScenarios[4].space.id;
const response2 = await supertest
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response2.status).to.eql(200);
const ruleId2 = response2.body.id;
objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting');
await retry.try(async () => {
// break AAD
await supertest
.put(`${getUrlPrefix(spaceId2)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
.set('kbn-xsrf', 'foo')
.send({
attributes: {
name: 'bar',
},
})
.expect(200);
});
await retry.try(async () => {
// there can be a successful execute before the error one
const someEvents = await getEventLog({
getService,
spaceId: spaceId2,
type: 'alert',
id: ruleId2,
provider: 'alerting',
actions: new Map([['execute', { gte: 1 }]]),
});
const errorEvents = someEvents.filter(
(event) => event?.kibana?.alerting?.status === 'error'
);
expect(errorEvents.length).to.be.above(0);
});
await retry.try(async () => {
// there can be a successful execute before the error one
const logResponse = await supertestWithoutAuth
.get(
`${getUrlPrefix(
spaceId
)}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(logResponse.statusCode).to.be(200);
expect(logResponse.body.data.length).to.be.above(1);
});
await retry.try(async () => {
// break AAD
await supertest
.put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`)
.set('kbn-xsrf', 'foo')
.send({
attributes: {
name: 'bar',
},
})
.expect(200);
});
await retry.try(async () => {
// there can be a successful execute before the error one
const someEvents = await getEventLog({
getService,
spaceId,
type: 'alert',
id: ruleId,
provider: 'alerting',
actions: new Map([['execute', { gte: 1 }]]),
});
const errorEvents = someEvents.filter(
(event) => event?.kibana?.alerting?.status === 'error'
);
expect(errorEvents.length).to.be.above(0);
});
const kpiLogs = await retry.try(async () => {
// there can be a successful execute before the error one
const logResponse = await supertestWithoutAuth
.get(
`${getUrlPrefix(
spaceId
)}/internal/alerting/_global_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&namespaces=${JSON.stringify(
[spaceId, spaceId2]
)}`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(logResponse.statusCode).to.be(200);
return logResponse.body;
});
expect(Object.keys(kpiLogs)).to.eql([
'success',
'unknown',
'failure',
'warning',
'activeAlerts',
'newAlerts',
'recoveredAlerts',
'erroredActions',
'triggeredActions',
]);
expect(kpiLogs.success).to.be.above(1);
expect(kpiLogs.failure).to.be.above(2);
});
});
}

View file

@ -78,5 +78,66 @@ export default function globalExecutionLogTests({ getService }: FtrProviderConte
const allLogsSpace0 = sanitizedLogs.every((l: any) => l.rule_id === alertId);
expect(allLogsSpace0).to.be(true);
});
it('should return logs from multiple spaces when passed the namespaces param', async () => {
const startTime = new Date().toISOString();
const spaceId = UserAtSpaceScenarios[1].space.id;
const user = UserAtSpaceScenarios[1].user;
const response = await supertest
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
objectRemover.add(spaceId, alertId, 'rule', 'alerting');
const spaceId2 = UserAtSpaceScenarios[4].space.id;
const response2 = await supertest
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response2.status).to.eql(200);
const alertId2 = response2.body.id;
objectRemover.add(spaceId2, alertId2, 'rule', 'alerting');
const logs = await retry.try(async () => {
// there can be a successful execute before the error one
const logResponse = await supertestWithoutAuth
.get(
`${getUrlPrefix(
spaceId
)}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1&namespaces=${JSON.stringify(
[spaceId, spaceId2]
)}`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(logResponse.statusCode).to.be(200);
return logResponse.body.data;
});
// Filter out any excess logs from rules not created by this test
const sanitizedLogs = logs.filter((l: any) => [alertId, alertId2].includes(l.rule_id));
const allLogsSpaces0and1 = sanitizedLogs.every((l: any) =>
[alertId, alertId2].includes(l.rule_id)
);
expect(allLogsSpaces0and1).to.be(true);
});
});
}

View file

@ -45,6 +45,8 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./excluded'));
loadTestFile(require.resolve('./snooze'));
loadTestFile(require.resolve('./global_execution_log'));
loadTestFile(require.resolve('./get_global_execution_kpi'));
loadTestFile(require.resolve('./get_action_error_log'));
});
});
}

View file

@ -14,5 +14,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./alert_create_flyout'));
loadTestFile(require.resolve('./details'));
loadTestFile(require.resolve('./connectors'));
loadTestFile(require.resolve('./logs_list'));
});
};

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { Role, User } from '../../../cases_api_integration/common/lib/authentication/types';
import {
createUsersAndRoles,
deleteUsersAndRoles,
} from '../../../cases_api_integration/common/lib/authentication';
const SPACE2 = {
id: 'space-2',
name: 'Space 2',
disabledFeatures: [],
};
const ONLY_S2_ROLE: Role = {
name: 'only_s2',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
base: ['all'],
feature: {},
spaces: [SPACE2.id],
},
],
},
};
const ONLY_S2_USER: User = {
username: 'only_s2_user',
password: 'changeme',
roles: [ONLY_S2_ROLE.name],
};
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'security']);
const spaces = getService('spaces');
async function refreshLogsList() {
await testSubjects.click('logsTab');
}
describe('logs list', function () {
before(async () => {
await createUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
await pageObjects.common.navigateToApp('triggersActions');
await testSubjects.click('logsTab');
});
after(async () => {
await deleteUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
await spaces.delete(SPACE2.id);
await pageObjects.security.forceLogout();
});
it('should not show the logs space switch when only one space exists', async () => {
const spacesSwitchExists = await testSubjects.exists('showAllSpacesSwitch');
expect(spacesSwitchExists).to.be(false);
});
it('should show the logs space switch when multiple spaces are accessible', async () => {
await spaces.create(SPACE2);
await refreshLogsList();
const spacesSwitch = await testSubjects.find('showAllSpacesSwitch');
expect(spacesSwitch).not.to.be(undefined);
const switchControl = await spacesSwitch.findByCssSelector('button');
expect(await switchControl.getAttribute('aria-checked')).to.be('false');
await switchControl.click();
expect(await switchControl.getAttribute('aria-checked')).to.be('true');
});
it('should not show the logs space switch when multiple spaces exist but only one is accessible', async () => {
await pageObjects.security.forceLogout();
await pageObjects.security.login(ONLY_S2_USER.username, ONLY_S2_USER.password);
await pageObjects.common.navigateToApp('triggersActions', { basePath: `/s/${SPACE2.id}` });
await testSubjects.click('logsTab');
const spacesSwitchExists = await testSubjects.exists('showAllSpacesSwitch');
expect(spacesSwitchExists).to.be(false);
});
});
};