[RAM][Maintenance Window] Improve Maintenance Window Backend Integration Testing (#157120)

## Summary
Resolves: https://github.com/elastic/kibana/issues/155902

Improves backend integration testing for maintenance windows, tests the
following:

# API:
- Add more tests around create/update, ensuring events are generated
correctly
- Add more tests around `/_active`, ensuring the query we use is solid
- Add more tests around cancelling/archiving
- Verify functionality of the 3 MW scenarios
- Verify summarized actions and security rules function correctly

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2023-05-16 15:22:37 -06:00 committed by GitHub
parent 556ba07452
commit 9910615a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 674 additions and 10 deletions

View file

@ -33,8 +33,7 @@ export default function activeMaintenanceWindowTests({ getService }: FtrProvider
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
afterEach(() => objectRemover.removeAll());
it('should handle update maintenance window request appropriately', async () => {
it('should handle get active maintenance window request appropriately', async () => {
// Create 2 active and 1 inactive maintenance window
const { body: createdMaintenanceWindow1 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)

View file

@ -27,7 +27,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
@ -84,5 +84,103 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
});
});
}
it('can archive and unarchive a maintenance window', async () => {
const space1 = UserAtSpaceScenarios[1].space.id;
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send(createParams)
.expect(200);
objectRemover.add(
space1,
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);
expect(createdMaintenanceWindow.status).eql('running');
const { body: archive } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_archive`
)
.set('kbn-xsrf', 'foo')
.send({ archive: true })
.expect(200);
expect(archive.status).eql('archived');
const { body: unarchived } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_archive`
)
.set('kbn-xsrf', 'foo')
.send({ archive: false })
.expect(200);
expect(unarchived.status).eql('running');
});
it('archiving a finished maintenance window does not change the events', async () => {
const space1 = UserAtSpaceScenarios[1].space.id;
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: {
...createParams.r_rule,
dtstart: moment.utc().subtract(1, 'day').toISOString(),
freq: 3, // daily
count: 4,
},
})
.expect(200);
objectRemover.add(
space1,
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);
const { body: finish } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_finish`
)
.set('kbn-xsrf', 'foo')
.send()
.expect(200);
// The finished maintenance window has a different end date for the first event
expect(finish.events[0].lte).eql(createdMaintenanceWindow.events[0].lte);
expect(finish.events[1].lte).not.eql(createdMaintenanceWindow.events[1].lte);
expect(finish.events[2].lte).eql(createdMaintenanceWindow.events[2].lte);
expect(finish.events[3].lte).eql(createdMaintenanceWindow.events[3].lte);
const { body: archive } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_archive`
)
.set('kbn-xsrf', 'foo')
.send({ archive: true })
.expect(200);
// Archiving should not change the events
expect(finish.events[0].lte).eql(archive.events[0].lte);
expect(finish.events[1].lte).eql(archive.events[1].lte);
});
});
}

View file

@ -26,7 +26,7 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;

View file

@ -26,7 +26,7 @@ export default function deleteMaintenanceWindowTests({ getService }: FtrProvider
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;

View file

@ -26,13 +26,12 @@ export default function findMaintenanceWindowTests({ getService }: FtrProviderCo
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
afterEach(() => objectRemover.removeAll());
it('should handle update maintenance window request appropriately', async () => {
it('should handle find maintenance window request appropriately', async () => {
const { body: createdMaintenanceWindow1 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')

View file

@ -80,5 +80,99 @@ export default function findMaintenanceWindowTests({ getService }: FtrProviderCo
});
});
}
it('should error when trying to finish a finished maintenance window', async () => {
const space1 = UserAtSpaceScenarios[1].space.id;
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: {
...createParams.r_rule,
count: 1,
},
})
.expect(200);
objectRemover.add(
space1,
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);
const { body: finish } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_finish`
)
.set('kbn-xsrf', 'foo')
.send()
.expect(200);
expect(finish.status).eql('finished');
// Cant finish a finished maintenance window
await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_finish`
)
.set('kbn-xsrf', 'foo')
.send()
.expect(400);
});
it('should error when trying to finish a upcoming maintenance window', async () => {
const space1 = UserAtSpaceScenarios[1].space.id;
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
...createParams,
r_rule: {
...createParams.r_rule,
count: 2, // 2 occurrences
},
})
.expect(200);
objectRemover.add(
space1,
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);
const { body: finish } = await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_finish`
)
.set('kbn-xsrf', 'foo')
.send()
.expect(200);
// Status now upcoming, new start and end dates reflect the upcoming event
expect(finish.status).eql('upcoming');
expect(finish.event_start_time).eql(createdMaintenanceWindow.events[1].gte);
expect(finish.event_end_time).eql(createdMaintenanceWindow.events[1].lte);
// Cannot finish an upcoming maintenance window
await supertest
.post(
`${getUrlPrefix(space1)}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}/_finish`
)
.set('kbn-xsrf', 'foo')
.send()
.expect(400);
});
});
}

View file

@ -26,7 +26,7 @@ export default function getMaintenanceWindowTests({ getService }: FtrProviderCon
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;

View file

@ -27,7 +27,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
freq: 2, // weekly
},
};
after(() => objectRemover.removeAll());
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
@ -90,6 +90,58 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
});
}
it('should update fields correctly', async () => {
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
title: 'test-maintenance-window',
duration: 60 * 60 * 1000, // 1 hr
r_rule: {
dtstart: new Date().toISOString(),
tzid: 'UTC',
freq: 2, // weekly
count: 1,
},
})
.expect(200);
objectRemover.add(
'space1',
createdMaintenanceWindow.id,
'rules/maintenance_window',
'alerting',
true
);
const newRRule = {
dtstart: moment.utc().add(1, 'day').toISOString(),
tzid: 'CET',
freq: 3,
count: 5,
};
const { body: updatedMW } = await supertest
.post(
`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${
createdMaintenanceWindow.id
}`
)
.set('kbn-xsrf', 'foo')
.send({
title: 'test-maintenance-window-new',
duration: 60 * 1000,
r_rule: newRRule,
enabled: false,
})
.expect(200);
expect(updatedMW.title).eql('test-maintenance-window-new');
expect(updatedMW.duration).eql(60 * 1000);
expect(updatedMW.r_rule).eql(newRRule);
expect(updatedMW.title).eql('test-maintenance-window-new');
});
it('should update RRule correctly when removing fields', async () => {
const { body: createdMaintenanceWindow } = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)

View file

@ -1308,6 +1308,104 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
}
});
});
it('should not fire summary actions during maintenance window', async () => {
const { body: window } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
title: 'test-maintenance-window-1',
duration: 60 * 60 * 1000, // 1 hr
r_rule: {
dtstart: moment.utc().toISOString(),
tzid: 'UTC',
freq: 0, // yearly
count: 1,
},
})
.expect(200);
objectRemover.add(space.id, window.id, 'rules/maintenance_window', 'alerting', true);
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'Test conn',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.always-firing-alert-as-data',
schedule: { interval: '24h' },
throttle: undefined,
notify_when: undefined,
params: {
index: ES_TEST_INDEX_NAME,
reference: 'test',
},
actions: [
{
id: createdAction.id,
group: 'default',
params: {
index: ES_TEST_INDEX_NAME,
reference: 'test',
message: '',
},
frequency: {
summary: true,
throttle: null,
notify_when: 'onActiveAlert',
},
},
],
})
)
.expect(200);
objectRemover.add(space.id, createdRule.id, 'rule', 'alerting');
// get the events we're expecting
await retry.try(async () => {
return await getEventLog({
getService,
spaceId: space.id,
type: 'alert',
id: createdRule.id,
provider: 'alerting',
actions: new Map([
['execute-start', { equal: 1 }],
['execute', { equal: 1 }],
['active-instance', { equal: 2 }],
]),
});
});
// Try to get actions, should fail
let hasActions = false;
try {
await getEventLog({
getService,
spaceId: space.id,
type: 'alert',
id: createdRule.id,
provider: 'alerting',
actions: new Map([['execute-action', { equal: 1 }]]),
});
hasActions = true;
} catch (e) {
hasActions = false;
}
expect(hasActions).eql(false);
});
});
}
});

View file

@ -15,5 +15,6 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
after(async () => await tearDown(getService));
loadTestFile(require.resolve('./builtin_alert_types'));
loadTestFile(require.resolve('./maintenance_window_flows'));
});
}

View file

@ -0,0 +1,323 @@
/*
* 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 { IValidatedEvent } from '@kbn/event-log-plugin/server';
import moment from 'moment';
import expect from '@kbn/expect';
import { Spaces } from '../../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function maintenanceWindowFlowsTests({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const supertest = getService('supertest');
const retry = getService('retry');
describe('maintenanceWindowFlows', () => {
const objectRemover = new ObjectRemover(supertestWithoutAuth);
afterEach(async () => {
await objectRemover.removeAll();
});
it('alerts triggered before a MW should fire actions when active or recovered during a MW', async () => {
const pattern = {
instance: [true, true, false, true],
};
// Create action and rule
const action = await createAction();
const rule = await createRule({ actionId: action.id, pattern });
// Run the first time - active
await getRuleEvents({
id: rule.id,
action: 1,
activeInstance: 1,
});
// Run again - active, 2 action
await runSoon(rule.id);
await getRuleEvents({
id: rule.id,
action: 2,
activeInstance: 2,
});
// Create active maintenance window
const maintenanceWindow = await createMaintenanceWindow();
const activeMaintenanceWindows = await getActiveMaintenanceWindows();
expect(activeMaintenanceWindows[0].id).eql(maintenanceWindow.id);
// Run again - recovered, 3 actions, fired in MW
await runSoon(rule.id);
await getRuleEvents({
id: rule.id,
action: 3,
activeInstance: 2,
recoveredInstance: 1,
});
// Run again - active, 3 actions, new active action NOT fired in MW
await runSoon(rule.id);
await getRuleEvents({
id: rule.id,
action: 3,
activeInstance: 3,
recoveredInstance: 1,
});
});
it('alerts triggered within a MW should not fire actions if active or recovered during a MW', async () => {
const pattern = {
instance: [true, true, false, true],
};
// Create active maintenance window
const maintenanceWindow = await createMaintenanceWindow();
const activeMaintenanceWindows = await getActiveMaintenanceWindows();
expect(activeMaintenanceWindows[0].id).eql(maintenanceWindow.id);
// Create action and rule
const action = await createAction();
const rule = await createRule({ actionId: action.id, pattern });
// Run the first time - active
await getRuleEvents({ id: rule.id, activeInstance: 1 });
await expectNoActionsFired(rule.id);
// Run again - active
await runSoon(rule.id);
await getRuleEvents({ id: rule.id, activeInstance: 2 });
await expectNoActionsFired(rule.id);
// Run again - recovered
await runSoon(rule.id);
await getRuleEvents({ id: rule.id, activeInstance: 2, recoveredInstance: 1 });
await expectNoActionsFired(rule.id);
// Run again - active again
await runSoon(rule.id);
await getRuleEvents({ id: rule.id, activeInstance: 3, recoveredInstance: 1 });
await expectNoActionsFired(rule.id);
});
it('alerts triggered within a MW should not fire actions if active or recovered outside a MW', async () => {
const pattern = {
instance: [true, true, false, true],
};
// Create active maintenance window
const maintenanceWindow = await createMaintenanceWindow();
const activeMaintenanceWindows = await getActiveMaintenanceWindows();
expect(activeMaintenanceWindows[0].id).eql(maintenanceWindow.id);
// Create action and rule
const action = await createAction();
const rule = await createRule({ actionId: action.id, pattern });
// Run the first time - active
await getRuleEvents({ id: rule.id, activeInstance: 1 });
await expectNoActionsFired(rule.id);
// End the maintenance window
await finishMaintenanceWindow(maintenanceWindow.id);
const empty = await getActiveMaintenanceWindows();
expect(empty).eql([]);
// Run again - active
await runSoon(rule.id);
await getRuleEvents({ id: rule.id, activeInstance: 2 });
await expectNoActionsFired(rule.id);
// Run again - recovered
await runSoon(rule.id);
await getRuleEvents({ id: rule.id, activeInstance: 2, recoveredInstance: 1 });
await expectNoActionsFired(rule.id);
// Run again - active again, this time fire the action since its a new alert instance
await runSoon(rule.id);
await getRuleEvents({
id: rule.id,
action: 1,
activeInstance: 3,
recoveredInstance: 1,
});
});
// Helper functions:
async function createRule({
actionId,
pattern,
}: {
actionId: string;
pattern: { instance: boolean[] };
}) {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
name: 'test-rule',
rule_type_id: 'test.patternFiring',
schedule: { interval: '24h' },
throttle: null,
notify_when: 'onActiveAlert',
params: {
pattern,
},
actions: [
{
id: actionId,
group: 'default',
params: {},
},
{
id: actionId,
group: 'recovered',
params: {},
},
],
})
)
.expect(200);
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
return createdRule;
}
async function createAction() {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'MY action',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
return createdAction;
}
async function createMaintenanceWindow() {
const { body: window } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({
title: 'test-maintenance-window-1',
duration: 60 * 60 * 1000, // 1 hr
r_rule: {
dtstart: moment.utc().toISOString(),
tzid: 'UTC',
freq: 0, // yearly
count: 1,
},
})
.expect(200);
objectRemover.add(Spaces.space1.id, window.id, 'rules/maintenance_window', 'alerting', true);
return window;
}
async function getActiveMaintenanceWindows() {
const { body: activeMaintenanceWindows } = await supertest
.get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window/_active`)
.set('kbn-xsrf', 'foo')
.expect(200);
return activeMaintenanceWindows;
}
function finishMaintenanceWindow(id: string) {
return supertest
.post(
`${getUrlPrefix(
Spaces.space1.id
)}/internal/alerting/rules/maintenance_window/${id}/_finish`
)
.set('kbn-xsrf', 'foo')
.expect(200);
}
async function getRuleEvents({
id,
action,
newInstance,
activeInstance,
recoveredInstance,
}: {
id: string;
action?: number;
newInstance?: number;
activeInstance?: number;
recoveredInstance?: number;
}) {
const actions: Array<[string, { equal: number }]> = [];
if (action) {
actions.push(['execute-action', { equal: action }]);
}
if (newInstance) {
actions.push(['new-instance', { equal: newInstance }]);
}
if (activeInstance) {
actions.push(['active-instance', { equal: activeInstance }]);
}
if (recoveredInstance) {
actions.push(['recovered-instance', { equal: recoveredInstance }]);
}
return retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces.space1.id,
type: 'alert',
id,
provider: 'alerting',
actions: new Map(actions),
});
});
}
async function expectNoActionsFired(id: string) {
const events = await retry.try(async () => {
const { body: result } = await supertest
.get(`${getUrlPrefix(Spaces.space1.id)}/_test/event_log/alert/${id}/_find?per_page=5000`)
.expect(200);
if (!result.total) {
throw new Error('no events found yet');
}
return result.data as IValidatedEvent[];
});
const actionEvents = events.filter((event) => {
return event?.event?.action === 'execute-action';
});
expect(actionEvents.length).eql(0);
}
function runSoon(id: string) {
return retry.try(async () => {
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${id}/_run_soon`)
.set('kbn-xsrf', 'foo')
.expect(204);
});
}
});
}