[Alerting] Add alert.updatedAt field to represent date of last user edit (#83784)

* Adding alert.updatedAt field that only updates on user edit

* Updating unit tests

* Functional tests

* Updating alert attributes excluded from AAD

* Fixing test

* PR comments

* Unskipping tests and updating es archiver data
This commit is contained in:
ymao1 2020-11-19 14:52:16 -05:00 committed by GitHub
parent ad5cf9e78b
commit e45b76c1b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 181 additions and 42 deletions

View file

@ -228,14 +228,17 @@ export class AlertsClient {
this.validateActions(alertType, data.actions);
const createTime = Date.now();
const { references, actions } = await this.denormalizeActions(data.actions);
const rawAlert: RawAlert = {
...data,
...this.apiKeyAsAlertAttributes(createdAPIKey, username),
actions,
createdBy: username,
updatedBy: username,
createdAt: new Date().toISOString(),
createdAt: new Date(createTime).toISOString(),
updatedAt: new Date(createTime).toISOString(),
params: validatedAlertTypeParams as RawAlert['params'],
muteAll: false,
mutedInstanceIds: [],
@ -289,12 +292,7 @@ export class AlertsClient {
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
}
return this.getAlertFromRaw(
createdAlert.id,
createdAlert.attributes,
createdAlert.updated_at,
references
);
return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references);
}
public async get({ id }: { id: string }): Promise<SanitizedAlert> {
@ -304,7 +302,7 @@ export class AlertsClient {
result.attributes.consumer,
ReadOperations.Get
);
return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references);
return this.getAlertFromRaw(result.id, result.attributes, result.references);
}
public async getAlertState({ id }: { id: string }): Promise<AlertTaskState | void> {
@ -393,13 +391,11 @@ export class AlertsClient {
type: 'alert',
});
// eslint-disable-next-line @typescript-eslint/naming-convention
const authorizedData = data.map(({ id, attributes, updated_at, references }) => {
const authorizedData = data.map(({ id, attributes, references }) => {
ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
return this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
updated_at,
references
);
});
@ -585,6 +581,7 @@ export class AlertsClient {
params: validatedAlertTypeParams as RawAlert['params'],
actions,
updatedBy: username,
updatedAt: new Date().toISOString(),
});
try {
updatedObject = await this.unsecuredSavedObjectsClient.create<RawAlert>(
@ -607,12 +604,7 @@ export class AlertsClient {
throw e;
}
return this.getPartialAlertFromRaw(
id,
updatedObject.attributes,
updatedObject.updated_at,
updatedObject.references
);
return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
}
private apiKeyAsAlertAttributes(
@ -677,6 +669,7 @@ export class AlertsClient {
await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)),
username
),
updatedAt: new Date().toISOString(),
updatedBy: username,
});
try {
@ -751,6 +744,7 @@ export class AlertsClient {
username
),
updatedBy: username,
updatedAt: new Date().toISOString(),
});
try {
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
@ -829,6 +823,7 @@ export class AlertsClient {
apiKey: null,
apiKeyOwner: null,
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
}),
{ version }
);
@ -875,6 +870,7 @@ export class AlertsClient {
muteAll: true,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
@ -913,6 +909,7 @@ export class AlertsClient {
muteAll: false,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
@ -957,6 +954,7 @@ export class AlertsClient {
this.updateMeta({
mutedInstanceIds,
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
}),
{ version }
);
@ -999,6 +997,7 @@ export class AlertsClient {
alertId,
this.updateMeta({
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
}),
{ version }
@ -1050,19 +1049,17 @@ export class AlertsClient {
private getAlertFromRaw(
id: string,
rawAlert: RawAlert,
updatedAt: SavedObject['updated_at'],
references: SavedObjectReference[] | undefined
): Alert {
// In order to support the partial update API of Saved Objects we have to support
// partial updates of an Alert, but when we receive an actual RawAlert, it is safe
// to cast the result to an Alert
return this.getPartialAlertFromRaw(id, rawAlert, updatedAt, references) as Alert;
return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert;
}
private getPartialAlertFromRaw(
id: string,
{ createdAt, meta, scheduledTaskId, ...rawAlert }: Partial<RawAlert>,
updatedAt: SavedObject['updated_at'] = createdAt,
{ createdAt, updatedAt, meta, scheduledTaskId, ...rawAlert }: Partial<RawAlert>,
references: SavedObjectReference[] | undefined
): PartialAlert {
// Not the prettiest code here, but if we want to use most of the

View file

@ -196,6 +196,7 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
mutedInstanceIds: [],
actions: [
@ -330,6 +331,7 @@ describe('create()', () => {
"foo",
],
"throttle": null,
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@ -418,6 +420,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -555,6 +558,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -631,6 +635,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -971,6 +976,7 @@ describe('create()', () => {
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
enabled: true,
meta: {
versionApiKeyLastmodified: 'v7.10.0',
@ -1092,6 +1098,7 @@ describe('create()', () => {
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
enabled: false,
meta: {
versionApiKeyLastmodified: 'v7.10.0',

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@ -45,6 +45,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('disable()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@ -136,6 +138,7 @@ describe('disable()', () => {
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{
@ -190,6 +193,7 @@ describe('disable()', () => {
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{

View file

@ -13,7 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@ -46,6 +46,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('enable()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@ -186,6 +188,7 @@ describe('enable()', () => {
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
apiKey: null,
apiKeyOwner: null,
@ -292,6 +295,7 @@ describe('enable()', () => {
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',

View file

@ -79,6 +79,7 @@ describe('find()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',

View file

@ -59,6 +59,7 @@ describe('get()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',

View file

@ -76,6 +76,7 @@ const BaseAlertInstanceSummarySavedObject: SavedObject<RawAlert> = {
createdBy: null,
updatedBy: null,
createdAt: mockedDateString,
updatedAt: mockedDateString,
apiKey: null,
apiKeyOwner: null,
throttle: null,

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -43,6 +43,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('muteAll()', () => {
test('mutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@ -74,6 +76,7 @@ describe('muteAll()', () => {
{
muteAll: true,
mutedInstanceIds: [],
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('muteInstance()', () => {
test('mutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@ -68,6 +70,7 @@ describe('muteInstance()', () => {
'1',
{
mutedInstanceIds: ['2'],
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('unmuteAll()', () => {
test('unmutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@ -75,6 +77,7 @@ describe('unmuteAll()', () => {
{
muteAll: false,
mutedInstanceIds: [],
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('unmuteInstance()', () => {
test('unmutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@ -69,6 +71,7 @@ describe('unmuteInstance()', () => {
{
mutedInstanceIds: [],
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
},
{ version: '123' }
);

View file

@ -140,8 +140,8 @@ describe('update()', () => {
],
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
@ -300,6 +300,7 @@ describe('update()', () => {
"foo",
],
"throttle": null,
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@ -362,6 +363,7 @@ describe('update()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -484,6 +486,7 @@ describe('update()', () => {
"foo",
],
"throttle": "5m",
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@ -534,6 +537,7 @@ describe('update()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@ -648,6 +652,7 @@ describe('update()', () => {
"foo",
],
"throttle": "5m",
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);

View file

@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
setGlobalDate();
describe('updateApiKey()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@ -113,6 +115,7 @@ describe('updateApiKey()', () => {
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
@ -162,6 +165,7 @@ describe('updateApiKey()', () => {
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{

View file

@ -16,6 +16,7 @@ export const AlertAttributesExcludedFromAAD = [
'muteAll',
'mutedInstanceIds',
'updatedBy',
'updatedAt',
'executionStatus',
];
@ -28,6 +29,7 @@ export type AlertAttributesExcludedFromAADType =
| 'muteAll'
| 'mutedInstanceIds'
| 'updatedBy'
| 'updatedAt'
| 'executionStatus';
export function setupSavedObjects(

View file

@ -62,6 +62,9 @@
"createdAt": {
"type": "date"
},
"updatedAt": {
"type": "date"
},
"apiKey": {
"type": "binary"
},

View file

@ -261,8 +261,48 @@ describe('7.10.0 migrates with failure', () => {
});
});
describe('7.11.0', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(
(shouldMigrateWhenPredicate, migration) => migration
);
});
test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const alert = getMockData({}, true);
expect(migration711(alert, { log })).toEqual({
...alert,
attributes: {
...alert.attributes,
updatedAt: alert.updated_at,
},
});
});
test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const alert = getMockData({});
expect(migration711(alert, { log })).toEqual({
...alert,
attributes: {
...alert.attributes,
updatedAt: alert.attributes.createdAt,
},
});
});
});
function getUpdatedAt(): string {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() + 2);
return updatedAt.toISOString();
}
function getMockData(
overwrites: Record<string, unknown> = {}
overwrites: Record<string, unknown> = {},
withSavedObjectUpdatedAt: boolean = false
): SavedObjectUnsanitizedDoc<Partial<RawAlert>> {
return {
attributes: {
@ -295,6 +335,7 @@ function getMockData(
],
...overwrites,
},
updated_at: withSavedObjectUpdatedAt ? getUpdatedAt() : undefined,
id: uuid.v4(),
type: 'alert',
};

View file

@ -37,8 +37,15 @@ export function getMigrations(
)
);
const migrationAlertUpdatedAtDate = encryptedSavedObjects.createMigration<RawAlert, RawAlert>(
// migrate all documents in 7.11 in order to add the "updatedAt" field
(doc): doc is SavedObjectUnsanitizedDoc<RawAlert> => true,
pipeMigrations(setAlertUpdatedAtDate)
);
return {
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtDate, '7.11.0'),
};
}
@ -59,6 +66,19 @@ function executeMigrationWithErrorHandling(
};
}
const setAlertUpdatedAtDate = (
doc: SavedObjectUnsanitizedDoc<RawAlert>
): SavedObjectUnsanitizedDoc<RawAlert> => {
const updatedAt = doc.updated_at || doc.attributes.createdAt;
return {
...doc,
attributes: {
...doc.attributes,
updatedAt,
},
};
};
const consumersToChange: Map<string, string> = new Map(
Object.entries({
alerting: 'alerts',

View file

@ -95,6 +95,7 @@ const DefaultAttributes = {
muteAll: true,
mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'],
updatedBy: 'someone',
updatedAt: '2019-02-12T21:01:22.479Z',
};
const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' };

View file

@ -147,6 +147,7 @@ export interface RawAlert extends SavedObjectAttributes {
createdBy: string | null;
updatedBy: string | null;
createdAt: string;
updatedAt: string;
apiKey: string | null;
apiKeyOwner: string | null;
throttle: string | null;

View file

@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login';
import { DETECTIONS_URL } from '../urls/navigation';
// FLAKY: https://github.com/elastic/kibana/issues/83773
describe.skip('Alerts', () => {
describe('Alerts', () => {
context('Closing alerts', () => {
beforeEach(() => {
esArchiverLoad('alerts');

View file

@ -114,8 +114,7 @@ const expectedEditedtags = editedRule.tags.join('');
const expectedEditedIndexPatterns =
editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns;
// SKIP: https://github.com/elastic/kibana/issues/83769
describe.skip('Custom detection rules creation', () => {
describe('Custom detection rules creation', () => {
before(() => {
esArchiverLoad('timeline');
});
@ -216,8 +215,7 @@ describe.skip('Custom detection rules creation', () => {
});
});
// FLAKY: https://github.com/elastic/kibana/issues/83793
describe.skip('Custom detection rules deletion and edition', () => {
describe('Custom detection rules deletion and edition', () => {
beforeEach(() => {
esArchiverLoad('custom_rules');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);

View file

@ -17,8 +17,7 @@ import { DETECTIONS_URL } from '../urls/navigation';
const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson';
// SKIP: https://github.com/elastic/kibana/issues/83769
describe.skip('Export rules', () => {
describe('Export rules', () => {
before(() => {
esArchiverLoad('export_rule');
cy.server();

View file

@ -17,8 +17,7 @@ import { loginAndWaitForPage } from '../tasks/login';
import { DETECTIONS_URL } from '../urls/navigation';
// FLAKY: https://github.com/elastic/kibana/issues/83771
describe.skip('Alerts timeline', () => {
describe('Alerts timeline', () => {
beforeEach(() => {
esArchiverLoad('timeline_alerts');
loginAndWaitForPage(DETECTIONS_URL);

View file

@ -17,8 +17,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { CASES_URL } from '../urls/navigation';
// FLAKY: https://github.com/elastic/kibana/issues/65278
describe.skip('Cases connectors', () => {
describe('Cases connectors', () => {
before(() => {
cy.server();
cy.route('POST', '**/api/actions/action').as('createConnector');

View file

@ -91,6 +91,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
});
expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
expect(Date.parse(response.body.updatedAt)).to.eql(Date.parse(response.body.createdAt));
expect(typeof response.body.scheduledTaskId).to.be('string');
const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId);

View file

@ -63,6 +63,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
const alertUpdatedAt = response.body.updatedAt;
dates.push(response.body.executionStatus.lastExecutionDate);
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
@ -70,6 +71,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
dates.push(executionStatus.lastExecutionDate);
dates.push(Date.now());
ensureDatetimesAreOrdered(dates);
ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt);
// Ensure AAD isn't broken
await checkAAD({
@ -97,6 +99,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
const alertUpdatedAt = response.body.updatedAt;
dates.push(response.body.executionStatus.lastExecutionDate);
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
@ -104,6 +107,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
dates.push(executionStatus.lastExecutionDate);
dates.push(Date.now());
ensureDatetimesAreOrdered(dates);
ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt);
// Ensure AAD isn't broken
await checkAAD({
@ -128,6 +132,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
const alertUpdatedAt = response.body.updatedAt;
dates.push(response.body.executionStatus.lastExecutionDate);
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
@ -135,6 +140,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
dates.push(executionStatus.lastExecutionDate);
dates.push(Date.now());
ensureDatetimesAreOrdered(dates);
ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt);
// Ensure AAD isn't broken
await checkAAD({
@ -162,12 +168,14 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
const alertUpdatedAt = response.body.updatedAt;
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
const executionStatus = await waitForStatus(alertId, new Set(['error']));
expect(executionStatus.error).to.be.ok();
expect(executionStatus.error.reason).to.be('execute');
expect(executionStatus.error.message).to.be('this alert is intended to fail');
ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt);
});
it('should eventually have error reason "unknown" when appropriate', async () => {
@ -183,6 +191,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
const alertUpdatedAt = response.body.updatedAt;
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
let executionStatus = await waitForStatus(alertId, new Set(['ok']));
@ -201,6 +210,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
executionStatus = await waitForStatus(alertId, new Set(['error']));
expect(executionStatus.error).to.be.ok();
expect(executionStatus.error.reason).to.be('unknown');
ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt);
const message = 'params invalid: [param1]: expected value of type [string] but got [number]';
expect(executionStatus.error.message).to.be(message);
@ -306,6 +316,18 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
await delay(WaitForStatusIncrement);
return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement);
}
async function ensureAlertUpdatedAtHasNotChanged(alertId: string, originalUpdatedAt: string) {
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${alertId}`
);
const { updatedAt, executionStatus } = response.body;
expect(Date.parse(updatedAt)).to.be.greaterThan(0);
expect(Date.parse(updatedAt)).to.eql(Date.parse(originalUpdatedAt));
expect(Date.parse(executionStatus.lastExecutionDate)).to.be.greaterThan(
Date.parse(originalUpdatedAt)
);
}
}
function expectErrorExecutionStatus(executionStatus: Record<string, any>, startDate: number) {

View file

@ -82,5 +82,14 @@ export default function createGetTests({ getService }: FtrProviderContext) {
},
]);
});
it('7.11.0 migrates alerts to contain `updatedAt` field', async () => {
const response = await supertest.get(
`${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e`
);
expect(response.status).to.eql(200);
expect(response.body.updatedAt).to.eql('2020-06-17T15:35:39.839Z');
});
});
}

View file

@ -321,6 +321,9 @@
"throttle": {
"type": "keyword"
},
"updatedAt": {
"type": "date"
},
"updatedBy": {
"type": "keyword"
},

View file

@ -191,6 +191,9 @@
"throttle": {
"type": "keyword"
},
"updatedAt": {
"type": "date"
},
"updatedBy": {
"type": "keyword"
}

View file

@ -322,6 +322,9 @@
"throttle": {
"type": "keyword"
},
"updatedAt": {
"type": "date"
},
"updatedBy": {
"type": "keyword"
}