mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.17`: - [[ResponseOps][Rules] Validate timezone in rule routes (#201508)](https://github.com/elastic/kibana/pull/201508) <!--- Backport version: 9.6.4 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Christos Nasikas","email":"christos.nasikas@elastic.co"},"sourceCommit":{"committedDate":"2025-01-24T17:46:24Z","message":"[ResponseOps][Rules] Validate timezone in rule routes (#201508)\n\n## Summary\r\n\r\nThis PR adds validation only for internal routes that use the `rRule`\r\nschema.\r\n\r\n## Testing\r\n\r\n1. Create a rule in main.\r\n2. Snooze the rule by using the API as\r\n\r\n```\r\nPOST /internal/alerting/rule/<ruleId>/_snooze\r\n{\r\n \"snooze_schedule\": {\r\n \"id\": \"e58e2340-dba6-454c-8308-b2ca66a7cf7b\",\r\n \"duration\": 86400000,\r\n \"rRule\": {\r\n \"dtstart\": \"2024-09-04T09:27:37.011Z\",\r\n \"tzid\": \"invalid\",\r\n \"freq\": 2,\r\n \"interval\": 1,\r\n \"byweekday\": [\r\n \"invalid\"\r\n ]\r\n }\r\n }\r\n}\r\n```\r\n\r\n4. Go to the rules page and verify that the rules are not loaded.\r\n5. Switch to my PR.\r\n6. Go to the rules page and verify that the rules load.\r\n\r\n### Checklist\r\n\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\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"9a3fc89629e1a6cec2f5200bb75099fcab866701","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","Feature:Alerting/RulesFramework","backport:prev-major","v8.18.0","v8.16.4","v8.17.2"],"title":"[ResponseOps][Rules] Validate timezone in rule routes","number":201508,"url":"https://github.com/elastic/kibana/pull/201508","mergeCommit":{"message":"[ResponseOps][Rules] Validate timezone in rule routes (#201508)\n\n## Summary\r\n\r\nThis PR adds validation only for internal routes that use the `rRule`\r\nschema.\r\n\r\n## Testing\r\n\r\n1. Create a rule in main.\r\n2. Snooze the rule by using the API as\r\n\r\n```\r\nPOST /internal/alerting/rule/<ruleId>/_snooze\r\n{\r\n \"snooze_schedule\": {\r\n \"id\": \"e58e2340-dba6-454c-8308-b2ca66a7cf7b\",\r\n \"duration\": 86400000,\r\n \"rRule\": {\r\n \"dtstart\": \"2024-09-04T09:27:37.011Z\",\r\n \"tzid\": \"invalid\",\r\n \"freq\": 2,\r\n \"interval\": 1,\r\n \"byweekday\": [\r\n \"invalid\"\r\n ]\r\n }\r\n }\r\n}\r\n```\r\n\r\n4. Go to the rules page and verify that the rules are not loaded.\r\n5. Switch to my PR.\r\n6. Go to the rules page and verify that the rules load.\r\n\r\n### Checklist\r\n\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\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"9a3fc89629e1a6cec2f5200bb75099fcab866701"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201508","number":201508,"mergeCommit":{"message":"[ResponseOps][Rules] Validate timezone in rule routes (#201508)\n\n## Summary\r\n\r\nThis PR adds validation only for internal routes that use the `rRule`\r\nschema.\r\n\r\n## Testing\r\n\r\n1. Create a rule in main.\r\n2. Snooze the rule by using the API as\r\n\r\n```\r\nPOST /internal/alerting/rule/<ruleId>/_snooze\r\n{\r\n \"snooze_schedule\": {\r\n \"id\": \"e58e2340-dba6-454c-8308-b2ca66a7cf7b\",\r\n \"duration\": 86400000,\r\n \"rRule\": {\r\n \"dtstart\": \"2024-09-04T09:27:37.011Z\",\r\n \"tzid\": \"invalid\",\r\n \"freq\": 2,\r\n \"interval\": 1,\r\n \"byweekday\": [\r\n \"invalid\"\r\n ]\r\n }\r\n }\r\n}\r\n```\r\n\r\n4. Go to the rules page and verify that the rules are not loaded.\r\n5. Switch to my PR.\r\n6. Go to the rules page and verify that the rules load.\r\n\r\n### Checklist\r\n\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\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"9a3fc89629e1a6cec2f5200bb75099fcab866701"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/208252","number":208252,"state":"OPEN"},{"branch":"8.16","label":"v8.16.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
4b08613a54
commit
d2a00e82c5
34 changed files with 1575 additions and 154 deletions
|
@ -9,13 +9,16 @@
|
|||
|
||||
import moment, { type Moment } from 'moment-timezone';
|
||||
|
||||
import { Frequency, Weekday, type WeekdayStr, type Options, type IterOptions } from './types';
|
||||
import {
|
||||
Frequency,
|
||||
Weekday,
|
||||
type WeekdayStr,
|
||||
type Options,
|
||||
type IterOptions,
|
||||
ConstructorOptions,
|
||||
} from './types';
|
||||
import { sanitizeOptions } from './sanitize';
|
||||
|
||||
type ConstructorOptions = Omit<Options, 'byweekday' | 'wkst'> & {
|
||||
byweekday?: Array<string | number> | null;
|
||||
wkst?: Weekday | WeekdayStr | number | null;
|
||||
};
|
||||
import { validateOptions } from './validate';
|
||||
|
||||
const ISO_WEEKDAYS = [
|
||||
Weekday.MO,
|
||||
|
@ -36,6 +39,7 @@ const TIMEOUT_LIMIT = 100000;
|
|||
|
||||
export class RRule {
|
||||
private options: Options;
|
||||
|
||||
constructor(options: ConstructorOptions) {
|
||||
this.options = sanitizeOptions(options as Options);
|
||||
if (typeof options.wkst === 'string') {
|
||||
|
@ -134,6 +138,16 @@ export class RRule {
|
|||
return dates;
|
||||
}
|
||||
}
|
||||
|
||||
static isValid(options: ConstructorOptions): boolean {
|
||||
try {
|
||||
validateOptions(options);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseByWeekdayPos = function (byweekday: ConstructorOptions['byweekday']) {
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('sanitizeOptions', () => {
|
|||
interval: 1,
|
||||
until: new Date('February 25, 2022 03:24:00'),
|
||||
count: 3,
|
||||
tzid: 'foobar',
|
||||
tzid: 'UTC',
|
||||
};
|
||||
|
||||
it('happy path', () => {
|
||||
|
@ -41,11 +41,18 @@ describe('sanitizeOptions', () => {
|
|||
});
|
||||
|
||||
it('throws an error when tzid is missing', () => {
|
||||
expect(() => sanitizeOptions({ ...options, tzid: '' })).toThrowError(
|
||||
// @ts-expect-error
|
||||
expect(() => sanitizeOptions({ ...options, tzid: null })).toThrowError(
|
||||
'Cannot create RRule: tzid is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when tzid is invalid', () => {
|
||||
expect(() => sanitizeOptions({ ...options, tzid: 'invalid' })).toThrowError(
|
||||
'Cannot create RRule: tzid is invalid'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when until field is invalid', () => {
|
||||
expect(() =>
|
||||
sanitizeOptions({
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import type { Options } from './types';
|
||||
|
||||
export function sanitizeOptions(opts: Options) {
|
||||
|
@ -25,6 +26,10 @@ export function sanitizeOptions(opts: Options) {
|
|||
throw new Error('Cannot create RRule: dtstart is an invalid date');
|
||||
}
|
||||
|
||||
if (moment.tz.zone(options.tzid) == null) {
|
||||
throw new Error('Cannot create RRule: tzid is invalid');
|
||||
}
|
||||
|
||||
if (options.until && isNaN(options.until.getTime())) {
|
||||
throw new Error('Cannot create RRule: until is an invalid date');
|
||||
}
|
||||
|
@ -39,7 +44,6 @@ export function sanitizeOptions(opts: Options) {
|
|||
}
|
||||
}
|
||||
|
||||
// Omit invalid options
|
||||
if (options.bymonth) {
|
||||
// Only months between 1 and 12 are valid
|
||||
options.bymonth = options.bymonth.filter(
|
||||
|
|
|
@ -52,3 +52,8 @@ export type Options = Omit<IterOptions, 'refDT'> & {
|
|||
count?: number;
|
||||
tzid: string;
|
||||
};
|
||||
|
||||
export type ConstructorOptions = Omit<Options, 'byweekday' | 'wkst'> & {
|
||||
byweekday?: Array<string | number> | null;
|
||||
wkst?: Weekday | WeekdayStr | number | null;
|
||||
};
|
||||
|
|
215
packages/kbn-rrule/validate.test.ts
Normal file
215
packages/kbn-rrule/validate.test.ts
Normal file
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { validateOptions } from './validate';
|
||||
import { Weekday, Frequency, type ConstructorOptions } from './types';
|
||||
|
||||
describe('validateOptions', () => {
|
||||
const options: ConstructorOptions = {
|
||||
wkst: Weekday.MO,
|
||||
byyearday: [1, 2, 3],
|
||||
bymonth: [1],
|
||||
bysetpos: [1],
|
||||
bymonthday: [1],
|
||||
byweekday: [Weekday.MO],
|
||||
byhour: [1],
|
||||
byminute: [1],
|
||||
bysecond: [1],
|
||||
dtstart: new Date('September 3, 1998 03:24:00'),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
until: new Date('February 25, 2022 03:24:00'),
|
||||
count: 3,
|
||||
tzid: 'UTC',
|
||||
};
|
||||
|
||||
it('happy path', () => {
|
||||
expect(() => validateOptions(options)).not.toThrow();
|
||||
});
|
||||
|
||||
describe('dtstart', () => {
|
||||
it('throws an error when dtstart is missing', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateOptions({ ...options, dtstart: null })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"dtstart is required"`);
|
||||
});
|
||||
|
||||
it('throws an error when dtstart is not a valid date', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, dtstart: new Date('invalid') })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"dtstart is an invalid date"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tzid', () => {
|
||||
it('throws an error when tzid is missing', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => validateOptions({ ...options, tzid: null })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"tzid is required"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when tzid is invalid', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, tzid: 'invalid' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"tzid is an invalid timezone"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interval', () => {
|
||||
it('throws an error when count is not a number', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateOptions({ ...options, interval: 'invalid' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"interval must be an integer greater than 0"`);
|
||||
});
|
||||
|
||||
it('throws an error when interval is not an integer', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, interval: 1.5 })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"interval must be an integer greater than 0"`);
|
||||
});
|
||||
|
||||
it('throws an error when interval is <= 0', () => {
|
||||
expect(() => validateOptions({ ...options, interval: 0 })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"interval must be an integer greater than 0"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('until', () => {
|
||||
it('throws an error when until field is an invalid date', () => {
|
||||
expect(() =>
|
||||
validateOptions({
|
||||
...options,
|
||||
until: new Date('invalid'),
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"until is an invalid date"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('throws an error when count is not a number', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateOptions({ ...options, count: 'invalid' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"count must be an integer greater than 0"`);
|
||||
});
|
||||
|
||||
it('throws an error when count is not an integer', () => {
|
||||
expect(() => validateOptions({ ...options, count: 1.5 })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"count must be an integer greater than 0"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when count is <= 0', () => {
|
||||
expect(() => validateOptions({ ...options, count: 0 })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"count must be an integer greater than 0"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth', () => {
|
||||
it('throws an error with out of range values', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, bymonth: [0, 6, 13] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonth must be an array of numbers between 1 and 12"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error with string values', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateOptions({ ...options, bymonth: ['invalid'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonth must be an array of numbers between 1 and 12"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when is empty', () => {
|
||||
expect(() => validateOptions({ ...options, bymonth: [] })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonth must be an array of numbers between 1 and 12"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonthday', () => {
|
||||
it('throws an error with out of range values', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, bymonthday: [0, 15, 32] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonthday must be an array of numbers between 1 and 31"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error with string values', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateOptions({ ...options, bymonthday: ['invalid'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonthday must be an array of numbers between 1 and 31"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when is empty', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, bymonthday: [] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"bymonthday must be an array of numbers between 1 and 31"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('byweekday', () => {
|
||||
it('throws an error with out of range values when it contains only numbers', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, byweekday: [0, 4, 8] })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"byweekday numbers must been between 1 and 7"`);
|
||||
});
|
||||
|
||||
it('throws an error with invalid values when it contains only string', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, byweekday: ['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"byweekday strings must be valid weekday strings"`);
|
||||
});
|
||||
|
||||
it('throws an error when is empty', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, byweekday: [] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"byweekday must be an array of at least one string or number"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error with mixed values', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, byweekday: [2, 'MO'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"byweekday values can be either numbers or strings, not both"`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw with properly formed byweekday strings', () => {
|
||||
expect(() =>
|
||||
validateOptions({
|
||||
...options,
|
||||
byweekday: ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO'],
|
||||
})
|
||||
).not.toThrow(`"byweekday numbers must been between 1 and 7"`);
|
||||
});
|
||||
|
||||
it('does not throw with non recurrence values', () => {
|
||||
expect(() =>
|
||||
validateOptions({ ...options, byweekday: ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] })
|
||||
).not.toThrow(`"byweekday numbers must been between 1 and 7"`);
|
||||
});
|
||||
});
|
||||
});
|
92
packages/kbn-rrule/validate.ts
Normal file
92
packages/kbn-rrule/validate.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { ConstructorOptions } from './types';
|
||||
|
||||
export function validateOptions(opts: ConstructorOptions) {
|
||||
const byWeekDayRegex = new RegExp('^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$');
|
||||
const options = { ...opts };
|
||||
|
||||
if (options.dtstart == null) {
|
||||
throw new Error('dtstart is required');
|
||||
}
|
||||
|
||||
if (options.tzid == null) {
|
||||
throw new Error('tzid is required');
|
||||
}
|
||||
|
||||
if (isNaN(options.dtstart.getTime())) {
|
||||
throw new Error('dtstart is an invalid date');
|
||||
}
|
||||
|
||||
if (moment.tz.zone(options.tzid) == null) {
|
||||
throw new Error('tzid is an invalid timezone');
|
||||
}
|
||||
|
||||
if (options.interval != null && (!Number.isInteger(options.interval) || options.interval < 1)) {
|
||||
throw new Error('interval must be an integer greater than 0');
|
||||
}
|
||||
|
||||
if (options.until != null && isNaN(options.until.getTime())) {
|
||||
throw new Error('until is an invalid date');
|
||||
}
|
||||
|
||||
if (options.count != null && (!Number.isInteger(options.count) || options.count < 1)) {
|
||||
throw new Error('count must be an integer greater than 0');
|
||||
}
|
||||
|
||||
if (
|
||||
options.bymonthday != null &&
|
||||
(options.bymonthday.length < 1 ||
|
||||
options.bymonthday.some(
|
||||
(monthDay) => !Number.isInteger(monthDay) || monthDay < 1 || monthDay > 31
|
||||
))
|
||||
) {
|
||||
throw new Error('bymonthday must be an array of numbers between 1 and 31');
|
||||
}
|
||||
|
||||
if (
|
||||
options.bymonth != null &&
|
||||
(options.bymonth.length < 1 ||
|
||||
options.bymonth.some((month) => !Number.isInteger(month) || month < 1 || month > 12))
|
||||
) {
|
||||
throw new Error('bymonth must be an array of numbers between 1 and 12');
|
||||
}
|
||||
|
||||
if (options.byweekday != null) {
|
||||
if (options.byweekday.length < 1) {
|
||||
throw new Error('byweekday must be an array of at least one string or number');
|
||||
}
|
||||
|
||||
const byWeekDayNumbers = options.byweekday?.filter(
|
||||
(weekDay) => typeof weekDay === 'number'
|
||||
) as number[];
|
||||
|
||||
const byWeekDayStrings = options.byweekday?.filter(
|
||||
(weekDay) => typeof weekDay === 'string'
|
||||
) as string[];
|
||||
|
||||
if (byWeekDayNumbers.length > 0 && byWeekDayStrings.length > 0) {
|
||||
throw new Error('byweekday values can be either numbers or strings, not both');
|
||||
}
|
||||
|
||||
if (
|
||||
byWeekDayNumbers.some(
|
||||
(weekDayNum) => !Number.isInteger(weekDayNum) || weekDayNum < 1 || weekDayNum > 7
|
||||
)
|
||||
) {
|
||||
throw new Error('byweekday numbers must been between 1 and 7');
|
||||
}
|
||||
|
||||
if (byWeekDayStrings.some((weekDayStr) => !byWeekDayRegex.test(weekDayStr))) {
|
||||
throw new Error('byweekday strings must be valid weekday strings');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 { rRuleRequestSchema } from './v1';
|
||||
|
||||
describe('rRuleRequestSchema', () => {
|
||||
const basicRequest = {
|
||||
dtstart: '2021-01-01T00:00:00Z',
|
||||
tzid: 'UTC',
|
||||
freq: 0,
|
||||
byweekday: ['MO'],
|
||||
bymonthday: [1],
|
||||
bymonth: [2],
|
||||
interval: 2,
|
||||
until: '2021-02-01T00:00:00Z',
|
||||
count: 2,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2021-01-01T00:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('no errors on proper request', () => {
|
||||
expect(rRuleRequestSchema.validate(basicRequest)).toEqual(basicRequest);
|
||||
});
|
||||
|
||||
describe('tzid', () => {
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, tzid: 'invalid' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interval', () => {
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, interval: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error when it is a number but not an integer', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, interval: 1.5 })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error when is set to less than one', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, interval: 0 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('until', () => {
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, until: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error if the date is in the past', () => {
|
||||
expect(() =>
|
||||
// one year ago
|
||||
rRuleRequestSchema.validate({ ...basicRequest, until: '2020-01-01T00:00:00Z' })
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, count: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error when it is a number but not an integer', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, count: 1.5 })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error when is set to less than one', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, count: 0 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('byweekday', () => {
|
||||
test('returns an error if the are no values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, byweekday: [] })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() =>
|
||||
rRuleRequestSchema.validate({ ...basicRequest, byweekday: ['invalid'] })
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonthday', () => {
|
||||
test('returns an error if the are no values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonthday: [] })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() =>
|
||||
rRuleRequestSchema.validate({ ...basicRequest, bymonthday: ['invalid'] })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error if the values are less than one', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonthday: [0] })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error if the values are bigger than 31', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonthday: [32] })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth', () => {
|
||||
test('returns an error if the are no values', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonth: [] })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error with invalid values', () => {
|
||||
expect(() =>
|
||||
rRuleRequestSchema.validate({ ...basicRequest, bymonth: ['invalid'] })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error if the values are less than one', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonth: [0] })).toThrow();
|
||||
});
|
||||
|
||||
test('returns an error if the values are bigger than 12', () => {
|
||||
expect(() => rRuleRequestSchema.validate({ ...basicRequest, bymonth: [13] })).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,16 +6,16 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validateTimezone } from '../../../rule/validation/validate_timezone/v1';
|
||||
import {
|
||||
validateStartDateV1,
|
||||
validateEndDateV1,
|
||||
createValidateRecurrenceByV1,
|
||||
validateRecurrenceByWeekdayV1,
|
||||
} from '../../validation';
|
||||
|
||||
export const rRuleRequestSchema = schema.object({
|
||||
dtstart: schema.string({ validate: validateStartDateV1 }),
|
||||
tzid: schema.string(),
|
||||
tzid: schema.string({ validate: validateTimezone }),
|
||||
freq: schema.maybe(
|
||||
schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)])
|
||||
),
|
||||
|
@ -42,17 +42,10 @@ export const rRuleRequestSchema = schema.object({
|
|||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
minSize: 1,
|
||||
validate: validateRecurrenceByWeekdayV1,
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonth'),
|
||||
})
|
||||
),
|
||||
bymonthday: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 31 }), { minSize: 1 })),
|
||||
bymonth: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 12 }), { minSize: 1 })),
|
||||
});
|
||||
|
|
|
@ -7,10 +7,8 @@
|
|||
|
||||
export { validateStartDate } from './validate_start_date/latest';
|
||||
export { validateEndDate } from './validate_end_date/latest';
|
||||
export { createValidateRecurrenceBy } from './validate_recurrence_by/latest';
|
||||
export { validateRecurrenceByWeekday } from './validate_recurrence_by_weekday/latest';
|
||||
|
||||
export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1';
|
||||
export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1';
|
||||
export { createValidateRecurrenceBy as createValidateRecurrenceByV1 } from './validate_recurrence_by/v1';
|
||||
export { validateRecurrenceByWeekday as validateRecurrenceByWeekdayV1 } from './validate_recurrence_by_weekday/v1';
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* 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 * from './v1';
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* 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 const validateRecurrenceBy = <T>(name: string, array: T[]) => {
|
||||
if (array.length === 0) {
|
||||
return `rRule ${name} cannot be empty`;
|
||||
}
|
||||
};
|
||||
|
||||
export const createValidateRecurrenceBy = <T>(name: string) => {
|
||||
return (array: T[]) => validateRecurrenceBy(name, array);
|
||||
};
|
|
@ -11,7 +11,17 @@ import { validateSnoozeScheduleV1 } from '../../validation';
|
|||
|
||||
export const ruleSnoozeScheduleSchema = schema.object(
|
||||
{
|
||||
id: schema.maybe(schema.string()),
|
||||
id: schema.maybe(
|
||||
schema.string({
|
||||
validate: (id: string) => {
|
||||
const regex = new RegExp('^[a-z0-9_-]+$', 'g');
|
||||
|
||||
if (!regex.test(id)) {
|
||||
return `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`;
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
duration: schema.number(),
|
||||
rRule: rRuleRequestSchemaV1,
|
||||
},
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export function validateTimezone(timezone: string) {
|
||||
if (moment.tz.names().includes(timezone)) {
|
||||
if (moment.tz.zone(timezone) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'string is not a valid timezone: ' + timezone;
|
||||
}
|
||||
|
|
|
@ -58,8 +58,8 @@ jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invali
|
|||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../lib/snooze/is_snooze_active', () => ({
|
||||
isSnoozeActive: jest.fn(),
|
||||
jest.mock('../../../../lib/snooze/get_active_snooze_if_exist', () => ({
|
||||
getActiveSnoozeIfExist: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('uuid', () => {
|
||||
|
@ -71,7 +71,9 @@ jest.mock('../get_schedule_frequency', () => ({
|
|||
validateScheduleLimit: jest.fn(),
|
||||
}));
|
||||
|
||||
const { isSnoozeActive } = jest.requireMock('../../../../lib/snooze/is_snooze_active');
|
||||
const { getActiveSnoozeIfExist } = jest.requireMock(
|
||||
'../../../../lib/snooze/get_active_snooze_if_exist'
|
||||
);
|
||||
const { validateScheduleLimit } = jest.requireMock('../get_schedule_frequency');
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
|
@ -1882,7 +1884,7 @@ describe('bulkEdit()', () => {
|
|||
|
||||
describe('snoozeSchedule operations', () => {
|
||||
afterEach(() => {
|
||||
isSnoozeActive.mockImplementation(() => false);
|
||||
getActiveSnoozeIfExist.mockImplementation(() => false);
|
||||
});
|
||||
|
||||
const getSnoozeSchedule = (useId: boolean = true) => {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { RulesClientContext } from '../../../../rules_client';
|
||||
import { snoozeRule } from './snooze_rule';
|
||||
import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { SnoozeRuleOptions } from './types';
|
||||
|
||||
const loggerErrorMock = jest.fn();
|
||||
const getBulkMock = jest.fn();
|
||||
|
||||
const savedObjectsMock = savedObjectsRepositoryMock.create();
|
||||
savedObjectsMock.get = jest.fn().mockReturnValue({
|
||||
attributes: {
|
||||
actions: [],
|
||||
},
|
||||
version: '9.0.0',
|
||||
});
|
||||
|
||||
const context = {
|
||||
logger: { error: loggerErrorMock },
|
||||
getActionsClient: () => {
|
||||
return {
|
||||
getBulk: getBulkMock,
|
||||
};
|
||||
},
|
||||
unsecuredSavedObjectsClient: savedObjectsMock,
|
||||
authorization: { ensureAuthorized: async () => {} },
|
||||
ruleTypeRegistry: {
|
||||
ensureRuleTypeEnabled: () => {},
|
||||
},
|
||||
getUserName: async () => {},
|
||||
} as unknown as RulesClientContext;
|
||||
|
||||
describe('validate snooze params and body', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw bad request for invalid params', async () => {
|
||||
const invalidParams = {
|
||||
id: 22,
|
||||
snoozeSchedule: getSnoozeSchedule(),
|
||||
};
|
||||
|
||||
// @ts-expect-error: testing invalid params
|
||||
await expect(snoozeRule(context, invalidParams)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating snooze - [id]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw bad request for invalid snooze schedule', async () => {
|
||||
const invalidParams = {
|
||||
id: '123',
|
||||
// @ts-expect-error: testing invalid params
|
||||
snoozeSchedule: getSnoozeSchedule({ rRule: { dtstart: 'invalid' } }),
|
||||
};
|
||||
|
||||
await expect(snoozeRule(context, invalidParams)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating snooze - [rRule.dtstart]: Invalid date: invalid"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const getSnoozeSchedule = (
|
||||
override?: SnoozeRuleOptions['snoozeSchedule']
|
||||
): SnoozeRuleOptions['snoozeSchedule'] => {
|
||||
return {
|
||||
id: '123',
|
||||
duration: 28800000,
|
||||
rRule: {
|
||||
dtstart: '2010-09-19T11:49:59.329Z',
|
||||
count: 1,
|
||||
tzid: 'UTC',
|
||||
},
|
||||
...override,
|
||||
};
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import Boom from '@hapi/boom';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
import { ruleSnoozeScheduleSchema } from '../../../../../common/routes/rule/request';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { getRuleSavedObject } from '../../../../rules_client/lib';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
|
@ -30,8 +31,9 @@ export async function snoozeRule(
|
|||
): Promise<void> {
|
||||
try {
|
||||
snoozeRuleParamsSchema.validate({ id });
|
||||
ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule });
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating snooze params - ${error.message}`);
|
||||
throw Boom.badRequest(`Error validating snooze - ${error.message}`);
|
||||
}
|
||||
const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart);
|
||||
if (snoozeDateValidationMsg) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { first, isEmpty } from 'lodash';
|
||||
import { SanitizedRule, RuleTypeParams } from '../../common/rule';
|
||||
import { isSnoozeActive } from './snooze/is_snooze_active';
|
||||
import { getActiveSnoozeIfExist } from './snooze/get_active_snooze_if_exist';
|
||||
|
||||
type RuleSnoozeProps = Pick<SanitizedRule<RuleTypeParams>, 'muteAll' | 'snoozeSchedule'>;
|
||||
type ActiveSnoozes = Array<{ snoozeEndTime: Date; id: string; lastOccurrence?: Date }>;
|
||||
|
@ -19,7 +19,7 @@ export function getActiveSnoozes(rule: RuleSnoozeProps): ActiveSnoozes | null {
|
|||
|
||||
return (
|
||||
rule.snoozeSchedule
|
||||
.map((snooze) => isSnoozeActive(snooze))
|
||||
.map((snooze) => getActiveSnoozeIfExist(snooze))
|
||||
.filter(Boolean)
|
||||
// Sort in descending snoozeEndTime order
|
||||
.sort((a, b) => b!.snoozeEndTime.getTime() - a!.snoozeEndTime.getTime()) as ActiveSnoozes
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { Frequency } from '@kbn/rrule';
|
||||
import sinon from 'sinon';
|
||||
import { RRuleRecord } from '../../types';
|
||||
import { getActiveSnoozeIfExist } from './get_active_snooze_if_exist';
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
||||
describe('getActiveSnoozeIfExist', () => {
|
||||
afterAll(() => fakeTimer.restore());
|
||||
|
||||
test('snooze is NOT active byweekday', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 27 2023 08:15:00 GMT+0000 - Monday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-27T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Feb 24 2023 23:00:00 GMT+0000 - Friday
|
||||
// - End date of: Feb 27 2023 06:00:00 GMT+0000 - Monday
|
||||
// - Which is obtained from start date + 2 days and 7 hours (198000000 ms)
|
||||
const snoozeA = {
|
||||
duration: 198000000,
|
||||
rRule: {
|
||||
byweekday: ['SA'],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.DAILY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-24T23:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is active byweekday', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 25 2023 08:15:00 GMT+0000 - Saturday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-25T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Feb 24 2023 23:00:00 GMT+0000 - Friday
|
||||
// - End date of: Feb 27 2023 06:00:00 GMT+0000 - Monday
|
||||
// - Which is obtained from start date + 2 days and 7 hours (198000000 ms)
|
||||
const snoozeA = {
|
||||
duration: 198000000,
|
||||
rRule: {
|
||||
byweekday: ['SA'],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.DAILY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-24T23:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-02-24T23:00:00.000Z,
|
||||
"snoozeEndTime": 2023-02-27T06:00:00.000Z,
|
||||
}
|
||||
`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is NOT active in recurrence byweekday', () => {
|
||||
// Set the current time as:
|
||||
// - March 01 2023 08:15:00 GMT+0000 - Wednesday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-03-01T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Feb 24 2023 23:00:00 GMT+0000 - Friday
|
||||
// - End date of: Feb 27 2023 06:00:00 GMT+0000 - Monday
|
||||
// - Which is obtained from start date + 2 days and 7 hours (198000000 ms)
|
||||
const snoozeA = {
|
||||
duration: 198000000,
|
||||
rRule: {
|
||||
byweekday: ['SA'],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.DAILY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-24T23:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is active in recurrence byweekday', () => {
|
||||
// Set the current time as:
|
||||
// - March 04 2023 08:15:00 GMT+0000 - Saturday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-03-04T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Feb 24 2023 23:00:00 GMT+0000 - Friday
|
||||
// - End date of: Feb 27 2023 06:00:00 GMT+0000 - Monday
|
||||
// - Which is obtained from start date + 2 days and 7 hours (198000000 ms)
|
||||
const snoozeA = {
|
||||
duration: 198000000,
|
||||
rRule: {
|
||||
byweekday: ['SA'],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.DAILY,
|
||||
interval: 1,
|
||||
dtstart: '2023-02-24T23:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-03-03T23:00:00.000Z,
|
||||
"snoozeEndTime": 2023-03-06T06:00:00.000Z,
|
||||
}
|
||||
`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is NOT active bymonth', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 27 2023 08:15:00 GMT+0000 - Monday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-09T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Jan 01 2023 00:00:00 GMT+0000 - Sunday
|
||||
// - End date of: Jan 31 2023 06:00:00 GMT+0000 - Tuesday
|
||||
// - Which is obtained from start date + 1 month (2629800000 ms)
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000, // 1 month
|
||||
rRule: {
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
bymonthday: [1],
|
||||
bymonth: [1],
|
||||
tzid: 'Europe/Madrid',
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is active bymonth', () => {
|
||||
// Set the current time as:
|
||||
// - Jan 25 2023 08:15:00 GMT+0000 - Saturday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-01-25T08:15:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Jan 01 2023 00:00:00 GMT+0000 - Sunday
|
||||
// - End date of: Jan 31 2023 06:00:00 GMT+0000 - Tuesday
|
||||
// - Which is obtained from start date + 1 month (2629800000 ms)
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000,
|
||||
rRule: {
|
||||
bymonthday: [1],
|
||||
bymonth: [1],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-01-01T00:00:00.000Z,
|
||||
"snoozeEndTime": 2023-02-01T00:00:00.000Z,
|
||||
}
|
||||
`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze is NOT active bymonth after the first month', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 01 2023 00:00:00 GMT+0000 - Wednesday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-01T00:00:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Jan 01 2023 00:00:00 GMT+0000 - Sunday
|
||||
// - End date of: Jan 31 2023 06:00:00 GMT+0000 - Tuesday
|
||||
// - Which is obtained from start date + 1 month (2629800000 ms)
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000,
|
||||
rRule: {
|
||||
bymonthday: [1],
|
||||
bymonth: [1],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
// THIS is wrong, we need to do the same thing that we did for `byweekday` for
|
||||
test('snooze is NOT active bymonth before the first month', () => {
|
||||
// Set the current time as:
|
||||
// - Dec 31 2022 23:00:00 GMT+0000 - Wednesday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2022-12-31T21:00:00.000Z'));
|
||||
|
||||
// Try to get snooze end time with:
|
||||
// - Start date of: Jan 01 2023 00:00:00 GMT+0000 - Sunday
|
||||
// - End date of: Jan 31 2023 06:00:00 GMT+0000 - Tuesday
|
||||
// - Which is obtained from start date + 1 month (2629800000 ms)
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000,
|
||||
rRule: {
|
||||
bymonthday: [1],
|
||||
bymonth: [1],
|
||||
tzid: 'Europe/Madrid',
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze still works with invalid bymonth value', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 27 2023 08:15:00 GMT+0000 - Monday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-09T08:15:00.000Z'));
|
||||
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000, // 1 month
|
||||
rRule: {
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
bymonthday: [1],
|
||||
bymonth: [0],
|
||||
tzid: 'Europe/Madrid',
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
});
|
|
@ -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 { RRule, Weekday } from '@kbn/rrule';
|
||||
import { RuleSnoozeSchedule } from '../../types';
|
||||
|
||||
const MAX_TIMESTAMP = 8640000000000000;
|
||||
|
||||
export function getActiveSnoozeIfExist(snooze: RuleSnoozeSchedule) {
|
||||
const { duration, rRule, id } = snooze;
|
||||
if (duration === -1)
|
||||
return {
|
||||
id,
|
||||
snoozeEndTime: new Date(MAX_TIMESTAMP),
|
||||
};
|
||||
const startTimeMS = Date.parse(rRule.dtstart);
|
||||
const initialEndTime = startTimeMS + duration;
|
||||
const isInitialStartSkipped = snooze.skipRecurrences?.includes(rRule.dtstart);
|
||||
// If now is during the first occurrence of the snooze
|
||||
const now = Date.now();
|
||||
if (now >= startTimeMS && now < initialEndTime && !isInitialStartSkipped)
|
||||
return {
|
||||
snoozeEndTime: new Date(initialEndTime),
|
||||
lastOccurrence: new Date(rRule.dtstart),
|
||||
id,
|
||||
};
|
||||
|
||||
// Check to see if now is during a recurrence of the snooze
|
||||
try {
|
||||
const rRuleOptions = {
|
||||
...rRule,
|
||||
dtstart: new Date(rRule.dtstart),
|
||||
until: rRule.until ? new Date(rRule.until) : null,
|
||||
byweekday: rRule.byweekday ?? null,
|
||||
wkst: rRule.wkst ? Weekday[rRule.wkst] : null,
|
||||
};
|
||||
|
||||
const recurrenceRule = new RRule(rRuleOptions);
|
||||
const lastOccurrence = recurrenceRule.before(new Date(now));
|
||||
|
||||
if (!lastOccurrence) return null;
|
||||
// Check if the current recurrence has been skipped manually
|
||||
if (snooze.skipRecurrences?.includes(lastOccurrence.toISOString())) return null;
|
||||
const lastOccurrenceEndTime = lastOccurrence.getTime() + duration;
|
||||
if (now < lastOccurrenceEndTime)
|
||||
return { lastOccurrence, snoozeEndTime: new Date(lastOccurrenceEndTime), id };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -5,5 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { isSnoozeActive } from './is_snooze_active';
|
||||
export { getActiveSnoozeIfExist } from './get_active_snooze_if_exist';
|
||||
export { isSnoozeExpired } from './is_snooze_expired';
|
||||
|
|
|
@ -9,11 +9,11 @@ import moment from 'moment';
|
|||
import { Frequency } from '@kbn/rrule';
|
||||
import sinon from 'sinon';
|
||||
import { RRuleRecord } from '../../types';
|
||||
import { isSnoozeActive } from './is_snooze_active';
|
||||
import { getActiveSnoozeIfExist } from './get_active_snooze_if_exist';
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
||||
describe('isSnoozeActive', () => {
|
||||
describe('getActiveSnoozeIfExist', () => {
|
||||
afterAll(() => fakeTimer.restore());
|
||||
|
||||
test('snooze is NOT active byweekday', () => {
|
||||
|
@ -36,7 +36,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-02-24T23:00:00.000Z,
|
||||
|
@ -90,7 +90,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
|
@ -114,7 +114,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-03-03T23:00:00.000Z,
|
||||
|
@ -145,7 +145,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
|
@ -170,7 +170,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "9141dc1f-ed85-4656-91e4-119173105432",
|
||||
"lastOccurrence": 2023-01-01T00:00:00.000Z,
|
||||
|
@ -201,7 +201,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
|
@ -227,7 +227,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
|
@ -248,7 +248,7 @@ describe('isSnoozeActive', () => {
|
|||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
expect(getActiveSnoozeIfExist(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import { RuleSnoozeSchedule } from '../../types';
|
|||
|
||||
const MAX_TIMESTAMP = 8640000000000000;
|
||||
|
||||
export function isSnoozeActive(snooze: RuleSnoozeSchedule) {
|
||||
export function getActiveSnoozeIfExist(snooze: RuleSnoozeSchedule) {
|
||||
const { duration, rRule, id } = snooze;
|
||||
if (duration === -1)
|
||||
return {
|
||||
|
@ -49,7 +49,7 @@ export function isSnoozeActive(snooze: RuleSnoozeSchedule) {
|
|||
if (now < lastOccurrenceEndTime)
|
||||
return { lastOccurrence, snoozeEndTime: new Date(lastOccurrenceEndTime), id };
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to process RRule ${rRule}: ${e}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import { RRule, Weekday } from '@kbn/rrule';
|
||||
import { RuleSnoozeSchedule } from '../../types';
|
||||
import { isSnoozeActive } from './is_snooze_active';
|
||||
import { getActiveSnoozeIfExist } from './get_active_snooze_if_exist';
|
||||
|
||||
export function isSnoozeExpired(snooze: RuleSnoozeSchedule) {
|
||||
if (isSnoozeActive(snooze)) {
|
||||
if (getActiveSnoozeIfExist(snooze)) {
|
||||
return false;
|
||||
}
|
||||
const now = Date.now();
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export function validateTimezone(timezone: string) {
|
||||
if (moment.tz.names().includes(timezone)) {
|
||||
if (moment.tz.zone(timezone) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'string is not a valid timezone: ' + timezone;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import moment from 'moment';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { RulesListNotifyBadge } from './notify_badge';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
|
||||
|
@ -18,13 +17,19 @@ describe('RulesListNotifyBadge', () => {
|
|||
const onRuleChanged = jest.fn();
|
||||
const snoozeRule = jest.fn();
|
||||
const unsnoozeRule = jest.fn();
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('1990-01-01T05:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders an unsnoozed badge', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
|
@ -38,14 +43,11 @@ describe('RulesListNotifyBadge', () => {
|
|||
);
|
||||
|
||||
// Rule without snooze
|
||||
const badge = wrapper.find(EuiButtonIcon);
|
||||
expect(badge.first().props().iconType).toEqual('bell');
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-unsnoozed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a snoozed badge', () => {
|
||||
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
|
@ -58,16 +60,12 @@ describe('RulesListNotifyBadge', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const snoozeBadge = wrapper.find(EuiButton);
|
||||
|
||||
expect(snoozeBadge.first().props().iconType).toEqual('bellSlash');
|
||||
expect(snoozeBadge.text()).toEqual('Feb 1');
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-snoozed')).toBeInTheDocument();
|
||||
expect(screen.getByText('Feb 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an indefinitely snoozed badge', () => {
|
||||
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
|
@ -80,15 +78,11 @@ describe('RulesListNotifyBadge', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const indefiniteSnoozeBadge = wrapper.find(EuiButtonIcon);
|
||||
|
||||
expect(indefiniteSnoozeBadge.first().props().iconType).toEqual('bellSlash');
|
||||
expect(indefiniteSnoozeBadge.text()).toEqual('');
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-snoozedIndefinitely')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow the user to snooze rules', async () => {
|
||||
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
|
||||
const wrapper = mountWithIntl(
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
|
@ -101,31 +95,26 @@ describe('RulesListNotifyBadge', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Open the popover
|
||||
wrapper.find(EuiButtonIcon).first().simulate('click');
|
||||
await user.click(screen.getByTestId('rulesListNotifyBadge-unsnoozed'));
|
||||
await user.click(await screen.findByTestId('linkSnooze1h'));
|
||||
|
||||
// Snooze for 1 hour
|
||||
wrapper.find('button[data-test-subj="linkSnooze1h"]').first().simulate('click');
|
||||
expect(snoozeRule).toHaveBeenCalledWith({
|
||||
duration: 3600000,
|
||||
id: null,
|
||||
rRule: {
|
||||
count: 1,
|
||||
dtstart: '1990-01-01T05:00:00.000Z',
|
||||
tzid: 'America/New_York',
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitFor(() => {
|
||||
expect(snoozeRule).toHaveBeenCalledWith({
|
||||
duration: 3600000,
|
||||
id: null,
|
||||
rRule: {
|
||||
count: 1,
|
||||
dtstart: '1990-01-01T05:00:00.000Z',
|
||||
tzid: 'America/New_York',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(onRuleChanged).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow the user to unsnooze rules', async () => {
|
||||
jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate());
|
||||
const wrapper = mountWithIntl(
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
|
@ -137,16 +126,80 @@ describe('RulesListNotifyBadge', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Open the popover
|
||||
wrapper.find(EuiButtonIcon).first().simulate('click');
|
||||
await user.click(screen.getByTestId('rulesListNotifyBadge-snoozedIndefinitely'));
|
||||
await user.click(await screen.findByTestId('ruleSnoozeCancel'));
|
||||
|
||||
// Unsnooze
|
||||
wrapper.find('[data-test-subj="ruleSnoozeCancel"] button').simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitFor(() => {
|
||||
expect(unsnoozeRule).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
expect(unsnoozeRule).toHaveBeenCalled();
|
||||
it('renders an invalid badge with invalid schedule timezone', () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
isSnoozedUntil: null,
|
||||
muteAll: false,
|
||||
snoozeSchedule: [
|
||||
{ duration: 1, rRule: { dtstart: '1990-01-01T05:00:00.200Z', tzid: 'invalid' } },
|
||||
],
|
||||
}}
|
||||
onRuleChanged={onRuleChanged}
|
||||
snoozeRule={snoozeRule}
|
||||
unsnoozeRule={unsnoozeRule}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-invalidSnooze')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an invalid badge with invalid schedule byweekday', () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
isSnoozedUntil: null,
|
||||
muteAll: false,
|
||||
snoozeSchedule: [
|
||||
{
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '1990-01-01T05:00:00.200Z',
|
||||
tzid: 'America/New_York',
|
||||
byweekday: ['invalid'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
onRuleChanged={onRuleChanged}
|
||||
snoozeRule={snoozeRule}
|
||||
unsnoozeRule={unsnoozeRule}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-invalidSnooze')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should clear an infinitive snooze schedule', async () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
muteAll: true,
|
||||
isSnoozedUntil: moment('1990-02-01').toDate(),
|
||||
}}
|
||||
onRuleChanged={onRuleChanged}
|
||||
snoozeRule={snoozeRule}
|
||||
unsnoozeRule={unsnoozeRule}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('rulesListNotifyBadge-snoozedIndefinitely'));
|
||||
await user.click(screen.getByTestId('ruleSnoozeCancel'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unsnoozeRule).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState, useRef } from 'react';
|
||||
import React, { useCallback, useMemo, useState, useRef, memo } from 'react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -19,8 +19,11 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RRuleParams } from '@kbn/alerting-types';
|
||||
import { Weekday } from '@kbn/rrule/types';
|
||||
import { RRule } from '@kbn/rrule';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { SnoozeSchedule } from '../../../../../types';
|
||||
import { RuleSnoozeSettings, SnoozeSchedule } from '../../../../../types';
|
||||
import { i18nAbbrMonthDayDate, i18nMonthDayDate } from '../../../../lib/i18n_month_day_date';
|
||||
import { SnoozePanel, futureTimeToInterval } from '../rule_snooze';
|
||||
import { getNextRuleSnoozeSchedule, isRuleSnoozed } from './helpers';
|
||||
|
@ -30,6 +33,9 @@ import {
|
|||
SNOOZE_SUCCESS_MESSAGE,
|
||||
UNSNOOZE_SUCCESS_MESSAGE,
|
||||
UNITS_TRANSLATION,
|
||||
INVALID_SNOOZE,
|
||||
INVALID_SNOOZE_TOOLTIP_TITLE,
|
||||
INVALID_SNOOZE_TOOLTIP_CONTENT,
|
||||
} from './translations';
|
||||
import { RulesListNotifyBadgeProps } from './types';
|
||||
|
||||
|
@ -106,6 +112,11 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
return !isSnoozed && Boolean(nextScheduledSnooze);
|
||||
}, [nextScheduledSnooze, isSnoozed]);
|
||||
|
||||
const isSnoozeValid = useMemo(
|
||||
() => isSnoozeScheduleValid(snoozeSettings?.snoozeSchedule),
|
||||
[snoozeSettings?.snoozeSchedule]
|
||||
);
|
||||
|
||||
const formattedSnoozeText = useMemo(() => {
|
||||
if (!isSnoozedUntil) {
|
||||
if (nextScheduledSnooze)
|
||||
|
@ -246,6 +257,24 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
);
|
||||
}, [showOnHover, isLoading, isDisabled, snoozeButtonAriaLabel, isPopoverOpen, openPopover]);
|
||||
|
||||
const onApplyUnsnooze = useCallback(
|
||||
async (scheduleIds?: string[]) => {
|
||||
try {
|
||||
setRequestInFlightLoading(true);
|
||||
closePopover();
|
||||
await unsnoozeRule(scheduleIds);
|
||||
await onRuleChanged();
|
||||
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
|
||||
} catch (e) {
|
||||
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
|
||||
} finally {
|
||||
setRequestInFlightLoading(false);
|
||||
requestAnimationFrame(() => focusTrapButtonRef.current?.focus());
|
||||
}
|
||||
},
|
||||
[closePopover, unsnoozeRule, onRuleChanged, toasts]
|
||||
);
|
||||
|
||||
const indefiniteSnoozeButton = useMemo(() => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
|
@ -264,6 +293,17 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
}, [isLoading, isDisabled, snoozeButtonAriaLabel, openPopover]);
|
||||
|
||||
const button = useMemo(() => {
|
||||
if (!isSnoozeValid) {
|
||||
return (
|
||||
<InvalidSnoozeButton
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => onApplyUnsnooze(getSnoozeScheduleIds(snoozeSettings?.snoozeSchedule))}
|
||||
ref={focusTrapButtonRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isScheduled) {
|
||||
return scheduledSnoozeButton;
|
||||
}
|
||||
|
@ -275,22 +315,36 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
}
|
||||
return unsnoozedButton;
|
||||
}, [
|
||||
isSnoozed,
|
||||
isSnoozeValid,
|
||||
isScheduled,
|
||||
isSnoozedIndefinitely,
|
||||
scheduledSnoozeButton,
|
||||
snoozedButton,
|
||||
indefiniteSnoozeButton,
|
||||
isSnoozed,
|
||||
unsnoozedButton,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
onApplyUnsnooze,
|
||||
snoozeSettings?.snoozeSchedule,
|
||||
scheduledSnoozeButton,
|
||||
indefiniteSnoozeButton,
|
||||
snoozedButton,
|
||||
]);
|
||||
|
||||
const buttonWithToolTip = useMemo(() => {
|
||||
if (!isSnoozeValid) {
|
||||
return (
|
||||
<EuiToolTip title={INVALID_SNOOZE_TOOLTIP_TITLE} content={INVALID_SNOOZE_TOOLTIP_CONTENT}>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
const tooltipContent =
|
||||
typeof disabled === 'string'
|
||||
? disabled
|
||||
: isPopoverOpen || showTooltipInline
|
||||
? undefined
|
||||
: snoozeTimeLeft;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
title={
|
||||
|
@ -308,7 +362,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [disabled, isPopoverOpen, button, showTooltipInline, snoozeTimeLeft]);
|
||||
}, [isSnoozeValid, disabled, isPopoverOpen, showTooltipInline, snoozeTimeLeft, button]);
|
||||
|
||||
const onApplySnooze = useCallback(
|
||||
async (schedule: SnoozeSchedule) => {
|
||||
|
@ -328,24 +382,6 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
[closePopover, snoozeRule, onRuleChanged, toasts]
|
||||
);
|
||||
|
||||
const onApplyUnsnooze = useCallback(
|
||||
async (scheduleIds?: string[]) => {
|
||||
try {
|
||||
setRequestInFlightLoading(true);
|
||||
closePopover();
|
||||
await unsnoozeRule(scheduleIds);
|
||||
await onRuleChanged();
|
||||
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
|
||||
} catch (e) {
|
||||
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
|
||||
} finally {
|
||||
setRequestInFlightLoading(false);
|
||||
requestAnimationFrame(() => focusTrapButtonRef.current?.focus());
|
||||
}
|
||||
},
|
||||
[closePopover, unsnoozeRule, onRuleChanged, toasts]
|
||||
);
|
||||
|
||||
const popover = (
|
||||
<EuiPopover
|
||||
data-test-subj="rulesListNotifyBadge"
|
||||
|
@ -366,6 +402,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
||||
if (showTooltipInline) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
|
@ -378,5 +415,78 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return popover;
|
||||
};
|
||||
|
||||
interface InvalidSnoozeButtonProps {
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
ref?: React.RefObject<HTMLButtonElement>;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const InvalidSnoozeButton: React.FC<InvalidSnoozeButtonProps> = memo(
|
||||
({ isLoading, isDisabled, onClick, ref }) => {
|
||||
return (
|
||||
<EuiButton
|
||||
size="s"
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
data-test-subj="rulesListNotifyBadge-invalidSnooze"
|
||||
aria-label={INVALID_SNOOZE_TOOLTIP_TITLE}
|
||||
minWidth={85}
|
||||
iconType="warning"
|
||||
color="danger"
|
||||
onClick={onClick}
|
||||
buttonRef={ref}
|
||||
>
|
||||
<EuiText size="xs">{INVALID_SNOOZE}</EuiText>
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const isSnoozeScheduleValid = (snoozeSchedule: RuleSnoozeSettings['snoozeSchedule']) => {
|
||||
if (snoozeSchedule == null || snoozeSchedule.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return snoozeSchedule.every(isSnoozeValid);
|
||||
};
|
||||
|
||||
const isSnoozeValid = (snooze: NonNullable<RuleSnoozeSettings['snoozeSchedule']>[number]) => {
|
||||
const { rRule } = snooze;
|
||||
|
||||
const rRuleOptions = {
|
||||
dtstart: rRule.dtstart,
|
||||
tzid: rRule.tzid,
|
||||
freq: rRule.freq,
|
||||
interval: rRule.interval,
|
||||
until: rRule.until,
|
||||
count: rRule.count,
|
||||
byweekday: rRule.byweekday,
|
||||
bymonthday: rRule.bymonthday,
|
||||
bymonth: rRule.bymonth,
|
||||
};
|
||||
|
||||
return isValidateRRule(rRuleOptions);
|
||||
};
|
||||
|
||||
const getSnoozeScheduleIds = (snooze: NonNullable<RuleSnoozeSettings['snoozeSchedule']> = []) => {
|
||||
return snooze.map(({ id }) => id).filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
const isValidateRRule = (rRule: RRuleParams): boolean => {
|
||||
const { dtstart, until, wkst, byweekday, ...rest } = rRule;
|
||||
|
||||
const rRuleOptions = {
|
||||
...rest,
|
||||
dtstart: new Date(rRule.dtstart),
|
||||
until: until ? new Date(until) : null,
|
||||
wkst: wkst ? Weekday[wkst] : null,
|
||||
byweekday: byweekday ?? null,
|
||||
};
|
||||
|
||||
return RRule.isValid(rRuleOptions);
|
||||
};
|
||||
|
|
|
@ -28,6 +28,27 @@ export const SNOOZE_FAILED_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INVALID_SNOOZE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.invalidSnooze',
|
||||
{
|
||||
defaultMessage: 'Invalid',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVALID_SNOOZE_TOOLTIP_CONTENT = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.invalidSnoozeTooltipContent',
|
||||
{
|
||||
defaultMessage: 'Click to remove',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVALID_SNOOZE_TOOLTIP_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.invalidSnoozeTooltipTitle',
|
||||
{
|
||||
defaultMessage: 'Invalid snooze settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPEN_SNOOZE_PANEL_ARIA_LABEL = (name: string) =>
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.openSnoozePanel',
|
||||
|
|
|
@ -72,7 +72,8 @@
|
|||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/observability-alerting-rule-utils",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/cloud-plugin"
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/rrule"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -214,5 +214,51 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider
|
|||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('should return 400 if the timezone is not valid', async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
...createParams,
|
||||
r_rule: { ...createParams.r_rule, tzid: 'invalid' },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the byweekday is not valid', async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
...createParams,
|
||||
r_rule: { ...createParams.r_rule, byweekday: ['invalid'] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonthday is not valid', async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
...createParams,
|
||||
r_rule: { ...createParams.r_rule, bymonthday: [35] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonth is not valid', async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
...createParams,
|
||||
r_rule: { ...createParams.r_rule, bymonth: [14] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -305,5 +305,119 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
|
|||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('should return 400 if the timezone is not valid', async () => {
|
||||
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
|
||||
);
|
||||
|
||||
await supertest
|
||||
.post(
|
||||
`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
r_rule: { ...createParams.r_rule, tzid: 'invalid' },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the byweekday is not valid', async () => {
|
||||
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
|
||||
);
|
||||
|
||||
await supertest
|
||||
.post(
|
||||
`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
r_rule: { ...createParams.r_rule, byweekday: ['invalid'] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonthday is not valid', async () => {
|
||||
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
|
||||
);
|
||||
|
||||
await supertest
|
||||
.post(
|
||||
`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
r_rule: { ...createParams.r_rule, bymonthday: [35] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonth is not valid', async () => {
|
||||
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
|
||||
);
|
||||
|
||||
await supertest
|
||||
.post(
|
||||
`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${
|
||||
createdMaintenanceWindow.id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
r_rule: { ...createParams.r_rule, bymonth: [14] },
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -478,6 +478,38 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should return 400 if the timezone of an action is not valid', async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
actions: [
|
||||
{
|
||||
id: 'test-id',
|
||||
group: 'default',
|
||||
params: {},
|
||||
alerts_filter: {
|
||||
timeframe: {
|
||||
days: [1, 2, 3, 4, 5, 6, 7],
|
||||
timezone: 'invalid',
|
||||
hours: { start: '00:00', end: '01:00' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'[request body.actions.0.alerts_filter.timeframe.timezone]: string is not a valid timezone: invalid',
|
||||
});
|
||||
});
|
||||
|
||||
describe('system actions', () => {
|
||||
const systemAction = {
|
||||
id: 'system-connector-test.system-action',
|
||||
|
|
|
@ -182,6 +182,51 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should return 400 if the timezone of an action is not valid', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestRuleData())
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const updatedData = {
|
||||
name: 'bcd',
|
||||
tags: ['bar'],
|
||||
schedule: { interval: '12s' },
|
||||
throttle: '1m',
|
||||
params: {},
|
||||
actions: [
|
||||
{
|
||||
id: 'test-id',
|
||||
group: 'default',
|
||||
params: {},
|
||||
alerts_filter: {
|
||||
timeframe: {
|
||||
days: [1, 2, 3, 4, 5, 6, 7],
|
||||
timezone: 'invalid',
|
||||
hours: { start: '00:00', end: '01:00' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await supertest
|
||||
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(updatedData);
|
||||
|
||||
expect(response.status).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'[request body.actions.0.alerts_filter.timeframe.timezone]: string is not a valid timezone: invalid',
|
||||
});
|
||||
});
|
||||
|
||||
describe('update rule flapping', () => {
|
||||
afterEach(async () => {
|
||||
await resetRulesSettings(supertest, 'space1');
|
||||
|
|
|
@ -407,6 +407,159 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('should return 400 if the id is not in a valid format', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdRule.id).send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
id: 'invalid key',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body.message).to.eql(
|
||||
`[request body.snooze_schedule.id]: Key must be lower case, a-z, 0-9, '_', and '-' are allowed`
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a uuid as a key', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
await alertUtils
|
||||
.getSnoozeRequest(createdRule.id)
|
||||
.send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
id: 'e58e2340-dba6-454c-8308-b2ca66a7cf7',
|
||||
},
|
||||
})
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it('should return 400 if the timezone is not valid', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdRule.id).send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
rRule: { ...SNOOZE_SCHEDULE.rRule, tzid: 'invalid' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body.message).to.eql(
|
||||
'[request body.snooze_schedule.rRule.tzid]: string is not a valid timezone: invalid'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 400 if the byweekday is not valid', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdRule.id).send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
rRule: { ...SNOOZE_SCHEDULE.rRule, byweekday: ['invalid'] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonthday is not valid', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdRule.id).send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
rRule: { ...SNOOZE_SCHEDULE.rRule, bymonthday: [35] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body.message).to.eql(
|
||||
'[request body.snooze_schedule.rRule.bymonthday.0]: Value must be equal to or lower than [31].'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 400 if the bymonth is not valid', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdRule.id).send({
|
||||
snooze_schedule: {
|
||||
...SNOOZE_SCHEDULE,
|
||||
rRule: { ...SNOOZE_SCHEDULE.rRule, bymonth: [14] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body.message).to.eql(
|
||||
'[request body.snooze_schedule.rRule.bymonth.0]: Value must be equal to or lower than [12].'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function getRuleEvents(id: string, minActions: number = 1) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
DETECTION_ENGINE_RULES_BULK_ACTION,
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
|
@ -69,7 +69,7 @@ export const snoozeRule = (id: string, duration: number): Cypress.Chainable =>
|
|||
body: {
|
||||
snooze_schedule: {
|
||||
duration,
|
||||
rRule: { dtstart: new Date().toISOString(), count: 1, tzid: moment().format('zz') },
|
||||
rRule: { dtstart: new Date().toISOString(), count: 1, tzid: moment.tz.guess() },
|
||||
},
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue