From ecfda990b11f6eb9ec153fd5157fa7e9cd71ca4c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 7 Feb 2025 13:53:00 +0000 Subject: [PATCH 01/32] schedule schema --- .../alerting/common/routes/schedule/index.ts | 12 ++++++ .../common/routes/schedule/schema/latest.ts | 8 ++++ .../common/routes/schedule/schema/v1.ts | 42 +++++++++++++++++++ .../common/routes/schedule/types/latest.ts | 8 ++++ .../common/routes/schedule/types/v1.ts | 10 +++++ .../routes/schedule/validation/index.ts | 16 +++++++ .../validation/validate_end_date/latest.ts | 8 ++++ .../validation/validate_end_date/v1.ts | 13 ++++++ .../validation/validate_every/latest.ts | 8 ++++ .../schedule/validation/validate_every/v1.ts | 27 ++++++++++++ .../validation/validate_on_weekday/latest.ts | 8 ++++ .../validation/validate_on_weekday/v1.test.ts | 29 +++++++++++++ .../validation/validate_on_weekday/v1.ts | 25 +++++++++++ .../validation/validate_start_date/latest.ts | 8 ++++ .../validation/validate_start_date/v1.ts | 12 ++++++ 15 files changed, 234 insertions(+) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts new file mode 100644 index 0000000000000..75938b332394b --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { scheduleRequestSchema } from './schema/latest'; +export type { ScheduleRequest } from './types/latest'; + +export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/v1'; +export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts new file mode 100644 index 0000000000000..8268ab1463940 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -0,0 +1,42 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { + validateStartDateV1, + validateEndDateV1, + validateEveryV1, + validateOnWeekDayV1, +} from '../validation'; + +export const scheduleRequestSchema = schema.object({ + duration: schema.number(), + start: schema.string({ + validate: validateStartDateV1, + }), + recurring: schema.maybe( + schema.object({ + end: schema.maybe(schema.string({ validate: validateEndDateV1 })), + every: schema.maybe(schema.string({ validate: validateEveryV1 })), + onWeekDay: schema.maybe( + schema.arrayOf(schema.string(), { minSize: 1, validate: validateOnWeekDayV1 }) + ), + onMonthDay: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 31 }), { minSize: 1 })), + onMonth: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 12 }), { minSize: 1 })), + occurrences: schema.maybe( + schema.number({ + validate: (occurrences: number) => { + if (!Number.isInteger(occurrences)) { + return 'schedule occurrences must be an integer greater than 0'; + } + }, + min: 1, + }) + ), + }) + ), +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts new file mode 100644 index 0000000000000..a1cc845ac3577 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { TypeOf } from '@kbn/config-schema'; +import { scheduleRequestSchema as scheduleRequestSchemaV1 } from '../schema/v1'; + +export type ScheduleRequest = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts new file mode 100644 index 0000000000000..e4ec32c41911a --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { validateStartDate } from './validate_start_date/latest'; +export { validateEndDate } from './validate_end_date/latest'; +export { validateEvery } from './validate_every/latest'; +export { validateOnWeekDay } from './validate_on_weekday/latest'; + +export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1'; +export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1'; +export { validateOnWeekDay as validateOnWeekDayV1 } from './validate_on_weekday/v1'; +export { validateEvery as validateEveryV1 } from './validate_every/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts new file mode 100644 index 0000000000000..c20fd7d5fc6c5 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts @@ -0,0 +1,13 @@ +/* + * 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 validateEndDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid snooze end date: ${date}`; + if (parsedValue <= Date.now()) return `Invalid snooze end date as it is in the past: ${date}`; + return; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts new file mode 100644 index 0000000000000..e4fc4006fa236 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +const DAILY_REGEX = /^[1-9][0-9]*d$/; +const WEEKLY_REGEX = /^[1-9][0-9]*w$/; +const MONTHLY_REGEX = /^[1-9][0-9]*m$/; +const YEARLY_REGEX = /^[1-9][0-9]*y$/; + +export function validateEvery(every: string) { + if (every.match(DAILY_REGEX)) { + return; + } + if (every.match(WEEKLY_REGEX)) { + return; + } + if (every.match(MONTHLY_REGEX)) { + return; + } + if (every.match(YEARLY_REGEX)) { + return; + } + return 'string is not a valid every of recurring schedule: ' + every; +} diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts new file mode 100644 index 0000000000000..975b55fd07227 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { validateRecurrenceByWeekday } from './v1'; + +describe('validateRecurrenceByWeekday', () => { + it('validates empty array', () => { + expect(validateRecurrenceByWeekday([])).toEqual('rRule byweekday cannot be empty'); + }); + + it('validates properly formed byweekday strings', () => { + const weekdays = ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO']; + + expect(validateRecurrenceByWeekday(weekdays)).toBeUndefined(); + }); + + it('validates improperly formed byweekday strings', () => { + expect(validateRecurrenceByWeekday(['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'])).toEqual( + 'invalid byweekday values in rRule byweekday: FOO,BAR' + ); + }); + + it('validates byweekday strings without recurrence', () => { + expect(validateRecurrenceByWeekday(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'])).toBeUndefined(); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts new file mode 100644 index 0000000000000..e016beb1ca6e6 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts @@ -0,0 +1,25 @@ +/* + * 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 validateOnWeekDay = (array: string[]) => { + if (array.length === 0) { + return 'OnWeekDay cannot be empty'; + } + + const onWeekDayRegex = new RegExp('^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'); + const invalidDays: string[] = []; + + array.forEach((day) => { + if (!onWeekDayRegex.test(day)) { + invalidDays.push(day); + } + }); + + if (invalidDays.length > 0) { + return `Invalid onWeekDay values in recurring schedule: ${invalidDays.join(',')}`; + } +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts new file mode 100644 index 0000000000000..3cbc0da0af1b5 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts @@ -0,0 +1,12 @@ +/* + * 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 validateStartDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + return; +}; From 9e92ec009218a7f9a1d313c5c8763d96a20738d1 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 11 Feb 2025 14:10:47 +0000 Subject: [PATCH 02/32] Add validations and transform --- .../common/routes/schedule/constants.ts | 8 +++ .../common/routes/schedule/schema/index.ts | 9 +++ .../common/routes/schedule/schema/v1.ts | 15 +++-- .../routes/schedule/transforms/index.ts | 9 +++ .../validate_every => transforms}/latest.ts | 0 .../common/routes/schedule/transforms/v1.ts | 55 +++++++++++++++++++ .../common/routes/schedule/types/index.ts | 9 +++ .../routes/schedule/validation/index.ts | 4 +- .../validation/validate_end_date/v1.ts | 7 ++- .../validate_interval_frequency/latest.ts | 8 +++ .../v1.ts | 26 ++++++--- .../validation/validate_start_date/v1.ts | 7 ++- 12 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/{validation/validate_every => transforms}/latest.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/latest.ts rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/{validate_every => validate_interval_frequency}/v1.ts (63%) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts new file mode 100644 index 0000000000000..fa215f89d427a --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts new file mode 100644 index 0000000000000..b4f341dc84914 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { scheduleRequestSchema } from './latest'; +export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 8268ab1463940..4f32ccccc4581 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -9,19 +9,26 @@ import { schema } from '@kbn/config-schema'; import { validateStartDateV1, validateEndDateV1, - validateEveryV1, + validateIntervalAndFrequencyV1, validateOnWeekDayV1, } from '../validation'; export const scheduleRequestSchema = schema.object({ - duration: schema.number(), + duration: schema.number({ + validate: (duration: number) => { + if (!Number.isInteger(duration) || duration === 0) { + return 'Invalid schedule duration. The duration must be either -1 (for indefinite) or a positive integer greater than 0.'; + } + }, + min: -1, + }), start: schema.string({ validate: validateStartDateV1, }), recurring: schema.maybe( schema.object({ end: schema.maybe(schema.string({ validate: validateEndDateV1 })), - every: schema.maybe(schema.string({ validate: validateEveryV1 })), + every: schema.maybe(schema.string({ validate: validateIntervalAndFrequencyV1 })), onWeekDay: schema.maybe( schema.arrayOf(schema.string(), { minSize: 1, validate: validateOnWeekDayV1 }) ), @@ -31,7 +38,7 @@ export const scheduleRequestSchema = schema.object({ schema.number({ validate: (occurrences: number) => { if (!Number.isInteger(occurrences)) { - return 'schedule occurrences must be an integer greater than 0'; + return 'schedule occurrences must be a positive integer'; } }, min: 1, diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts new file mode 100644 index 0000000000000..0a3e8885cdb55 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { transformScheduleToRRule } from './latest'; +export { transformScheduleToRRule as transformScheduleToRRuleV1 } from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts new file mode 100644 index 0000000000000..cd7299be1b14e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts @@ -0,0 +1,55 @@ +/* + * 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-timezone'; +import { Frequency } from '@kbn/rrule'; +import type { RRule } from '../../../../server/application/r_rule/types'; +import { ScheduleRequest } from '../types/v1'; + +const transformFrequency = (frequency: string) => { + switch (frequency) { + case 'y': + return Frequency.YEARLY; + case 'M': + return Frequency.MONTHLY; + case 'w': + return Frequency.WEEKLY; + case 'd': + return Frequency.DAILY; + case 'h': + return Frequency.HOURLY; + case 'm': + return Frequency.MINUTELY; + case 's': + return Frequency.SECONDLY; + default: + throw new Error(`Invalid frequency: ${frequency}`); + } +}; + +export const transformScheduleToRRule: (schedule: ScheduleRequest) => { + rRule: RRule | undefined; +} = (schedule) => { + const { recurring } = schedule ?? {}; + const [interval, frequency] = recurring?.every?.split('') ?? []; + const freq = frequency ? transformFrequency(frequency) : undefined; + const timeZone = moment.tz.guess(); + + return { + rRule: { + byweekday: recurring?.onWeekDay, + bymonthday: recurring?.onMonthDay, + bymonth: recurring?.onMonth, + until: recurring?.end, + count: recurring?.occurrences, + interval: interval ? parseInt(interval, 10) : undefined, + freq, + dtstart: schedule.start, + tzid: `${timeZone}`, + }, + }; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts new file mode 100644 index 0000000000000..6e83da254997d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts @@ -0,0 +1,9 @@ +/* + * 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 type { ScheduleRequest } from './latest'; +export type { ScheduleRequest as ScheduleRequestV1 } from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts index e4ec32c41911a..f3dcd1b952b80 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts @@ -7,10 +7,10 @@ export { validateStartDate } from './validate_start_date/latest'; export { validateEndDate } from './validate_end_date/latest'; -export { validateEvery } from './validate_every/latest'; +export { validateIntervalAndFrequency } from './validate_interval_frequency/latest'; export { validateOnWeekDay } from './validate_on_weekday/latest'; export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1'; export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1'; export { validateOnWeekDay as validateOnWeekDayV1 } from './validate_on_weekday/v1'; -export { validateEvery as validateEveryV1 } from './validate_every/v1'; +export { validateIntervalAndFrequency as validateIntervalAndFrequencyV1 } from './validate_interval_frequency/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts index c20fd7d5fc6c5..7a574c3c13c49 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts @@ -5,9 +5,14 @@ * 2.0. */ +import { ISO_DATE_PATTERN } from '../../constants'; + export const validateEndDate = (date: string) => { const parsedValue = Date.parse(date); if (isNaN(parsedValue)) return `Invalid snooze end date: ${date}`; - if (parsedValue <= Date.now()) return `Invalid snooze end date as it is in the past: ${date}`; + if (parsedValue <= Date.now()) + return `Invalid snooze schedule end date as it is in the past: ${date}`; + if (!ISO_DATE_PATTERN.test(date)) + return `Invalid end date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; return; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts similarity index 63% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts index e4fc4006fa236..a1ae249208550 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_every/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts @@ -5,22 +5,34 @@ * 2.0. */ -const DAILY_REGEX = /^[1-9][0-9]*d$/; -const WEEKLY_REGEX = /^[1-9][0-9]*w$/; -const MONTHLY_REGEX = /^[1-9][0-9]*m$/; const YEARLY_REGEX = /^[1-9][0-9]*y$/; +const MONTHLY_REGEX = /^[1-9][0-9]*M$/; +const WEEKLY_REGEX = /^[1-9][0-9]*w$/; +const DAILY_REGEX = /^[1-9][0-9]*d$/; +const HOURLY_REGEX = /^[1-9][0-9]*h$/; +const MINUTELY_REGEX = /^[1-9][0-9]*m$/; +const SECONDLY_REGEX = /^[1-9][0-9]*s$/; -export function validateEvery(every: string) { - if (every.match(DAILY_REGEX)) { +export function validateIntervalAndFrequency(every: string) { + if (every.match(YEARLY_REGEX)) { + return; + } + if (every.match(MONTHLY_REGEX)) { return; } if (every.match(WEEKLY_REGEX)) { return; } - if (every.match(MONTHLY_REGEX)) { + if (every.match(DAILY_REGEX)) { return; } - if (every.match(YEARLY_REGEX)) { + if (every.match(HOURLY_REGEX)) { + return; + } + if (every.match(MINUTELY_REGEX)) { + return; + } + if (every.match(SECONDLY_REGEX)) { return; } return 'string is not a valid every of recurring schedule: ' + every; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts index 3cbc0da0af1b5..22f775bb07855 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts @@ -5,8 +5,13 @@ * 2.0. */ +import { ISO_DATE_PATTERN } from '../../constants'; + export const validateStartDate = (date: string) => { const parsedValue = Date.parse(date); - if (isNaN(parsedValue)) return `Invalid date: ${date}`; + + if (isNaN(parsedValue)) return `Invalid snooze start date: ${date}`; + if (!ISO_DATE_PATTERN.test(date)) + return `Invalid start date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; return; }; From b0fecec0f51c7b8703357cd821f512266092445f Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 13 Feb 2025 13:54:24 +0000 Subject: [PATCH 03/32] updated validations with regex and added transform for timezone --- .../common/routes/schedule/constants.ts | 5 +- .../common/routes/schedule/schema/v1.ts | 10 +--- .../routes/schedule/transforms/index.ts | 4 +- .../common/routes/schedule/transforms/v1.ts | 46 ++++++++++++------- .../common/routes/schedule/types/v1.ts | 2 +- .../routes/schedule/validation/index.ts | 2 + .../validation/validate_duration/latest.ts | 8 ++++ .../validation/validate_duration/v1.ts | 24 ++++++++++ .../validation/validate_end_date/v1.ts | 15 ++++-- .../validate_interval_frequency/v1.ts | 35 +++----------- .../validation/validate_on_weekday/v1.ts | 4 +- .../validation/validate_start_date/v1.ts | 10 ++-- 12 files changed, 101 insertions(+), 64 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts index fa215f89d427a..a4f60149b088c 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -5,4 +5,7 @@ * 2.0. */ -export const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; +export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}?(Z|[+-]\d{2}:?\d{2})?$/; +export const WEEKDAY_REGEX = '^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'; +export const DURATION_REGEX = /(\d+)(s|m|h)/; +export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|m|y)/; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 4f32ccccc4581..6d390773d1769 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -11,20 +11,14 @@ import { validateEndDateV1, validateIntervalAndFrequencyV1, validateOnWeekDayV1, + validateDurationV1, } from '../validation'; export const scheduleRequestSchema = schema.object({ - duration: schema.number({ - validate: (duration: number) => { - if (!Number.isInteger(duration) || duration === 0) { - return 'Invalid schedule duration. The duration must be either -1 (for indefinite) or a positive integer greater than 0.'; - } - }, - min: -1, - }), start: schema.string({ validate: validateStartDateV1, }), + duration: schema.string({ validate: validateDurationV1 }), recurring: schema.maybe( schema.object({ end: schema.maybe(schema.string({ validate: validateEndDateV1 })), diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts index 0a3e8885cdb55..b3987f636870f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { transformScheduleToRRule } from './latest'; -export { transformScheduleToRRule as transformScheduleToRRuleV1 } from './v1'; +export { transformSchedule } from './latest'; +export { transformSchedule as transformScheduleV1 } from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts index cd7299be1b14e..2697f06409a41 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts @@ -9,37 +9,51 @@ import moment from 'moment-timezone'; import { Frequency } from '@kbn/rrule'; import type { RRule } from '../../../../server/application/r_rule/types'; import { ScheduleRequest } from '../types/v1'; +import { DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../constants'; -const transformFrequency = (frequency: string) => { +const transformFrequency = (frequency?: string) => { switch (frequency) { case 'y': return Frequency.YEARLY; - case 'M': + case 'm': return Frequency.MONTHLY; case 'w': return Frequency.WEEKLY; case 'd': return Frequency.DAILY; - case 'h': - return Frequency.HOURLY; - case 'm': - return Frequency.MINUTELY; - case 's': - return Frequency.SECONDLY; default: - throw new Error(`Invalid frequency: ${frequency}`); + return; } }; -export const transformScheduleToRRule: (schedule: ScheduleRequest) => { +const getDurationMilliseconds = (duration: string): number => { + const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; + + return moment + .duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor) + .asMilliseconds(); +}; + +const getTimezoneForOffset = (offset: number): string[] => { + const now = new Date(); + return moment.tz.names().filter((tz) => moment.tz(now, tz).utcOffset() === offset); +}; + +export const transformSchedule: (schedule: ScheduleRequest) => { + duration: number; rRule: RRule | undefined; } = (schedule) => { - const { recurring } = schedule ?? {}; - const [interval, frequency] = recurring?.every?.split('') ?? []; - const freq = frequency ? transformFrequency(frequency) : undefined; - const timeZone = moment.tz.guess(); + const { recurring, duration, start } = schedule ?? {}; + const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; + const freq = transformFrequency(frequency); + const durationInMilliseconds = duration === '-1' ? -1 : getDurationMilliseconds(duration); + + const browserTimeZone = moment.tz.guess(); + const offset = moment.parseZone(start).format('Z'); + const timeZoneFromStart = getTimezoneForOffset(moment.duration(offset).asMinutes()); return { + duration: durationInMilliseconds, rRule: { byweekday: recurring?.onWeekDay, bymonthday: recurring?.onMonthDay, @@ -48,8 +62,8 @@ export const transformScheduleToRRule: (schedule: ScheduleRequest) => { count: recurring?.occurrences, interval: interval ? parseInt(interval, 10) : undefined, freq, - dtstart: schedule.start, - tzid: `${timeZone}`, + dtstart: start, + tzid: timeZoneFromStart[0] ?? browserTimeZone, }, }; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts index a1cc845ac3577..20a870fbd7fd1 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts @@ -5,6 +5,6 @@ * 2.0. */ import type { TypeOf } from '@kbn/config-schema'; -import { scheduleRequestSchema as scheduleRequestSchemaV1 } from '../schema/v1'; +import { scheduleRequestSchemaV1 } from '..'; export type ScheduleRequest = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts index f3dcd1b952b80..1c93f2838f654 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts @@ -9,8 +9,10 @@ export { validateStartDate } from './validate_start_date/latest'; export { validateEndDate } from './validate_end_date/latest'; export { validateIntervalAndFrequency } from './validate_interval_frequency/latest'; export { validateOnWeekDay } from './validate_on_weekday/latest'; +export { validateDuration } from './validate_duration/latest'; export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1'; export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1'; export { validateOnWeekDay as validateOnWeekDayV1 } from './validate_on_weekday/v1'; export { validateIntervalAndFrequency as validateIntervalAndFrequencyV1 } from './validate_interval_frequency/v1'; +export { validateDuration as validateDurationV1 } from './validate_duration/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts new file mode 100644 index 0000000000000..35991da5f539f --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dateMath from '@kbn/datemath'; +import { DURATION_REGEX } from '../../constants'; + +export const validateDuration = (duration: string) => { + const durationRegexp = new RegExp(DURATION_REGEX, 'g'); + + if (duration !== '-1' && !durationRegexp.test(duration)) { + return `Invalid schedule duration format: ${duration}`; + } + + const date = dateMath.parse(`now-${duration}`); + + if (!date || !date.isValid()) { + return 'Invalid duration.'; + } + return; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts index 7a574c3c13c49..0de6ca889b697 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts @@ -5,14 +5,21 @@ * 2.0. */ -import { ISO_DATE_PATTERN } from '../../constants'; +import { ISO_DATE_REGEX } from '../../constants'; export const validateEndDate = (date: string) => { const parsedValue = Date.parse(date); - if (isNaN(parsedValue)) return `Invalid snooze end date: ${date}`; - if (parsedValue <= Date.now()) + + if (isNaN(parsedValue)) { + return `Invalid snooze end date: ${date}`; + } + if (parsedValue <= Date.now()) { return `Invalid snooze schedule end date as it is in the past: ${date}`; - if (!ISO_DATE_PATTERN.test(date)) + } + + if (!ISO_DATE_REGEX.test(date)) { return `Invalid end date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; + } + return; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts index a1ae249208550..b488ccdd4c333 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts @@ -5,35 +5,14 @@ * 2.0. */ -const YEARLY_REGEX = /^[1-9][0-9]*y$/; -const MONTHLY_REGEX = /^[1-9][0-9]*M$/; -const WEEKLY_REGEX = /^[1-9][0-9]*w$/; -const DAILY_REGEX = /^[1-9][0-9]*d$/; -const HOURLY_REGEX = /^[1-9][0-9]*h$/; -const MINUTELY_REGEX = /^[1-9][0-9]*m$/; -const SECONDLY_REGEX = /^[1-9][0-9]*s$/; +import { INTERVAL_FREQUENCY_REGEXP } from '../../constants'; export function validateIntervalAndFrequency(every: string) { - if (every.match(YEARLY_REGEX)) { - return; - } - if (every.match(MONTHLY_REGEX)) { - return; - } - if (every.match(WEEKLY_REGEX)) { - return; - } - if (every.match(DAILY_REGEX)) { - return; - } - if (every.match(HOURLY_REGEX)) { - return; - } - if (every.match(MINUTELY_REGEX)) { - return; - } - if (every.match(SECONDLY_REGEX)) { - return; + const everyRegexp = new RegExp(INTERVAL_FREQUENCY_REGEXP, 'g'); + + if (!everyRegexp.test(every)) { + return `'every' string of recurring schedule is not valid : ${every}`; } - return 'string is not a valid every of recurring schedule: ' + every; + + return; } diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts index e016beb1ca6e6..dbcad36c80bed 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { WEEKDAY_REGEX } from '../../constants'; + export const validateOnWeekDay = (array: string[]) => { if (array.length === 0) { return 'OnWeekDay cannot be empty'; } - const onWeekDayRegex = new RegExp('^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'); + const onWeekDayRegex = new RegExp(WEEKDAY_REGEX); const invalidDays: string[] = []; array.forEach((day) => { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts index 22f775bb07855..954739b5cca7b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts @@ -5,13 +5,17 @@ * 2.0. */ -import { ISO_DATE_PATTERN } from '../../constants'; +import { ISO_DATE_REGEX } from '../../constants'; export const validateStartDate = (date: string) => { const parsedValue = Date.parse(date); - if (isNaN(parsedValue)) return `Invalid snooze start date: ${date}`; - if (!ISO_DATE_PATTERN.test(date)) + if (isNaN(parsedValue)) { + return `Invalid snooze start date: ${date}`; + } + if (!ISO_DATE_REGEX.test(date)) { return `Invalid start date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; + } + return; }; From 78c3a7d3f3b3d4dfef973a05f4f106a961df0169 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 14 Feb 2025 14:12:19 +0000 Subject: [PATCH 04/32] added public snooze route --- .../snooze/{ => external}/schemas/latest.ts | 0 .../apis/snooze/{ => external}/schemas/v1.ts | 4 +- .../apis/snooze/external/types}/latest.ts | 0 .../rule/apis/snooze/external/types/v1.ts | 11 ++ .../common/routes/rule/apis/snooze/index.ts | 14 +- .../apis/snooze/internal/schemas/latest.ts | 8 + .../rule/apis/snooze/internal/schemas/v1.ts | 17 ++ .../common/routes/schedule/transforms/v1.ts | 4 +- .../validation/validate_duration/v1.ts | 5 +- .../validate_interval_frequency/v1.ts | 6 + .../validation/validate_on_weekday/v1.test.ts | 12 +- .../shared/alerting/server/routes/index.ts | 3 +- .../snooze/external/snooze_rule_route.test.ts | 149 ++++++++++++++++++ .../apis/snooze/external/snooze_rule_route.ts | 80 ++++++++++ .../server/routes/rule/apis/snooze/index.ts | 3 +- .../{ => internal}/snooze_rule_route.test.ts | 10 +- .../{ => internal}/snooze_rule_route.ts | 22 +-- .../snooze/{ => internal}/transforms/index.ts | 1 - .../transform_snooze_body/latest.ts | 8 + .../transforms/transform_snooze_body/v1.ts | 8 +- 20 files changed, 329 insertions(+), 36 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/{ => external}/schemas/latest.ts (100%) rename x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/{ => external}/schemas/v1.ts (79%) rename x-pack/platform/plugins/shared/alerting/{server/routes/rule/apis/snooze/transforms/transform_snooze_body => common/routes/rule/apis/snooze/external/types}/latest.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/{ => internal}/snooze_rule_route.test.ts (91%) rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/{ => internal}/snooze_rule_route.ts (69%) rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/{ => internal}/transforms/index.ts (99%) create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/latest.ts rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/{ => internal}/transforms/transform_snooze_body/v1.ts (54%) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/schemas/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/schemas/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts similarity index 79% rename from x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/schemas/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts index 9b12ab9767512..5cfbf109985a3 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import { ruleSnoozeScheduleSchemaV1 } from '../../../request'; +import { scheduleRequestSchemaV1 } from '../../../../../schedule'; export const snoozeParamsSchema = schema.object({ id: schema.string(), }); export const snoozeBodySchema = schema.object({ - snooze_schedule: ruleSnoozeScheduleSchemaV1, + schedule: scheduleRequestSchemaV1, }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/transform_snooze_body/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/transform_snooze_body/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts new file mode 100644 index 0000000000000..0e5a76e23326d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts @@ -0,0 +1,11 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { snoozeParamsSchemaV1, snoozeBodySchemaV1 } from '../..'; + +export type SnoozeParams = TypeOf; +export type SnoozeBody = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts index cfd7ad0cbce05..05d14c5ec4802 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts @@ -5,9 +5,19 @@ * 2.0. */ -export { snoozeParamsSchema, snoozeBodySchema } from './schemas/latest'; +export { snoozeParamsSchema, snoozeBodySchema } from './external/schemas/latest'; +export { snoozeParamsInternalSchema, snoozeBodyInternalSchema } from './internal/schemas/latest'; +export type { SnoozeParams, SnoozeBody } from './external/types/latest'; export { snoozeParamsSchema as snoozeParamsSchemaV1, snoozeBodySchema as snoozeBodySchemaV1, -} from './schemas/v1'; +} from './external/schemas/v1'; +export { + snoozeParamsInternalSchema as snoozeParamsInternalSchemaV1, + snoozeBodyInternalSchema as snoozeBodyInternalSchemaV1, +} from './internal/schemas/v1'; +export type { + SnoozeParams as SnoozeParamsV1, + SnoozeBody as SnoozeBodyV1, +} from './external/types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/v1.ts new file mode 100644 index 0000000000000..a9df2819712c9 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/internal/schemas/v1.ts @@ -0,0 +1,17 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { ruleSnoozeScheduleSchemaV1 } from '../../../../request'; + +export const snoozeParamsInternalSchema = schema.object({ + id: schema.string(), +}); + +export const snoozeBodyInternalSchema = schema.object({ + snooze_schedule: ruleSnoozeScheduleSchemaV1, +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts index 2697f06409a41..8b061c73f3cb2 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts @@ -48,9 +48,9 @@ export const transformSchedule: (schedule: ScheduleRequest) => { const freq = transformFrequency(frequency); const durationInMilliseconds = duration === '-1' ? -1 : getDurationMilliseconds(duration); - const browserTimeZone = moment.tz.guess(); - const offset = moment.parseZone(start).format('Z'); + const offset = moment.parseZone(start).utcOffset(); const timeZoneFromStart = getTimezoneForOffset(moment.duration(offset).asMinutes()); + const browserTimeZone = moment.tz.guess(); return { duration: durationInMilliseconds, diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts index 35991da5f539f..c8a4239be0bcb 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts @@ -10,8 +10,11 @@ import { DURATION_REGEX } from '../../constants'; export const validateDuration = (duration: string) => { const durationRegexp = new RegExp(DURATION_REGEX, 'g'); + if (duration === '-1') { + return; + } - if (duration !== '-1' && !durationRegexp.test(duration)) { + if (!durationRegexp.test(duration)) { return `Invalid schedule duration format: ${duration}`; } diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts index b488ccdd4c333..122963250f09a 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.ts @@ -5,6 +5,7 @@ * 2.0. */ +import dateMath from '@kbn/datemath'; import { INTERVAL_FREQUENCY_REGEXP } from '../../constants'; export function validateIntervalAndFrequency(every: string) { @@ -14,5 +15,10 @@ export function validateIntervalAndFrequency(every: string) { return `'every' string of recurring schedule is not valid : ${every}`; } + const parsedEvery = dateMath.parse(`now-${every}`); + if (!parsedEvery || !parsedEvery.isValid()) { + return `Invalid 'every' field conversion to date.`; + } + return; } diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts index 975b55fd07227..455ae7ecf160f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts @@ -4,26 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { validateRecurrenceByWeekday } from './v1'; +import { validateOnWeekDay } from './v1'; describe('validateRecurrenceByWeekday', () => { it('validates empty array', () => { - expect(validateRecurrenceByWeekday([])).toEqual('rRule byweekday cannot be empty'); + expect(validateOnWeekDay([])).toEqual('OnWeekDay cannot be empty'); }); it('validates properly formed byweekday strings', () => { const weekdays = ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO']; - expect(validateRecurrenceByWeekday(weekdays)).toBeUndefined(); + expect(validateOnWeekDay(weekdays)).toBeUndefined(); }); it('validates improperly formed byweekday strings', () => { - expect(validateRecurrenceByWeekday(['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'])).toEqual( - 'invalid byweekday values in rRule byweekday: FOO,BAR' + expect(validateOnWeekDay(['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'])).toEqual( + 'Invalid onWeekDay values in recurring schedule: FOO,BAR' ); }); it('validates byweekday strings without recurrence', () => { - expect(validateRecurrenceByWeekday(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'])).toBeUndefined(); + expect(validateOnWeekDay(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'])).toBeUndefined(); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/index.ts index cedd8de9446f6..545f195b09b67 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/index.ts @@ -37,7 +37,7 @@ import { unmuteAllRuleRoute } from './rule/apis/unmute_all'; import { unmuteAlertRoute } from './rule/apis/unmute_alert/unmute_alert_route'; import { updateRuleApiKeyRoute } from './rule/apis/update_api_key/update_rule_api_key_route'; import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route'; -import { snoozeRuleRoute } from './rule/apis/snooze'; +import { snoozeRuleInternalRoute, snoozeRuleRoute } from './rule/apis/snooze'; import { unsnoozeRuleRoute } from './rule/apis/unsnooze'; import { runSoonRoute } from './run_soon'; import { bulkDeleteRulesRoute } from './rule/apis/bulk_delete/bulk_delete_rules_route'; @@ -121,6 +121,7 @@ export function defineRoutes(opts: RouteOptions) { bulkDeleteRulesRoute({ router, licenseState }); bulkEnableRulesRoute({ router, licenseState }); bulkDisableRulesRoute({ router, licenseState }); + snoozeRuleInternalRoute(router, licenseState); snoozeRuleRoute(router, licenseState); unsnoozeRuleRoute(router, licenseState); cloneRuleRoute(router, licenseState); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts new file mode 100644 index 0000000000000..d75f5637b2ca7 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; +import { snoozeRuleRoute } from './snooze_rule_route'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../../../../../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +const schedule = { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + recurring: { + occurrences: 1, + }, +}; + +describe('snoozeAlertRoute', () => { + it('snoozes an alert', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + schedule, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeSchedule": Object { + "duration": 864000000, + "rRule": Object { + "count": 1, + "dtstart": "2021-03-07T00:00:00.000Z", + "tzid": "UTC", + }, + }, + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('also snoozes an alert when passed snoozeEndTime of -1', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + schedule: { + ...schedule, + duration: -1, + }, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeSchedule": Object { + "duration": -1, + "rRule": Object { + "count": 1, + "dtstart": "2021-03-07T00:00:00.000Z", + "tzid": "UTC", + }, + }, + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + rulesClient.snooze.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts new file mode 100644 index 0000000000000..62de2aedf327d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -0,0 +1,80 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { + type SnoozeParams, + snoozeBodySchema, + snoozeParamsSchema, +} from '../../../../../../common/routes/rule/apis/snooze'; +import { ILicenseState, RuleMutedError } from '../../../../../lib'; +import { verifyAccessAndContext } from '../../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; +import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; +import { transformSchedule } from '../../../../../../common/routes/schedule/transforms'; +import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; + +export const snoozeRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}/_snooze`, + security: DEFAULT_ALERTING_ROUTE_SECURITY, + options: { + access: 'public', + summary: 'Snooze a rule', + tags: ['oas-tag:alerting'], + }, + validate: { + request: { + params: snoozeParamsSchema, + body: snoozeBodySchema, + }, + response: { + 204: { + description: 'Indicates a successful call.', + }, + 400: { + description: 'Indicates an invalid schema.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, + }, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertingContext = await context.alerting; + const rulesClient = await alertingContext.getRulesClient(); + const params: SnoozeParams = req.params; + const { rRule, duration } = transformSchedule(req.body.schedule); + + try { + await rulesClient.snooze({ + ...params, + snoozeSchedule: { + duration, + rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], + }, + }); + return res.noContent(); + } catch (e) { + if (e instanceof RuleMutedError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/index.ts index 2554a9040c650..7790ac67f7322 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { snoozeRuleRoute } from './snooze_rule_route'; +export { snoozeRuleRoute as snoozeRuleInternalRoute } from './internal/snooze_rule_route'; +export { snoozeRuleRoute } from './external/snooze_rule_route'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts similarity index 91% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.test.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts index 51d1b9a23cda8..d951b46e2e444 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts @@ -7,13 +7,13 @@ import { snoozeRuleRoute } from './snooze_rule_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../../../lib/license_state.mock'; -import { mockHandlerArguments } from '../../../_mock_handler_arguments'; -import { rulesClientMock } from '../../../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; +import { licenseStateMock } from '../../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; const rulesClient = rulesClientMock.create(); -jest.mock('../../../../lib/license_api_access', () => ({ +jest.mock('../../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts similarity index 69% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts index d7c93e76b7e3e..a40d79f5f067d 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts @@ -8,16 +8,16 @@ import { TypeOf } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; import { - snoozeBodySchema, - snoozeParamsSchema, -} from '../../../../../common/routes/rule/apis/snooze'; -import { ILicenseState, RuleMutedError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; -import { AlertingRequestHandlerContext, INTERNAL_ALERTING_SNOOZE_RULE } from '../../../../types'; + snoozeBodyInternalSchema, + snoozeParamsInternalSchema, +} from '../../../../../../common/routes/rule/apis/snooze'; +import { ILicenseState, RuleMutedError } from '../../../../../lib'; +import { verifyAccessAndContext } from '../../../../lib'; +import { AlertingRequestHandlerContext, INTERNAL_ALERTING_SNOOZE_RULE } from '../../../../../types'; import { transformSnoozeBodyV1 } from './transforms'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../constants'; +import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; -export type SnoozeRuleRequestParamsV1 = TypeOf; +export type SnoozeRuleRequestInternalParamsV1 = TypeOf; export const snoozeRuleRoute = ( router: IRouter, @@ -29,15 +29,15 @@ export const snoozeRuleRoute = ( security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'internal' }, validate: { - params: snoozeParamsSchema, - body: snoozeBodySchema, + params: snoozeParamsInternalSchema, + body: snoozeBodyInternalSchema, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const alertingContext = await context.alerting; const rulesClient = await alertingContext.getRulesClient(); - const params: SnoozeRuleRequestParamsV1 = req.params; + const params: SnoozeRuleRequestInternalParamsV1 = req.params; const body = transformSnoozeBodyV1(req.body); try { await rulesClient.snooze({ ...params, ...body }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts similarity index 99% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/index.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts index fce05888d26fa..fce9858f6aa5c 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts @@ -6,5 +6,4 @@ */ export { transformSnoozeBody } from './transform_snooze_body/latest'; - export { transformSnoozeBody as transformSnoozeBodyV1 } from './transform_snooze_body/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/latest.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/transform_snooze_body/v1.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/v1.ts similarity index 54% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/transform_snooze_body/v1.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/v1.ts index ff7c890ef9cbd..3d98e56d332fd 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/transforms/transform_snooze_body/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/transform_snooze_body/v1.ts @@ -6,12 +6,12 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { snoozeBodySchemaV1 } from '../../../../../../../common/routes/rule/apis/snooze'; +import { snoozeBodyInternalSchemaV1 } from '../../../../../../../../common/routes/rule/apis/snooze'; -type SnoozeBodySchema = TypeOf; +type SnoozeBodyInternalSchema = TypeOf; -export const transformSnoozeBody: (opts: SnoozeBodySchema) => { - snoozeSchedule: SnoozeBodySchema['snooze_schedule']; +export const transformSnoozeBody: (opts: SnoozeBodyInternalSchema) => { + snoozeSchedule: SnoozeBodyInternalSchema['snooze_schedule']; } = ({ snooze_schedule: snoozeSchedule }) => ({ snoozeSchedule, }); From 398e82941219112d61150b810af4922f34fbb3ec Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 18 Feb 2025 14:33:02 +0000 Subject: [PATCH 05/32] api description and unit test --- .../common/routes/schedule/schema/v1.ts | 58 ++++- .../routes/schedule/transforms/v1.test.ts | 198 ++++++++++++++++++ .../validation/validate_duration/v1.test.ts | 41 ++++ .../validation/validate_duration/v1.ts | 2 +- .../validation/validate_end_date/v1.test.ts | 51 +++++ .../validate_interval_frequency/v1.test.ts | 49 +++++ .../validation/validate_on_weekday/v1.test.ts | 14 +- .../validation/validate_start_date/v1.test.ts | 33 +++ .../snooze/external/snooze_rule_route.test.ts | 160 +++++++++++++- 9 files changed, 588 insertions(+), 18 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 6d390773d1769..013edbbde8d6c 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -17,17 +17,60 @@ import { export const scheduleRequestSchema = schema.object({ start: schema.string({ validate: validateStartDateV1, + meta: { + description: 'Start date and time of the snooze schedule in ISO 8601 format.', + }, + }), + duration: schema.string({ + validate: validateDurationV1, + meta: { + description: 'Duration of the snooze schedule.', + }, }), - duration: schema.string({ validate: validateDurationV1 }), recurring: schema.maybe( schema.object({ - end: schema.maybe(schema.string({ validate: validateEndDateV1 })), - every: schema.maybe(schema.string({ validate: validateIntervalAndFrequencyV1 })), + end: schema.maybe( + schema.string({ + validate: validateEndDateV1, + meta: { + description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', + }, + }) + ), + every: schema.maybe( + schema.string({ + validate: validateIntervalAndFrequencyV1, + meta: { + description: 'Recurrence interval and frequency of the snooze schedule.', + }, + }) + ), onWeekDay: schema.maybe( - schema.arrayOf(schema.string(), { minSize: 1, validate: validateOnWeekDayV1 }) + schema.arrayOf(schema.string(), { + minSize: 1, + validate: validateOnWeekDayV1, + meta: { + description: + 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', + }, + }) + ), + onMonthDay: schema.maybe( + schema.arrayOf(schema.number({ min: 1, max: 31 }), { + minSize: 1, + meta: { + description: 'Specific days of the month for recurrence of the snooze schedule.', + }, + }) + ), + onMonth: schema.maybe( + schema.arrayOf(schema.number({ min: 1, max: 12 }), { + minSize: 1, + meta: { + description: 'Specific months for recurrence of the snooze schedule.', + }, + }) ), - onMonthDay: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 31 }), { minSize: 1 })), - onMonth: schema.maybe(schema.arrayOf(schema.number({ min: 1, max: 12 }), { minSize: 1 })), occurrences: schema.maybe( schema.number({ validate: (occurrences: number) => { @@ -36,6 +79,9 @@ export const scheduleRequestSchema = schema.object({ } }, min: 1, + meta: { + description: 'Total number of recurrences of the snooze schedule.', + }, }) ), }) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts new file mode 100644 index 0000000000000..6bc911e74062f --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { transformSchedule } from './v1'; + +describe('transformSchedule', () => { + it('transforms start and duration correctly', () => { + expect(transformSchedule({ duration: '2h', start: '2021-05-10T00:00:00.000Z' })).toEqual({ + duration: 7200000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); + + it('transforms duration as indefinite correctly', () => { + expect(transformSchedule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' })).toEqual({ + duration: -1, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); + + it('transforms start date and tzid correctly', () => { + expect( + transformSchedule({ duration: '1500s', start: '2025-02-10T21:30:00.000+05:30' }) + ).toEqual({ + duration: 1500000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2025-02-10T21:30:00.000+05:30', + freq: undefined, + interval: undefined, + tzid: 'America/New_York', + until: undefined, + }, + }); + }); + + it('transforms start date without timezone correctly', () => { + expect(transformSchedule({ duration: '500s', start: '2025-01-02T00:00:00.000' })).toEqual({ + duration: 500000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2025-01-02T00:00:00.000', + freq: undefined, + interval: undefined, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); + + it('transforms recurring with weekday correctly', () => { + expect( + transformSchedule({ + duration: '30m', + start: '2025-02-17T19:04:46.320Z', + recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, + }) + ).toEqual({ + duration: 1800000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: ['Mo', 'FR'], + count: undefined, + dtstart: '2025-02-17T19:04:46.320Z', + freq: 3, + interval: 1, + tzid: 'Africa/Abidjan', + until: '2025-05-17T05:05:00.000Z', + }, + }); + }); + + it('transforms recurring with month and day correctly', () => { + expect( + transformSchedule({ + duration: '5h', + start: '2025-02-17T19:04:46.320Z', + recurring: { + every: '1m', + end: '2025-12-17T05:05:00.000Z', + onMonthDay: [1, 31], + onMonth: [1, 3, 5], + }, + }) + ).toEqual({ + duration: 18000000, + rRule: { + bymonth: [1, 3, 5], + bymonthday: [1, 31], + byweekday: undefined, + count: undefined, + dtstart: '2025-02-17T19:04:46.320Z', + freq: 1, + interval: 1, + tzid: 'Africa/Abidjan', + until: '2025-12-17T05:05:00.000Z', + }, + }); + }); + + it('transforms recurring with occurrences correctly', () => { + expect( + transformSchedule({ + duration: '300s', + start: '2025-01-14T05:05:00.000Z', + recurring: { every: '2w', occurrences: 3 }, + }) + ).toEqual({ + duration: 300000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: 3, + dtstart: '2025-01-14T05:05:00.000Z', + freq: 2, + interval: 2, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); + + it('transforms duration to 0 when incorrect', () => { + expect( + transformSchedule({ + duration: '1y', + start: '2025-01-14T05:05:00.000Z', + }) + ).toEqual({ + duration: 0, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2025-01-14T05:05:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); + + it('transforms frequency and interval to undefined when incorrect', () => { + expect( + transformSchedule({ + duration: '1m', + start: '2025-01-14T05:05:00.000Z', + recurring: { every: '-1h' }, + }) + ).toEqual({ + duration: 60000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2025-01-14T05:05:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'Africa/Abidjan', + until: undefined, + }, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts new file mode 100644 index 0000000000000..be656f5455b70 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { validateDuration } from './v1'; + +describe('validateDuration', () => { + it('validates duration in hours correctly', () => { + expect(validateDuration('2h')).toBeUndefined(); + }); + + it('validates duration in minutes correctly', () => { + expect(validateDuration('45m')).toBeUndefined(); + }); + + it('validates duration in seconds correctly', () => { + expect(validateDuration('1000s')).toBeUndefined(); + }); + + it('validates indefinite duration correctly', () => { + expect(validateDuration('-1')).toBeUndefined(); + }); + + it('throws error when invalid unit', () => { + expect(validateDuration('1y')).toEqual('Invalid schedule duration format: 1y'); + }); + + it('throws error when parsed as an invalid number', () => { + expect(validateDuration('-1h')).toEqual('Invalid schedule duration.'); + }); + + it('throws error when empty string', () => { + expect(validateDuration('invalid')).toEqual('Invalid schedule duration format: invalid'); + }); + + it('throws error when empty string with spaces', () => { + expect(validateDuration(' ')).toEqual('Invalid schedule duration format: '); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts index c8a4239be0bcb..3d6fa5be35e6f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts @@ -21,7 +21,7 @@ export const validateDuration = (duration: string) => { const date = dateMath.parse(`now-${duration}`); if (!date || !date.isValid()) { - return 'Invalid duration.'; + return 'Invalid schedule duration.'; } return; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts new file mode 100644 index 0000000000000..ff5dfb7a34947 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { validateEndDate } from './v1'; + +describe('validateEndDate', () => { + const mockCurrentDate = new Date('2025-01-01T00:00:00.000Z'); + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers().setSystemTime(mockCurrentDate); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('validates end date correctly', () => { + expect(validateEndDate('2025-02-17T05:05:00.000Z')).toBeUndefined(); + }); + + it('validates end date with time offset correctly', () => { + expect(validateEndDate('2025-02-10T21:30:00.000+05:30')).toBeUndefined(); + }); + + it('validates for a future date without timezone correctly', () => { + const futureDate = '2025-01-02T00:00:00.000'; + expect(validateEndDate(futureDate)).toBeUndefined(); + }); + + it('throws error for invalid date', () => { + expect(validateEndDate('invalid-date')).toEqual('Invalid snooze end date: invalid-date'); + }); + + it('throws error for past date', () => { + const pastDate = '2024-12-31T23:59:59.999Z'; + expect(validateEndDate(pastDate)).toBe( + `Invalid snooze schedule end date as it is in the past: ${pastDate}` + ); + }); + + it('throws error when not ISO format', () => { + expect(validateEndDate('Feb 18 2025')).toEqual( + 'Invalid end date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts new file mode 100644 index 0000000000000..04a0322385285 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { validateIntervalAndFrequency } from './v1'; + +describe('validateIntervalAndFrequency', () => { + it('validates frequency in days correctly', () => { + expect(validateIntervalAndFrequency('2d')).toBeUndefined(); + }); + + it('validates frequency in weeks correctly', () => { + expect(validateIntervalAndFrequency('5w')).toBeUndefined(); + }); + + it('validates frequency in months correctly', () => { + expect(validateIntervalAndFrequency('3m')).toBeUndefined(); + }); + + it('validates frequency in years correctly', () => { + expect(validateIntervalAndFrequency('1y')).toBeUndefined(); + }); + + it('throws error when invalid frequency', () => { + expect(validateIntervalAndFrequency('1h')).toEqual( + `'every' string of recurring schedule is not valid : 1h` + ); + }); + + it('throws error when an invalid interval', () => { + expect(validateIntervalAndFrequency('-1w')).toEqual( + `Invalid 'every' field conversion to date.` + ); + }); + + it('throws error when invalid string', () => { + expect(validateIntervalAndFrequency('invalid')).toEqual( + `'every' string of recurring schedule is not valid : invalid` + ); + }); + + it('throws error when empty string with spaces', () => { + expect(validateIntervalAndFrequency(' ')).toEqual( + `'every' string of recurring schedule is not valid : ` + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts index 455ae7ecf160f..88d0a621c0b26 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts @@ -6,24 +6,30 @@ */ import { validateOnWeekDay } from './v1'; -describe('validateRecurrenceByWeekday', () => { +describe('validateOnWeekDay', () => { it('validates empty array', () => { expect(validateOnWeekDay([])).toEqual('OnWeekDay cannot be empty'); }); - it('validates properly formed byweekday strings', () => { + it('validates properly formed onWeekDay strings', () => { const weekdays = ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO']; expect(validateOnWeekDay(weekdays)).toBeUndefined(); }); - it('validates improperly formed byweekday strings', () => { + it('validates improperly formed onWeekDay strings', () => { expect(validateOnWeekDay(['+1MO', 'FOO', '+3WE', 'BAR', '-4FR'])).toEqual( 'Invalid onWeekDay values in recurring schedule: FOO,BAR' ); }); - it('validates byweekday strings without recurrence', () => { + it('validates invalid week numbers in onWeekDay strings', () => { + expect(validateOnWeekDay(['+5MO', '+3WE', '-7FR'])).toEqual( + 'Invalid onWeekDay values in recurring schedule: +5MO,-7FR' + ); + }); + + it('validates onWeekDay strings without recurrence', () => { expect(validateOnWeekDay(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'])).toBeUndefined(); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts new file mode 100644 index 0000000000000..dc0bbdf780910 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { validateStartDate } from './v1'; + +describe('validateStartDate', () => { + beforeEach(() => jest.clearAllMocks()); + + it('validates end date correctly', () => { + expect(validateStartDate('2025-02-17T05:05:00.000Z')).toBeUndefined(); + }); + + it('validates end date correctly with offset', () => { + expect(validateStartDate('2025-02-10T21:30:00.000+05:30')).toBeUndefined(); + }); + + it('validates for a date without timezone correctly', () => { + expect(validateStartDate('2025-01-02T00:00:00.000')).toBeUndefined(); + }); + + it('throws error for invalid date', () => { + expect(validateStartDate('invalid-date')).toEqual('Invalid snooze start date: invalid-date'); + }); + + it('throws error when not ISO format', () => { + expect(validateStartDate('Feb 18 2025')).toEqual( + 'Invalid start date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index d75f5637b2ca7..59588563246f5 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -65,9 +65,15 @@ describe('snoozeAlertRoute', () => { "snoozeSchedule": Object { "duration": 864000000, "rRule": Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": undefined, "count": 1, "dtstart": "2021-03-07T00:00:00.000Z", - "tzid": "UTC", + "freq": undefined, + "interval": undefined, + "tzid": "Africa/Abidjan", + "until": undefined, }, }, }, @@ -98,7 +104,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { ...schedule, - duration: -1, + duration: '-1', }, }, }, @@ -115,9 +121,148 @@ describe('snoozeAlertRoute', () => { "snoozeSchedule": Object { "duration": -1, "rRule": Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": undefined, "count": 1, "dtstart": "2021-03-07T00:00:00.000Z", - "tzid": "UTC", + "freq": undefined, + "interval": undefined, + "tzid": "Africa/Abidjan", + "until": undefined, + }, + }, + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('snoozes an alert with recurring', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + schedule: { + ...schedule, + recurring: { + every: '1w', + end: '2021-05-10T00:00:00.000Z', + onWeekDay: ['MO'], + }, + }, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeSchedule": Object { + "duration": 864000000, + "rRule": Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": Array [ + "MO", + ], + "count": undefined, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": 2, + "interval": 1, + "tzid": "Africa/Abidjan", + "until": "2021-05-10T00:00:00.000Z", + }, + }, + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('snoozes an alert with occurrences', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + schedule: { + ...schedule, + recurring: { + every: '1y', + occurrences: 5, + onMonthDay: [5, 25], + onMonth: [2, 4, 6, 8, 10, 12], + }, + }, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeSchedule": Object { + "duration": 864000000, + "rRule": Object { + "bymonth": Array [ + 2, + 4, + 6, + 8, + 10, + 12, + ], + "bymonthday": Array [ + 5, + 25, + ], + "byweekday": undefined, + "count": 5, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": 0, + "interval": 1, + "tzid": "Africa/Abidjan", + "until": undefined, }, }, }, @@ -137,10 +282,11 @@ describe('snoozeAlertRoute', () => { rulesClient.snooze.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { params: { id: '1' }, body: { schedule: { duration: '1h' } } }, + ['ok', 'forbidden'] + ); await handler(context, req, res); From c97f6885b95a61db2b10dcb4a9eb789423f2d09d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:47:08 +0000 Subject: [PATCH 06/32] [CI] Auto-commit changed files from 'node scripts/notice' --- x-pack/platform/plugins/shared/alerting/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/alerting/tsconfig.json b/x-pack/platform/plugins/shared/alerting/tsconfig.json index 3375a48fca722..b8ee87c693a69 100644 --- a/x-pack/platform/plugins/shared/alerting/tsconfig.json +++ b/x-pack/platform/plugins/shared/alerting/tsconfig.json @@ -75,7 +75,8 @@ "@kbn/core-saved-objects-base-server-internal", "@kbn/core-security-server-mocks", "@kbn/core-http-server-utils", - "@kbn/response-ops-rule-params" + "@kbn/response-ops-rule-params", + "@kbn/datemath" ], "exclude": [ "target/**/*" From 13f7f1b16bcbf0f9045e7d77300803f05df1e7ce Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:53:23 +0000 Subject: [PATCH 07/32] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 123 ++++++++++++++++++++++++++++++++ oas_docs/bundle.serverless.json | 123 ++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index f89ad4a5c938d..e4036ef5c81ff 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4274,6 +4274,129 @@ ] } }, + "/api/alerting/rule/{id}/_snooze": { + "post": { + "operationId": "post-alerting-rule-id-snooze", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + } + }, + "summary": "Snooze a rule", + "tags": [ + "alerting" + ] + } + }, "/api/alerting/rule/{id}/_unmute_all": { "post": { "operationId": "post-alerting-rule-id-unmute-all", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index b9d0b75240c02..3747c624c7acc 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4274,6 +4274,129 @@ ] } }, + "/api/alerting/rule/{id}/_snooze": { + "post": { + "operationId": "post-alerting-rule-id-snooze", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + } + }, + "summary": "Snooze a rule", + "tags": [ + "alerting" + ] + } + }, "/api/alerting/rule/{id}/_unmute_all": { "post": { "operationId": "post-alerting-rule-id-unmute-all", From 78be27e3f578a1c7533d1c8e2e577d82537a6ac1 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 19 Feb 2025 16:55:57 +0000 Subject: [PATCH 08/32] added timezone and schedule validation --- .../common/routes/schedule/constants.ts | 2 +- .../alerting/common/routes/schedule/index.ts | 2 + .../common/routes/schedule/schema/v1.ts | 161 ++++++++++-------- .../routes/schedule/transforms/v1.test.ts | 39 ++--- .../common/routes/schedule/transforms/v1.ts | 23 ++- .../routes/schedule/validation/index.ts | 4 + .../validation/validate_end_date/v1.test.ts | 15 +- .../validation/validate_start_date/v1.test.ts | 16 +- .../validate_timezone/latest.ts} | 3 +- .../validation/validate_timezone/v1.test.ts | 27 +++ .../validate_timezone/v1.ts} | 10 +- .../validation_schedule/latest.ts} | 3 +- .../validation/validation_schedule/v1.test.ts | 38 +++++ .../validation/validation_schedule/v1.ts | 34 ++++ .../snooze/external/snooze_rule_route.test.ts | 8 +- .../apis/snooze/external/snooze_rule_route.ts | 2 +- 16 files changed, 247 insertions(+), 140 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/{transforms/index.ts => validation/validate_timezone/latest.ts} (69%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/{schema/index.ts => validation/validate_timezone/v1.ts} (53%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/{types/index.ts => validation/validation_schedule/latest.ts} (68%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts index a4f60149b088c..769282176597b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}?(Z|[+-]\d{2}:?\d{2})?$/; +export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; export const WEEKDAY_REGEX = '^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'; export const DURATION_REGEX = /(\d+)(s|m|h)/; export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|m|y)/; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts index 75938b332394b..76d6f848218ca 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts @@ -6,7 +6,9 @@ */ export { scheduleRequestSchema } from './schema/latest'; +export { transformSchedule } from './transforms/latest'; export type { ScheduleRequest } from './types/latest'; export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/v1'; +export { transformSchedule as transformScheduleV1 } from './transforms/v1'; export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 013edbbde8d6c..fa2cd360cac5f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -12,78 +12,93 @@ import { validateIntervalAndFrequencyV1, validateOnWeekDayV1, validateDurationV1, + validateTimezoneV1, + validateScheduleV1, } from '../validation'; -export const scheduleRequestSchema = schema.object({ - start: schema.string({ - validate: validateStartDateV1, - meta: { - description: 'Start date and time of the snooze schedule in ISO 8601 format.', - }, - }), - duration: schema.string({ - validate: validateDurationV1, - meta: { - description: 'Duration of the snooze schedule.', - }, - }), - recurring: schema.maybe( - schema.object({ - end: schema.maybe( - schema.string({ - validate: validateEndDateV1, - meta: { - description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', - }, - }) - ), - every: schema.maybe( - schema.string({ - validate: validateIntervalAndFrequencyV1, - meta: { - description: 'Recurrence interval and frequency of the snooze schedule.', - }, - }) - ), - onWeekDay: schema.maybe( - schema.arrayOf(schema.string(), { - minSize: 1, - validate: validateOnWeekDayV1, - meta: { - description: - 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', - }, - }) - ), - onMonthDay: schema.maybe( - schema.arrayOf(schema.number({ min: 1, max: 31 }), { - minSize: 1, - meta: { - description: 'Specific days of the month for recurrence of the snooze schedule.', - }, - }) - ), - onMonth: schema.maybe( - schema.arrayOf(schema.number({ min: 1, max: 12 }), { - minSize: 1, - meta: { - description: 'Specific months for recurrence of the snooze schedule.', - }, - }) - ), - occurrences: schema.maybe( - schema.number({ - validate: (occurrences: number) => { - if (!Number.isInteger(occurrences)) { - return 'schedule occurrences must be a positive integer'; - } - }, - min: 1, - meta: { - description: 'Total number of recurrences of the snooze schedule.', - }, - }) - ), - }) - ), -}); +export const scheduleRequestSchema = schema.object( + { + start: schema.string({ + validate: validateStartDateV1, + meta: { + description: 'Start date and time of the snooze schedule in ISO 8601 format.', + }, + }), + duration: schema.string({ + validate: validateDurationV1, + meta: { + description: 'Duration of the snooze schedule.', + }, + }), + timezone: schema.maybe( + schema.string({ + validate: validateTimezoneV1, + meta: { + description: 'Timezone of the snooze schedule.', + }, + }) + ), + recurring: schema.maybe( + schema.object({ + end: schema.maybe( + schema.string({ + validate: validateEndDateV1, + meta: { + description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', + }, + }) + ), + every: schema.maybe( + schema.string({ + validate: validateIntervalAndFrequencyV1, + meta: { + description: 'Recurrence interval and frequency of the snooze schedule.', + }, + }) + ), + onWeekDay: schema.maybe( + schema.arrayOf(schema.string(), { + minSize: 1, + validate: validateOnWeekDayV1, + meta: { + description: + 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', + }, + }) + ), + onMonthDay: schema.maybe( + schema.arrayOf(schema.number({ min: 1, max: 31 }), { + minSize: 1, + meta: { + description: 'Specific days of the month for recurrence of the snooze schedule.', + }, + }) + ), + onMonth: schema.maybe( + schema.arrayOf(schema.number({ min: 1, max: 12 }), { + minSize: 1, + meta: { + description: 'Specific months for recurrence of the snooze schedule.', + }, + }) + ), + occurrences: schema.maybe( + schema.number({ + validate: (occurrences: number) => { + if (!Number.isInteger(occurrences)) { + return 'schedule occurrences must be a positive integer'; + } + }, + min: 1, + meta: { + description: 'Total number of recurrences of the snooze schedule.', + }, + }) + ), + }) + ), + }, + { + validate: validateScheduleV1, + } +); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts index 6bc911e74062f..4d302c94bfcc3 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts @@ -18,7 +18,7 @@ describe('transformSchedule', () => { dtstart: '2021-05-10T00:00:00.000Z', freq: undefined, interval: undefined, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: undefined, }, }); @@ -35,7 +35,7 @@ describe('transformSchedule', () => { dtstart: '2021-05-10T00:00:00.000Z', freq: undefined, interval: undefined, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: undefined, }, }); @@ -43,7 +43,11 @@ describe('transformSchedule', () => { it('transforms start date and tzid correctly', () => { expect( - transformSchedule({ duration: '1500s', start: '2025-02-10T21:30:00.000+05:30' }) + transformSchedule({ + duration: '1500s', + start: '2025-02-10T21:30.00.000Z', + timezone: 'America/New_York', + }) ).toEqual({ duration: 1500000, rRule: { @@ -51,7 +55,7 @@ describe('transformSchedule', () => { bymonthday: undefined, byweekday: undefined, count: undefined, - dtstart: '2025-02-10T21:30:00.000+05:30', + dtstart: '2025-02-10T21:30.00.000Z', freq: undefined, interval: undefined, tzid: 'America/New_York', @@ -60,23 +64,6 @@ describe('transformSchedule', () => { }); }); - it('transforms start date without timezone correctly', () => { - expect(transformSchedule({ duration: '500s', start: '2025-01-02T00:00:00.000' })).toEqual({ - duration: 500000, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2025-01-02T00:00:00.000', - freq: undefined, - interval: undefined, - tzid: 'Africa/Abidjan', - until: undefined, - }, - }); - }); - it('transforms recurring with weekday correctly', () => { expect( transformSchedule({ @@ -94,7 +81,7 @@ describe('transformSchedule', () => { dtstart: '2025-02-17T19:04:46.320Z', freq: 3, interval: 1, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: '2025-05-17T05:05:00.000Z', }, }); @@ -122,7 +109,7 @@ describe('transformSchedule', () => { dtstart: '2025-02-17T19:04:46.320Z', freq: 1, interval: 1, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: '2025-12-17T05:05:00.000Z', }, }); @@ -145,7 +132,7 @@ describe('transformSchedule', () => { dtstart: '2025-01-14T05:05:00.000Z', freq: 2, interval: 2, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: undefined, }, }); @@ -167,7 +154,7 @@ describe('transformSchedule', () => { dtstart: '2025-01-14T05:05:00.000Z', freq: undefined, interval: undefined, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: undefined, }, }); @@ -190,7 +177,7 @@ describe('transformSchedule', () => { dtstart: '2025-01-14T05:05:00.000Z', freq: undefined, interval: undefined, - tzid: 'Africa/Abidjan', + tzid: 'UTC', until: undefined, }, }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts index 8b061c73f3cb2..0c21b9fb9e0cc 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts @@ -27,6 +27,10 @@ const transformFrequency = (frequency?: string) => { }; const getDurationMilliseconds = (duration: string): number => { + if (duration === '-1') { + return -1; + } + const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; return moment @@ -34,23 +38,16 @@ const getDurationMilliseconds = (duration: string): number => { .asMilliseconds(); }; -const getTimezoneForOffset = (offset: number): string[] => { - const now = new Date(); - return moment.tz.names().filter((tz) => moment.tz(now, tz).utcOffset() === offset); -}; - export const transformSchedule: (schedule: ScheduleRequest) => { duration: number; rRule: RRule | undefined; } = (schedule) => { - const { recurring, duration, start } = schedule ?? {}; + const { recurring, duration, start, timezone } = schedule ?? {}; + const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; - const freq = transformFrequency(frequency); - const durationInMilliseconds = duration === '-1' ? -1 : getDurationMilliseconds(duration); + const transformedFrequency = transformFrequency(frequency); - const offset = moment.parseZone(start).utcOffset(); - const timeZoneFromStart = getTimezoneForOffset(moment.duration(offset).asMinutes()); - const browserTimeZone = moment.tz.guess(); + const durationInMilliseconds = getDurationMilliseconds(duration); return { duration: durationInMilliseconds, @@ -61,9 +58,9 @@ export const transformSchedule: (schedule: ScheduleRequest) => { until: recurring?.end, count: recurring?.occurrences, interval: interval ? parseInt(interval, 10) : undefined, - freq, + freq: transformedFrequency, dtstart: start, - tzid: timeZoneFromStart[0] ?? browserTimeZone, + tzid: timezone ?? 'UTC', }, }; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts index 1c93f2838f654..b4db9830ad280 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/index.ts @@ -10,9 +10,13 @@ export { validateEndDate } from './validate_end_date/latest'; export { validateIntervalAndFrequency } from './validate_interval_frequency/latest'; export { validateOnWeekDay } from './validate_on_weekday/latest'; export { validateDuration } from './validate_duration/latest'; +export { validateTimezone } from './validate_timezone/latest'; +export { validateSchedule } from './validation_schedule/latest'; export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1'; export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1'; export { validateOnWeekDay as validateOnWeekDayV1 } from './validate_on_weekday/v1'; export { validateIntervalAndFrequency as validateIntervalAndFrequencyV1 } from './validate_interval_frequency/v1'; export { validateDuration as validateDurationV1 } from './validate_duration/v1'; +export { validateTimezone as validateTimezoneV1 } from './validate_timezone/v1'; +export { validateSchedule as validateScheduleV1 } from './validation_schedule/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts index ff5dfb7a34947..d8f82753f6d4b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts @@ -23,15 +23,6 @@ describe('validateEndDate', () => { expect(validateEndDate('2025-02-17T05:05:00.000Z')).toBeUndefined(); }); - it('validates end date with time offset correctly', () => { - expect(validateEndDate('2025-02-10T21:30:00.000+05:30')).toBeUndefined(); - }); - - it('validates for a future date without timezone correctly', () => { - const futureDate = '2025-01-02T00:00:00.000'; - expect(validateEndDate(futureDate)).toBeUndefined(); - }); - it('throws error for invalid date', () => { expect(validateEndDate('invalid-date')).toEqual('Invalid snooze end date: invalid-date'); }); @@ -48,4 +39,10 @@ describe('validateEndDate', () => { 'Invalid end date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' ); }); + + it('throws error when end date with time offset', () => { + expect(validateEndDate('2025-02-10T21:30:00.000+05:30')).toEqual( + 'Invalid end date format: 2025-02-10T21:30:00.000+05:30. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + ); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts index dc0bbdf780910..a129f0fe7c979 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts @@ -9,16 +9,12 @@ import { validateStartDate } from './v1'; describe('validateStartDate', () => { beforeEach(() => jest.clearAllMocks()); - it('validates end date correctly', () => { + it('validates start date correctly', () => { expect(validateStartDate('2025-02-17T05:05:00.000Z')).toBeUndefined(); }); - it('validates end date correctly with offset', () => { - expect(validateStartDate('2025-02-10T21:30:00.000+05:30')).toBeUndefined(); - }); - - it('validates for a date without timezone correctly', () => { - expect(validateStartDate('2025-01-02T00:00:00.000')).toBeUndefined(); + it('validates iso start date correctly', () => { + expect(validateStartDate(new Date().toISOString())).toBeUndefined(); }); it('throws error for invalid date', () => { @@ -30,4 +26,10 @@ describe('validateStartDate', () => { 'Invalid start date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' ); }); + + it('throws error for start date with offset', () => { + expect(validateStartDate('2025-02-10T21:30:00.000+05:30')).toEqual( + 'Invalid start date format: 2025-02-10T21:30:00.000+05:30. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + ); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/latest.ts similarity index 69% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/latest.ts index b3987f636870f..25300c97a6d2e 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/latest.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { transformSchedule } from './latest'; -export { transformSchedule as transformScheduleV1 } from './v1'; +export * from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts new file mode 100644 index 0000000000000..dde7a22355af0 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { validateTimezone } from './v1'; + +describe('validateTimezone', () => { + beforeEach(() => jest.clearAllMocks()); + + it('validates time zone correctly', () => { + expect(validateTimezone('America/New_York')).toBeUndefined(); + }); + + it('validates correctly when no timezone', () => { + expect(validateTimezone()).toBeUndefined(); + }); + + it('throws error for invalid timezone', () => { + expect(validateTimezone('invalid')).toEqual('Invalid snooze timezone: invalid'); + }); + + it('throws error for empty string', () => { + expect(validateTimezone(' ')).toEqual('Invalid snooze timezone: '); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts similarity index 53% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts index b4f341dc84914..0163dcb51ad82 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts @@ -5,5 +5,11 @@ * 2.0. */ -export { scheduleRequestSchema } from './latest'; -export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './v1'; +import moment from 'moment-timezone'; + +export const validateTimezone = (timezone?: string) => { + if (timezone && moment.tz.zone(timezone) == null) { + return `Invalid snooze timezone: ${timezone}`; + } + return; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/latest.ts similarity index 68% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/latest.ts index 6e83da254997d..25300c97a6d2e 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/latest.ts @@ -5,5 +5,4 @@ * 2.0. */ -export type { ScheduleRequest } from './latest'; -export type { ScheduleRequest as ScheduleRequestV1 } from './v1'; +export * from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts new file mode 100644 index 0000000000000..3adbfc28e8e47 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { validateSchedule } from './v1'; + +describe('validateSchedule', () => { + beforeEach(() => jest.clearAllMocks()); + + it('validates duration and every correctly', () => { + expect( + validateSchedule({ + duration: '1h', + recurring: { every: '2w' }, + }) + ).toBeUndefined(); + }); + + it('throws error when duration in minutes and greater than interval', () => { + expect( + validateSchedule({ + duration: '2000m', + recurring: { every: '1d' }, + }) + ).toEqual('Recurrence every 1d must be longer than the snooze duration 2000m'); + }); + + it('throws error when duration in hours greater than interval', () => { + expect( + validateSchedule({ + duration: '700h', + recurring: { every: '1w' }, + }) + ).toEqual(`Recurrence every 1w must be longer than the snooze duration 700h`); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts new file mode 100644 index 0000000000000..f2dd2ae58bb13 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts @@ -0,0 +1,34 @@ +/* + * 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 { DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants'; + +export const validateSchedule = (schedule: { + duration: string; + recurring?: { every?: string }; +}) => { + const { duration, recurring } = schedule; + if (recurring?.every && duration !== '-1') { + const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; + const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; + + const intervalInDays = moment + .duration(interval, frequency as moment.unitOfTime.DurationConstructor) + .asDays(); + + const durationInDays = moment + .duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor) + .asDays(); + + if (intervalInDays && interval && durationInDays >= intervalInDays) { + return `Recurrence every ${recurring?.every} must be longer than the snooze duration ${duration}`; + } + } + + return; +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index 59588563246f5..d7424e3d2e5f4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -72,7 +72,7 @@ describe('snoozeAlertRoute', () => { "dtstart": "2021-03-07T00:00:00.000Z", "freq": undefined, "interval": undefined, - "tzid": "Africa/Abidjan", + "tzid": "UTC", "until": undefined, }, }, @@ -128,7 +128,7 @@ describe('snoozeAlertRoute', () => { "dtstart": "2021-03-07T00:00:00.000Z", "freq": undefined, "interval": undefined, - "tzid": "Africa/Abidjan", + "tzid": "UTC", "until": undefined, }, }, @@ -190,7 +190,7 @@ describe('snoozeAlertRoute', () => { "dtstart": "2021-03-07T00:00:00.000Z", "freq": 2, "interval": 1, - "tzid": "Africa/Abidjan", + "tzid": "UTC", "until": "2021-05-10T00:00:00.000Z", }, }, @@ -261,7 +261,7 @@ describe('snoozeAlertRoute', () => { "dtstart": "2021-03-07T00:00:00.000Z", "freq": 0, "interval": 1, - "tzid": "Africa/Abidjan", + "tzid": "UTC", "until": undefined, }, }, diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 62de2aedf327d..f083d4108a3f0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -15,7 +15,7 @@ import { ILicenseState, RuleMutedError } from '../../../../../lib'; import { verifyAccessAndContext } from '../../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; -import { transformSchedule } from '../../../../../../common/routes/schedule/transforms'; +import { transformSchedule } from '../../../../../../common/routes/schedule'; import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; export const snoozeRuleRoute = ( From 7ad29b056e887d7e6256c7d2eac870ee22ea36f6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:33:34 +0000 Subject: [PATCH 09/32] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 4 ++++ oas_docs/bundle.serverless.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index e4036ef5c81ff..2369fcd7c21a4 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4360,6 +4360,10 @@ "start": { "description": "Start date and time of the snooze schedule in ISO 8601 format.", "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" } }, "required": [ diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 3747c624c7acc..e968125a50eb6 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4360,6 +4360,10 @@ "start": { "description": "Start date and time of the snooze schedule in ISO 8601 format.", "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" } }, "required": [ From d7cee14161f693c984428a9c2960d47447a8fca4 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 21 Feb 2025 12:43:58 +0000 Subject: [PATCH 10/32] add response schema --- .../rule/apis/snooze/external/schemas/v1.ts | 6 +- .../rule/apis/snooze/external/types/v1.ts | 3 +- .../common/routes/rule/apis/snooze/index.ts | 10 +- .../common/routes/schedule/constants.ts | 3 +- .../alerting/common/routes/schedule/index.ts | 13 +- .../schedule/schema/{ => request}/latest.ts | 0 .../schedule/schema/{ => request}/v1.ts | 2 +- .../routes/schedule/schema/response/latest.ts | 8 + .../routes/schedule/schema/response/v1.ts | 84 +++++ .../routes/schedule/transforms/v1.test.ts | 78 ++--- .../common/routes/schedule/transforms/v1.ts | 85 ++++- .../validate_interval_frequency/v1.test.ts | 8 +- .../validation/validation_schedule/v1.ts | 4 + .../snooze/external/snooze_rule.test.ts | 82 +++++ .../methods/snooze/external/snooze_rule.ts | 133 ++++++++ .../rule/methods/snooze/external/types.ts | 17 + .../application/rule/methods/snooze/index.ts | 6 +- .../snooze/{ => internal}/schemas/index.ts | 0 .../schemas/snooze_rule_body_schema.ts | 2 +- .../schemas/snooze_rule_params_schema.ts | 0 .../snooze/{ => internal}/snooze_rule.test.ts | 2 +- .../snooze/{ => internal}/snooze_rule.ts | 24 +- .../snooze/{ => internal}/types/index.ts | 0 .../types/snooze_rule_options.ts | 2 +- .../snooze/external/snooze_rule_route.test.ts | 297 +++++++++++------- .../apis/snooze/external/snooze_rule_route.ts | 34 +- .../snooze/internal/snooze_rule_route.test.ts | 16 +- .../apis/snooze/internal/snooze_rule_route.ts | 2 +- .../alerting/server/rules_client.mock.ts | 1 + .../rules_client/common/snooze_utils.ts | 8 +- .../server/rules_client/rules_client.ts | 10 +- 31 files changed, 733 insertions(+), 207 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/{ => request}/latest.ts (100%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/{ => request}/v1.ts (99%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/schemas/index.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/schemas/snooze_rule_body_schema.ts (88%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/schemas/snooze_rule_params_schema.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/snooze_rule.test.ts (97%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/snooze_rule.ts (77%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/types/index.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{ => internal}/types/snooze_rule_options.ts (88%) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts index 5cfbf109985a3..7b6a41492e5e4 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { scheduleRequestSchemaV1 } from '../../../../../schedule'; +import { scheduleRequestSchemaV1, scheduleResponseSchemaV1 } from '../../../../../schedule'; export const snoozeParamsSchema = schema.object({ id: schema.string(), @@ -15,3 +15,7 @@ export const snoozeParamsSchema = schema.object({ export const snoozeBodySchema = schema.object({ schedule: scheduleRequestSchemaV1, }); + +export const snoozeResponseSchema = schema.object({ + body: scheduleResponseSchemaV1, +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts index 0e5a76e23326d..6b0abd5f99cfa 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts @@ -5,7 +5,8 @@ * 2.0. */ import { TypeOf } from '@kbn/config-schema'; -import { snoozeParamsSchemaV1, snoozeBodySchemaV1 } from '../..'; +import { snoozeParamsSchemaV1, snoozeBodySchemaV1, snoozeResponseSchemaV1 } from '../..'; export type SnoozeParams = TypeOf; export type SnoozeBody = TypeOf; +export type SnoozeResponse = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts index 05d14c5ec4802..b60b35d7a3725 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/index.ts @@ -5,13 +5,18 @@ * 2.0. */ -export { snoozeParamsSchema, snoozeBodySchema } from './external/schemas/latest'; +export { + snoozeParamsSchema, + snoozeBodySchema, + snoozeResponseSchema, +} from './external/schemas/latest'; export { snoozeParamsInternalSchema, snoozeBodyInternalSchema } from './internal/schemas/latest'; -export type { SnoozeParams, SnoozeBody } from './external/types/latest'; +export type { SnoozeParams, SnoozeBody, SnoozeResponse } from './external/types/latest'; export { snoozeParamsSchema as snoozeParamsSchemaV1, snoozeBodySchema as snoozeBodySchemaV1, + snoozeResponseSchema as snoozeResponseSchemaV1, } from './external/schemas/v1'; export { snoozeParamsInternalSchema as snoozeParamsInternalSchemaV1, @@ -20,4 +25,5 @@ export { export type { SnoozeParams as SnoozeParamsV1, SnoozeBody as SnoozeBodyV1, + SnoozeResponse as SnoozeResponseV1, } from './external/types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts index 769282176597b..a94e6f3133eb4 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -8,4 +8,5 @@ export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; export const WEEKDAY_REGEX = '^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'; export const DURATION_REGEX = /(\d+)(s|m|h)/; -export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|m|y)/; +export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|M|y)/; +export const DEFAULT_TIMEZONE = 'UTC'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts index 76d6f848218ca..c4bdd351b4fcd 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts @@ -5,10 +5,15 @@ * 2.0. */ -export { scheduleRequestSchema } from './schema/latest'; -export { transformSchedule } from './transforms/latest'; +export { scheduleRequestSchema } from './schema/request/latest'; +export { scheduleResponseSchema } from './schema/response/latest'; +export { transformScheduleToRRule, transformRRuleToSchedule } from './transforms/latest'; export type { ScheduleRequest } from './types/latest'; -export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/v1'; -export { transformSchedule as transformScheduleV1 } from './transforms/v1'; +export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/request/v1'; +export { scheduleResponseSchema as scheduleResponseSchemaV1 } from './schema/response/v1'; +export { + transformScheduleToRRule as transformScheduleToRRuleV1, + transformRRuleToSchedule as transformRRuleToScheduleV1, +} from './transforms/v1'; export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts similarity index 99% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts index fa2cd360cac5f..473434e82092c 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts @@ -14,7 +14,7 @@ import { validateDurationV1, validateTimezoneV1, validateScheduleV1, -} from '../validation'; +} from '../../validation'; export const scheduleRequestSchema = schema.object( { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts @@ -0,0 +1,8 @@ +/* + * 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'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts new file mode 100644 index 0000000000000..a2333e1ad6ccf --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts @@ -0,0 +1,84 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const scheduleResponseSchema = schema.arrayOf( + schema.object({ + id: schema.maybe( + schema.string({ + meta: { + description: 'Identifier of the snooze schedule.', + }, + }) + ), + start: schema.string({ + meta: { + description: 'Start date and time of the snooze schedule in ISO 8601 format.', + }, + }), + duration: schema.string({ + meta: { + description: 'Duration of the snooze schedule.', + }, + }), + timezone: schema.maybe( + schema.string({ + meta: { + description: 'Timezone of the snooze schedule.', + }, + }) + ), + recurring: schema.maybe( + schema.object({ + end: schema.maybe( + schema.string({ + meta: { + description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', + }, + }) + ), + every: schema.maybe( + schema.string({ + meta: { + description: 'Recurrence interval and frequency of the snooze schedule.', + }, + }) + ), + onWeekDay: schema.maybe( + schema.arrayOf(schema.string(), { + meta: { + description: + 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', + }, + }) + ), + onMonthDay: schema.maybe( + schema.arrayOf(schema.number(), { + meta: { + description: 'Specific days of the month for recurrence of the snooze schedule.', + }, + }) + ), + onMonth: schema.maybe( + schema.arrayOf(schema.number(), { + meta: { + description: 'Specific months for recurrence of the snooze schedule.', + }, + }) + ), + occurrences: schema.maybe( + schema.number({ + meta: { + description: 'Total number of recurrences of the snooze schedule.', + }, + }) + ), + }) + ), + }) +); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts index 4d302c94bfcc3..7e0af8c8725a6 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts @@ -4,46 +4,50 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { transformSchedule } from './v1'; +import { transformScheduleToRRule } from './v1'; -describe('transformSchedule', () => { +describe('transformScheduleToRRule', () => { it('transforms start and duration correctly', () => { - expect(transformSchedule({ duration: '2h', start: '2021-05-10T00:00:00.000Z' })).toEqual({ - duration: 7200000, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2021-05-10T00:00:00.000Z', - freq: undefined, - interval: undefined, - tzid: 'UTC', - until: undefined, - }, - }); + expect(transformScheduleToRRule({ duration: '2h', start: '2021-05-10T00:00:00.000Z' })).toEqual( + { + duration: 7200000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'UTC', + until: undefined, + }, + } + ); }); it('transforms duration as indefinite correctly', () => { - expect(transformSchedule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' })).toEqual({ - duration: -1, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2021-05-10T00:00:00.000Z', - freq: undefined, - interval: undefined, - tzid: 'UTC', - until: undefined, - }, - }); + expect(transformScheduleToRRule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' })).toEqual( + { + duration: -1, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'UTC', + until: undefined, + }, + } + ); }); it('transforms start date and tzid correctly', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '1500s', start: '2025-02-10T21:30.00.000Z', timezone: 'America/New_York', @@ -66,7 +70,7 @@ describe('transformSchedule', () => { it('transforms recurring with weekday correctly', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '30m', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, @@ -89,11 +93,11 @@ describe('transformSchedule', () => { it('transforms recurring with month and day correctly', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '5h', start: '2025-02-17T19:04:46.320Z', recurring: { - every: '1m', + every: '1M', end: '2025-12-17T05:05:00.000Z', onMonthDay: [1, 31], onMonth: [1, 3, 5], @@ -117,7 +121,7 @@ describe('transformSchedule', () => { it('transforms recurring with occurrences correctly', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '300s', start: '2025-01-14T05:05:00.000Z', recurring: { every: '2w', occurrences: 3 }, @@ -140,7 +144,7 @@ describe('transformSchedule', () => { it('transforms duration to 0 when incorrect', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '1y', start: '2025-01-14T05:05:00.000Z', }) @@ -162,7 +166,7 @@ describe('transformSchedule', () => { it('transforms frequency and interval to undefined when incorrect', () => { expect( - transformSchedule({ + transformScheduleToRRule({ duration: '1m', start: '2025-01-14T05:05:00.000Z', recurring: { every: '-1h' }, diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts index 0c21b9fb9e0cc..2de2800276223 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts @@ -6,16 +6,18 @@ */ import moment from 'moment-timezone'; +import { isEmpty, isUndefined, omitBy } from 'lodash'; import { Frequency } from '@kbn/rrule'; +import { RuleSnooze } from '@kbn/alerting-types'; import type { RRule } from '../../../../server/application/r_rule/types'; import { ScheduleRequest } from '../types/v1'; -import { DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../constants'; +import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../constants'; -const transformFrequency = (frequency?: string) => { +const transformEveryToFrequency = (frequency?: string) => { switch (frequency) { case 'y': return Frequency.YEARLY; - case 'm': + case 'M': return Frequency.MONTHLY; case 'w': return Frequency.WEEKLY; @@ -26,7 +28,22 @@ const transformFrequency = (frequency?: string) => { } }; -const getDurationMilliseconds = (duration: string): number => { +const transformFrequencyToEvery = (frequency: Frequency) => { + switch (frequency) { + case Frequency.YEARLY: + return 'y'; + case Frequency.MONTHLY: + return 'M'; + case Frequency.WEEKLY: + return 'w'; + case Frequency.DAILY: + return 'd'; + default: + return; + } +}; + +const getDurationInMilliseconds = (duration: string): number => { if (duration === '-1') { return -1; } @@ -38,16 +55,33 @@ const getDurationMilliseconds = (duration: string): number => { .asMilliseconds(); }; -export const transformSchedule: (schedule: ScheduleRequest) => { +const getDurationInString = (duration: number): string => { + if (duration === -1) { + return '-1'; + } + + const durationInHours = moment.duration(duration, 'milliseconds').asHours(); + if (durationInHours > 1) { + return `${durationInHours}h`; + } + + const durationInSeconds = moment.duration(duration, 'milliseconds').asSeconds(); + if (durationInSeconds % 60 === 0) { + return `${durationInSeconds / 60}m`; + } + return `${durationInSeconds}s`; +}; + +export const transformScheduleToRRule: (schedule: ScheduleRequest) => { duration: number; rRule: RRule | undefined; } = (schedule) => { const { recurring, duration, start, timezone } = schedule ?? {}; const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; - const transformedFrequency = transformFrequency(frequency); + const transformedFrequency = transformEveryToFrequency(frequency); - const durationInMilliseconds = getDurationMilliseconds(duration); + const durationInMilliseconds = getDurationInMilliseconds(duration); return { duration: durationInMilliseconds, @@ -60,7 +94,42 @@ export const transformSchedule: (schedule: ScheduleRequest) => { interval: interval ? parseInt(interval, 10) : undefined, freq: transformedFrequency, dtstart: start, - tzid: timezone ?? 'UTC', + tzid: timezone ?? DEFAULT_TIMEZONE, }, }; }; + +export const transformRRuleToSchedule: ( + snoozeSchedule: RuleSnooze | undefined +) => Array = (snoozeSchedule) => { + if (!snoozeSchedule) { + return []; + } + + const result = snoozeSchedule.map((schedule) => { + const { rRule, duration, id } = schedule; + const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); + const transformedDuration = getDurationInString(duration); + + const recurring = { + end: rRule.until ? new Date(rRule.until).toISOString() : undefined, + every: rRule.interval ? `${rRule.interval}${transformedFrequency}` : undefined, + onWeekDay: rRule.byweekday === null ? undefined : (rRule.byweekday as string[]), + onMonthDay: rRule.bymonthday === null ? undefined : rRule.bymonthday, + onMonth: rRule.bymonth === null ? undefined : rRule.bymonth, + occurrences: rRule.count, + }; + + const filteredRecurring = omitBy(recurring, isUndefined); + + return { + id, + duration: transformedDuration, + start: new Date(rRule.dtstart).toISOString(), + timezone: rRule.tzid, + ...(isEmpty(filteredRecurring) ? {} : { recurring: filteredRecurring }), + }; + }); + + return result; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts index 04a0322385285..4e77702ec1690 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_interval_frequency/v1.test.ts @@ -16,7 +16,7 @@ describe('validateIntervalAndFrequency', () => { }); it('validates frequency in months correctly', () => { - expect(validateIntervalAndFrequency('3m')).toBeUndefined(); + expect(validateIntervalAndFrequency('3M')).toBeUndefined(); }); it('validates frequency in years correctly', () => { @@ -29,6 +29,12 @@ describe('validateIntervalAndFrequency', () => { ); }); + it('throws error when invalid frequency in minutes', () => { + expect(validateIntervalAndFrequency('5m')).toEqual( + `'every' string of recurring schedule is not valid : 5m` + ); + }); + it('throws error when an invalid interval', () => { expect(validateIntervalAndFrequency('-1w')).toEqual( `Invalid 'every' field conversion to date.` diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts index f2dd2ae58bb13..ff02f3801344b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts @@ -30,5 +30,9 @@ export const validateSchedule = (schedule: { } } + if (duration === '-1' && recurring) { + return `The duration of -1 snoozes the rule indefinitely. Recurring schedules cannot be set when the duration is -1.`; + } + return; }; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts new file mode 100644 index 0000000000000..792680f3e3611 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts @@ -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 type { SnoozeRule } 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?: SnoozeRule['snoozeSchedule'] +): SnoozeRule['snoozeSchedule'] => { + return { + id: '123', + duration: 28800000, + rRule: { + dtstart: '2010-09-19T11:49:59.329Z', + count: 1, + tzid: 'UTC', + }, + ...override, + }; +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts new file mode 100644 index 0000000000000..f10dee9a4413d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts @@ -0,0 +1,133 @@ +/* + * 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 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'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../../authorization'; +import { retryIfConflicts } from '../../../../../lib/retry_if_conflicts'; +import { validateSnoozeStartDate } from '../../../../../lib/validate_snooze_date'; +import { RuleMutedError } from '../../../../../lib/errors/rule_muted'; +import { RulesClientContext } from '../../../../../rules_client/types'; +import { RawRule, SanitizedRule } from '../../../../../types'; +import { + getSnoozeAttributes, + verifySnoozeAttributeScheduleLimit, +} from '../../../../../rules_client/common'; +import { updateRuleSo } from '../../../../../data/rule'; +import { updateMetaAttributes } from '../../../../../rules_client/lib/update_meta_attributes'; +import { RuleParams } from '../../../types'; +import { + transformRuleDomainToRule, + transformRuleAttributesToRuleDomain, +} from '../../../transforms'; +import { snoozeParamsSchema } from '../../../../../../common/routes/rule/apis/snooze'; +import type { SnoozeRule } from './types'; + +export async function snoozeRule( + context: RulesClientContext, + { id, snoozeSchedule }: SnoozeRule +): Promise> { + try { + snoozeParamsSchema.validate({ id }); + ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule }); + } catch (error) { + throw Boom.badRequest(`Error validating snooze - ${error.message}`); + } + const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart); + if (snoozeDateValidationMsg) { + throw new RuleMutedError(snoozeDateValidationMsg); + } + + return await retryIfConflicts( + context.logger, + `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, + async () => await snoozeWithOCC(context, { id, snoozeSchedule }) + ); +} + +async function snoozeWithOCC( + context: RulesClientContext, + { id, snoozeSchedule }: SnoozeRule +): Promise> { + const { attributes, version } = await withSpan( + { name: 'getRuleSavedObject', type: 'rules' }, + () => + getRuleSavedObject(context, { + ruleId: id, + }) + ); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Snooze, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' }); + } + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + savedObject: { type: RULE_SAVED_OBJECT_TYPE, id, name: attributes.name }, + error, + }) + ); + throw error; + } + + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + outcome: 'unknown', + savedObject: { type: RULE_SAVED_OBJECT_TYPE, id, name: attributes.name }, + }) + ); + + context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); + + try { + verifySnoozeAttributeScheduleLimit(newAttrs); + } catch (error) { + throw Boom.badRequest(error.message); + } + + const updatedRuleRaw = await updateRuleSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsUpdateOptions: { version }, + id, + updateRuleAttributes: updateMetaAttributes(context, { + ...newAttrs, + updatedBy: await context.getUserName(), + updatedAt: new Date().toISOString(), + }), + }); + + const ruleDomain = transformRuleAttributesToRuleDomain( + updatedRuleRaw.attributes as RawRule, + { + id: updatedRuleRaw.id, + logger: context.logger, + ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId!), + references: updatedRuleRaw.references, + }, + context.isSystemAction + ); + + const rule = transformRuleDomainToRule(ruleDomain); + + return rule as SanitizedRule; +} diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts new file mode 100644 index 0000000000000..6a65f65b4a5e0 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts @@ -0,0 +1,17 @@ +/* + * 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 type { RRule } from '../../../../r_rule/types'; + +export interface SnoozeRule { + id: string; + snoozeSchedule: { + id: string; + duration: number; + rRule: RRule; + }; +} diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts index 842e092d40d57..242a5cacf0b82 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts @@ -5,5 +5,7 @@ * 2.0. */ -export type { SnoozeRuleOptions } from './types'; -export { snoozeRule } from './snooze_rule'; +export type { SnoozeRuleOptions } from './internal/types'; +export type { SnoozeRule } from './external/types'; +export { snoozeRule as snoozeRuleInternal } from './internal/snooze_rule'; +export { snoozeRule } from './external/snooze_rule'; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/index.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/index.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts similarity index 88% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts index 180e69fb33ad1..7b7b85dfa4fe8 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../common/routes/rule/request'; +import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../../common/routes/rule/request'; export const snoozeRuleBodySchema = schema.object({ snoozeSchedule: ruleSnoozeScheduleRequestSchema, diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_params_schema.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_params_schema.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_params_schema.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_params_schema.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts similarity index 97% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts index 9e86aeab24fc4..29e56a7782a40 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RulesClientContext } from '../../../../rules_client'; +import { RulesClientContext } from '../../../../../rules_client'; import { snoozeRule } from './snooze_rule'; import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks'; import { SnoozeRuleOptions } from './types'; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts similarity index 77% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts index 7d8634e4be006..44a461bdccb2e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts @@ -7,21 +7,21 @@ 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'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; -import { retryIfConflicts } from '../../../../lib/retry_if_conflicts'; -import { validateSnoozeStartDate } from '../../../../lib/validate_snooze_date'; -import { RuleMutedError } from '../../../../lib/errors/rule_muted'; -import { RulesClientContext } from '../../../../rules_client/types'; +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'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../../authorization'; +import { retryIfConflicts } from '../../../../../lib/retry_if_conflicts'; +import { validateSnoozeStartDate } from '../../../../../lib/validate_snooze_date'; +import { RuleMutedError } from '../../../../../lib/errors/rule_muted'; +import { RulesClientContext } from '../../../../../rules_client/types'; import { getSnoozeAttributes, verifySnoozeAttributeScheduleLimit, -} from '../../../../rules_client/common'; -import { updateRuleSo } from '../../../../data/rule'; -import { updateMetaAttributes } from '../../../../rules_client/lib/update_meta_attributes'; +} from '../../../../../rules_client/common'; +import { updateRuleSo } from '../../../../../data/rule'; +import { updateMetaAttributes } from '../../../../../rules_client/lib/update_meta_attributes'; import { snoozeRuleParamsSchema } from './schemas'; import type { SnoozeRuleOptions } from './types'; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/index.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/index.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts similarity index 88% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts index 77d077b6ee0e5..3d0c506bc157e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts @@ -5,7 +5,7 @@ * 2.0. */ import { TypeOf } from '@kbn/config-schema'; -import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../common/routes/rule/request'; +import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../../common/routes/rule/request'; export interface SnoozeRuleOptions { id: string; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index d7424e3d2e5f4..da68e2dd9d220 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -11,6 +11,7 @@ import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; import { snoozeRuleRoute } from './snooze_rule_route'; +import { SanitizedRule } from '@kbn/alerting-types'; const rulesClient = rulesClientMock.create(); jest.mock('../../../../../lib/license_api_access', () => ({ @@ -29,6 +30,38 @@ const schedule = { }, }; +const mockedRule = { + apiKeyOwner: 'api-key-owner', + consumer: 'bar', + createdBy: 'elastic', + updatedBy: 'elastic', + enabled: true, + id: '1', + name: 'abc', + alertTypeId: '1', + tags: ['foo'], + throttle: '10m', + schedule: { interval: '12s' }, + params: { + otherField: false, + }, + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + snoozeSchedule: [ + { + duration: 864000000, + id: 'random-schedule-id', + rRule: { + count: 1, + tzid: 'UTC', + dtstart: '2021-03-07T00:00:00.000Z', + }, + }, + ], +}; + +rulesClient.update.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule); + describe('snoozeAlertRoute', () => { it('snoozes an alert', async () => { const licenseState = licenseStateMock.create(); @@ -38,9 +71,9 @@ describe('snoozeAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`); - rulesClient.snooze.mockResolvedValueOnce(); + rulesClient.snooze.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -58,29 +91,26 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "snoozeSchedule": Object { - "duration": 864000000, - "rRule": Object { - "bymonth": undefined, - "bymonthday": undefined, - "byweekday": undefined, - "count": 1, - "dtstart": "2021-03-07T00:00:00.000Z", - "freq": undefined, - "interval": undefined, - "tzid": "UTC", - "until": undefined, - }, - }, - }, - ] + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.duration).toMatchInlineSnapshot( + `864000000` + ); + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` + Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": undefined, + "count": 1, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": undefined, + "interval": undefined, + "tzid": "UTC", + "until": undefined, + } `); - expect(res.noContent).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: [{ ...schedule, id: 'random-schedule-id', timezone: 'UTC' }], + }); }); it('also snoozes an alert when passed snoozeEndTime of -1', async () => { @@ -91,9 +121,22 @@ describe('snoozeAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); - - rulesClient.snooze.mockResolvedValueOnce(); + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`); + + rulesClient.snooze.mockResolvedValueOnce({ + ...mockedRule, + snoozeSchedule: [ + { + duration: -1, + id: 'random-schedule-id', + rRule: { + count: 1, + tzid: 'UTC', + dtstart: '2021-03-07T00:00:00.000Z', + }, + }, + ], + } as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -114,29 +157,24 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "snoozeSchedule": Object { - "duration": -1, - "rRule": Object { - "bymonth": undefined, - "bymonthday": undefined, - "byweekday": undefined, - "count": 1, - "dtstart": "2021-03-07T00:00:00.000Z", - "freq": undefined, - "interval": undefined, - "tzid": "UTC", - "until": undefined, - }, - }, - }, - ] + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.duration).toMatchInlineSnapshot(`-1`); + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` + Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": undefined, + "count": 1, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": undefined, + "interval": undefined, + "tzid": "UTC", + "until": undefined, + } `); - expect(res.noContent).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: [{ ...schedule, duration: '-1', id: 'random-schedule-id', timezone: 'UTC' }], + }); }); it('snoozes an alert with recurring', async () => { @@ -147,9 +185,25 @@ describe('snoozeAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); - - rulesClient.snooze.mockResolvedValueOnce(); + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`); + + rulesClient.snooze.mockResolvedValueOnce({ + ...mockedRule, + snoozeSchedule: [ + { + duration: 864000000, + id: 'random-schedule-id', + rRule: { + tzid: 'America/New_York', + dtstart: '2021-03-07T00:00:00.000Z', + byweekday: ['MO'], + freq: 2, + interval: 1, + until: '2021-05-10T00:00:00.000Z', + }, + }, + ], + } as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -160,6 +214,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { ...schedule, + timezone: 'America/New_York', recurring: { every: '1w', end: '2021-05-10T00:00:00.000Z', @@ -174,31 +229,36 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "snoozeSchedule": Object { - "duration": 864000000, - "rRule": Object { - "bymonth": undefined, - "bymonthday": undefined, - "byweekday": Array [ - "MO", - ], - "count": undefined, - "dtstart": "2021-03-07T00:00:00.000Z", - "freq": 2, - "interval": 1, - "tzid": "UTC", - "until": "2021-05-10T00:00:00.000Z", - }, - }, - }, - ] + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` + Object { + "bymonth": undefined, + "bymonthday": undefined, + "byweekday": Array [ + "MO", + ], + "count": undefined, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": 2, + "interval": 1, + "tzid": "America/New_York", + "until": "2021-05-10T00:00:00.000Z", + } `); - expect(res.noContent).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: [ + { + ...schedule, + recurring: { + every: '1w', + end: '2021-05-10T00:00:00.000Z', + onWeekDay: ['MO'], + }, + id: 'random-schedule-id', + timezone: 'America/New_York', + }, + ], + }); }); it('snoozes an alert with occurrences', async () => { @@ -209,9 +269,26 @@ describe('snoozeAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_snooze"`); - - rulesClient.snooze.mockResolvedValueOnce(); + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`); + + rulesClient.snooze.mockResolvedValueOnce({ + ...mockedRule, + snoozeSchedule: [ + { + duration: 864000000, + id: 'random-schedule-id', + rRule: { + count: 5, + dtstart: '2021-03-07T00:00:00.000Z', + freq: 0, + interval: 1, + tzid: 'UTC', + bymonth: [2, 4, 6, 8, 10, 12], + bymonthday: [5, 25], + }, + }, + ], + } as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -237,39 +314,45 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "snoozeSchedule": Object { - "duration": 864000000, - "rRule": Object { - "bymonth": Array [ - 2, - 4, - 6, - 8, - 10, - 12, - ], - "bymonthday": Array [ - 5, - 25, - ], - "byweekday": undefined, - "count": 5, - "dtstart": "2021-03-07T00:00:00.000Z", - "freq": 0, - "interval": 1, - "tzid": "UTC", - "until": undefined, - }, - }, - }, - ] + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` + Object { + "bymonth": Array [ + 2, + 4, + 6, + 8, + 10, + 12, + ], + "bymonthday": Array [ + 5, + 25, + ], + "byweekday": undefined, + "count": 5, + "dtstart": "2021-03-07T00:00:00.000Z", + "freq": 0, + "interval": 1, + "tzid": "UTC", + "until": undefined, + } `); - expect(res.noContent).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: [ + { + ...schedule, + recurring: { + every: '1y', + occurrences: 5, + onMonthDay: [5, 25], + onMonth: [2, 4, 6, 8, 10, 12], + }, + id: 'random-schedule-id', + timezone: 'UTC', + }, + ], + }); }); it('ensures the rule type gets validated for the license', async () => { diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index f083d4108a3f0..13493fd6c1b3d 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -5,17 +5,23 @@ * 2.0. */ +import { v4 as uuidV4 } from 'uuid'; import { IRouter } from '@kbn/core/server'; import { type SnoozeParams, + type SnoozeResponse, snoozeBodySchema, snoozeParamsSchema, + snoozeResponseSchema, } from '../../../../../../common/routes/rule/apis/snooze'; import { ILicenseState, RuleMutedError } from '../../../../../lib'; import { verifyAccessAndContext } from '../../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; -import { transformSchedule } from '../../../../../../common/routes/schedule'; +import { + transformScheduleToRRule, + transformRRuleToSchedule, +} from '../../../../../../common/routes/schedule'; import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; export const snoozeRuleRoute = ( @@ -24,7 +30,7 @@ export const snoozeRuleRoute = ( ) => { router.post( { - path: `${BASE_ALERTING_API_PATH}/rule/{id}/_snooze`, + path: `${BASE_ALERTING_API_PATH}/rule/{id}/snooze_schedule`, security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'public', @@ -37,7 +43,8 @@ export const snoozeRuleRoute = ( body: snoozeBodySchema, }, response: { - 204: { + 200: { + body: () => snoozeResponseSchema, description: 'Indicates a successful call.', }, 400: { @@ -57,17 +64,24 @@ export const snoozeRuleRoute = ( const alertingContext = await context.alerting; const rulesClient = await alertingContext.getRulesClient(); const params: SnoozeParams = req.params; - const { rRule, duration } = transformSchedule(req.body.schedule); + const { rRule, duration } = transformScheduleToRRule(req.body.schedule); + const snoozeSchedule = { + id: uuidV4(), + duration, + rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], + }; try { - await rulesClient.snooze({ + const snoozedRule = await rulesClient.snooze({ ...params, - snoozeSchedule: { - duration, - rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], - }, + snoozeSchedule, }); - return res.noContent(); + + const response: SnoozeResponse = { + body: transformRRuleToSchedule(snoozedRule?.snoozeSchedule), + }; + + return res.ok(response); } catch (e) { if (e instanceof RuleMutedError) { return e.sendResponse(res); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts index d951b46e2e444..6d88a37ad52b0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts @@ -41,7 +41,7 @@ describe('snoozeAlertRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); - rulesClient.snooze.mockResolvedValueOnce(); + rulesClient.snoozeInternal.mockResolvedValueOnce(); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -58,8 +58,8 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.snoozeInternal).toHaveBeenCalledTimes(1); + expect(rulesClient.snoozeInternal.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -88,7 +88,7 @@ describe('snoozeAlertRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); - rulesClient.snooze.mockResolvedValueOnce(); + rulesClient.snoozeInternal.mockResolvedValueOnce(); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -108,8 +108,8 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.snoozeInternal).toHaveBeenCalledTimes(1); + expect(rulesClient.snoozeInternal.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -136,7 +136,9 @@ describe('snoozeAlertRoute', () => { const [, handler] = router.post.mock.calls[0]; - rulesClient.snooze.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); + rulesClient.snoozeInternal.mockRejectedValue( + new RuleTypeDisabledError('Fail', 'license_invalid') + ); const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ 'ok', diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts index a40d79f5f067d..4345f48d6d8f8 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts @@ -40,7 +40,7 @@ export const snoozeRuleRoute = ( const params: SnoozeRuleRequestInternalParamsV1 = req.params; const body = transformSnoozeBodyV1(req.body); try { - await rulesClient.snooze({ ...params, ...body }); + await rulesClient.snoozeInternal({ ...params, ...body }); return res.noContent(); } catch (e) { if (e instanceof RuleMutedError) { diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts index 5bf7ed4dee82b..5c72afdc277b0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts @@ -51,6 +51,7 @@ const createRulesClientMock = () => { bulkEnableRules: jest.fn(), bulkDisableRules: jest.fn(), snooze: jest.fn(), + snoozeInternal: jest.fn(), unsnooze: jest.fn(), runSoon: jest.fn(), clone: jest.fn(), diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts index 6e8e08bb3827f..23bf37b4441aa 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts @@ -19,14 +19,8 @@ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleDom // If duration is -1, instead mute all const { id: snoozeId, duration } = snoozeSchedule; - if (duration === -1) { - return { - muteAll: true, - snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes), - }; - } - return { + ...(duration === -1 ? { muteAll: true } : {}), snoozeSchedule: (snoozeId ? clearScheduledSnoozesAttributesById(attributes, [snoozeId]) : clearUnscheduledSnoozeAttributes(attributes) diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts index 65d6a118b8561..292c8a01c6332 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts @@ -14,7 +14,12 @@ import { RulesClientContext } from './types'; import { cloneRule, CloneRuleParams } from '../application/rule/methods/clone'; import { createRule, CreateRuleParams } from '../application/rule/methods/create'; import { updateRule, UpdateRuleParams } from '../application/rule/methods/update'; -import { snoozeRule, SnoozeRuleOptions } from '../application/rule/methods/snooze'; +import { + snoozeRule, + snoozeRuleInternal, + type SnoozeRuleOptions, + type SnoozeRule, +} from '../application/rule/methods/snooze'; import { unsnoozeRule, UnsnoozeParams } from '../application/rule/methods/unsnooze'; import { getRule, GetRuleParams } from '../application/rule/methods/get'; import { resolveRule, ResolveParams } from '../application/rule/methods/resolve'; @@ -178,7 +183,8 @@ export class RulesClient { public disableRule = (params: DisableRuleParams) => disableRule(this.context, params); public enableRule = (params: EnableRuleParams) => enableRule(this.context, params); - public snooze = (options: SnoozeRuleOptions) => snoozeRule(this.context, options); + public snoozeInternal = (options: SnoozeRuleOptions) => snoozeRuleInternal(this.context, options); + public snooze = (options: SnoozeRule) => snoozeRule(this.context, options); public unsnooze = (options: UnsnoozeParams) => unsnoozeRule(this.context, options); public muteAll = (options: { id: string }) => muteAll(this.context, options); From ca8e3fabb5ba00a314d99c9e50a70a4092f5536c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 21 Feb 2025 18:05:37 +0000 Subject: [PATCH 11/32] updated request and response schema with custom object --- .../rule/apis/snooze/external/schemas/v1.ts | 17 ++- .../alerting/common/routes/schedule/index.ts | 15 +- .../schedule/schema/{request => }/latest.ts | 0 .../routes/schedule/schema/response/v1.ts | 84 ----------- .../schedule/schema/{request => }/v1.ts | 2 +- .../response => transforms/request}/latest.ts | 0 .../transforms/{ => request}/v1.test.ts | 81 +++++----- .../routes/schedule/transforms/request/v1.ts | 68 +++++++++ .../transforms/{ => response}/latest.ts | 0 .../schedule/transforms/response/v1.test.ts | 142 ++++++++++++++++++ .../routes/schedule/transforms/response/v1.ts | 76 ++++++++++ .../common/routes/schedule/transforms/v1.ts | 135 ----------------- .../snooze/external/snooze_rule.test.ts | 1 - .../methods/snooze/external/snooze_rule.ts | 15 +- .../rule/methods/snooze/external/types.ts | 1 - .../snooze/external/snooze_rule_route.test.ts | 105 ++++++++----- .../apis/snooze/external/snooze_rule_route.ts | 33 +++- 17 files changed, 447 insertions(+), 328 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/{request => }/latest.ts (100%) delete mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/{request => }/v1.ts (99%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/{schema/response => transforms/request}/latest.ts (100%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{ => request}/v1.test.ts (75%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{ => response}/latest.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts delete mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts index 7b6a41492e5e4..ec4e9521a73e8 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts @@ -6,16 +6,27 @@ */ import { schema } from '@kbn/config-schema'; -import { scheduleRequestSchemaV1, scheduleResponseSchemaV1 } from '../../../../../schedule'; +import { scheduleRequestSchemaV1 } from '../../../../../schedule'; export const snoozeParamsSchema = schema.object({ id: schema.string(), }); export const snoozeBodySchema = schema.object({ - schedule: scheduleRequestSchemaV1, + schedule: schema.object({ + custom: schema.maybe(scheduleRequestSchemaV1), + }), }); export const snoozeResponseSchema = schema.object({ - body: scheduleResponseSchemaV1, + body: schema.object({ + schedule: schema.object({ + id: schema.string({ + meta: { + description: 'Identifier of the snooze schedule.', + }, + }), + custom: schema.maybe(scheduleRequestSchemaV1), + }), + }), }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts index c4bdd351b4fcd..0c7cbd5c51af2 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts @@ -5,15 +5,12 @@ * 2.0. */ -export { scheduleRequestSchema } from './schema/request/latest'; -export { scheduleResponseSchema } from './schema/response/latest'; -export { transformScheduleToRRule, transformRRuleToSchedule } from './transforms/latest'; +export { scheduleRequestSchema } from './schema/latest'; +export { transformCustomScheduleToRRule } from './transforms/request/latest'; +export { transformRRuleToCustomSchedule } from './transforms/response/latest'; export type { ScheduleRequest } from './types/latest'; -export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/request/v1'; -export { scheduleResponseSchema as scheduleResponseSchemaV1 } from './schema/response/v1'; -export { - transformScheduleToRRule as transformScheduleToRRuleV1, - transformRRuleToSchedule as transformRRuleToScheduleV1, -} from './transforms/v1'; +export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/v1'; +export { transformCustomScheduleToRRule as transformCustomScheduleToRRuleV1 } from './transforms/request/v1'; +export { transformRRuleToCustomSchedule as transformRRuleToCustomScheduleV1 } from './transforms/response/v1'; export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts deleted file mode 100644 index a2333e1ad6ccf..0000000000000 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/v1.ts +++ /dev/null @@ -1,84 +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. - */ - -import { schema } from '@kbn/config-schema'; - -export const scheduleResponseSchema = schema.arrayOf( - schema.object({ - id: schema.maybe( - schema.string({ - meta: { - description: 'Identifier of the snooze schedule.', - }, - }) - ), - start: schema.string({ - meta: { - description: 'Start date and time of the snooze schedule in ISO 8601 format.', - }, - }), - duration: schema.string({ - meta: { - description: 'Duration of the snooze schedule.', - }, - }), - timezone: schema.maybe( - schema.string({ - meta: { - description: 'Timezone of the snooze schedule.', - }, - }) - ), - recurring: schema.maybe( - schema.object({ - end: schema.maybe( - schema.string({ - meta: { - description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', - }, - }) - ), - every: schema.maybe( - schema.string({ - meta: { - description: 'Recurrence interval and frequency of the snooze schedule.', - }, - }) - ), - onWeekDay: schema.maybe( - schema.arrayOf(schema.string(), { - meta: { - description: - 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', - }, - }) - ), - onMonthDay: schema.maybe( - schema.arrayOf(schema.number(), { - meta: { - description: 'Specific days of the month for recurrence of the snooze schedule.', - }, - }) - ), - onMonth: schema.maybe( - schema.arrayOf(schema.number(), { - meta: { - description: 'Specific months for recurrence of the snooze schedule.', - }, - }) - ), - occurrences: schema.maybe( - schema.number({ - meta: { - description: 'Total number of recurrences of the snooze schedule.', - }, - }) - ), - }) - ), - }) -); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts similarity index 99% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 473434e82092c..fa2cd360cac5f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/request/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -14,7 +14,7 @@ import { validateDurationV1, validateTimezoneV1, validateScheduleV1, -} from '../../validation'; +} from '../validation'; export const scheduleRequestSchema = schema.object( { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/response/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts similarity index 75% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts index 7e0af8c8725a6..0658a11c19b48 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts @@ -4,50 +4,51 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { transformScheduleToRRule } from './v1'; -describe('transformScheduleToRRule', () => { +import { transformCustomScheduleToRRule } from './v1'; + +describe('transformCustomScheduleToRRule', () => { it('transforms start and duration correctly', () => { - expect(transformScheduleToRRule({ duration: '2h', start: '2021-05-10T00:00:00.000Z' })).toEqual( - { - duration: 7200000, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2021-05-10T00:00:00.000Z', - freq: undefined, - interval: undefined, - tzid: 'UTC', - until: undefined, - }, - } - ); + expect( + transformCustomScheduleToRRule({ duration: '2h', start: '2021-05-10T00:00:00.000Z' }) + ).toEqual({ + duration: 7200000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'UTC', + until: undefined, + }, + }); }); it('transforms duration as indefinite correctly', () => { - expect(transformScheduleToRRule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' })).toEqual( - { - duration: -1, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2021-05-10T00:00:00.000Z', - freq: undefined, - interval: undefined, - tzid: 'UTC', - until: undefined, - }, - } - ); + expect( + transformCustomScheduleToRRule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' }) + ).toEqual({ + duration: -1, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2021-05-10T00:00:00.000Z', + freq: undefined, + interval: undefined, + tzid: 'UTC', + until: undefined, + }, + }); }); it('transforms start date and tzid correctly', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '1500s', start: '2025-02-10T21:30.00.000Z', timezone: 'America/New_York', @@ -70,7 +71,7 @@ describe('transformScheduleToRRule', () => { it('transforms recurring with weekday correctly', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '30m', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, @@ -93,7 +94,7 @@ describe('transformScheduleToRRule', () => { it('transforms recurring with month and day correctly', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '5h', start: '2025-02-17T19:04:46.320Z', recurring: { @@ -121,7 +122,7 @@ describe('transformScheduleToRRule', () => { it('transforms recurring with occurrences correctly', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '300s', start: '2025-01-14T05:05:00.000Z', recurring: { every: '2w', occurrences: 3 }, @@ -144,7 +145,7 @@ describe('transformScheduleToRRule', () => { it('transforms duration to 0 when incorrect', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '1y', start: '2025-01-14T05:05:00.000Z', }) @@ -166,7 +167,7 @@ describe('transformScheduleToRRule', () => { it('transforms frequency and interval to undefined when incorrect', () => { expect( - transformScheduleToRRule({ + transformCustomScheduleToRRule({ duration: '1m', start: '2025-01-14T05:05:00.000Z', recurring: { every: '-1h' }, diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts new file mode 100644 index 0000000000000..4936809724536 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts @@ -0,0 +1,68 @@ +/* + * 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-timezone'; +import { Frequency } from '@kbn/rrule'; +import type { RRule } from '../../../../../server/application/r_rule/types'; +import { ScheduleRequest } from '../../types/v1'; +import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants'; + +const transformEveryToFrequency = (frequency?: string) => { + switch (frequency) { + case 'y': + return Frequency.YEARLY; + case 'M': + return Frequency.MONTHLY; + case 'w': + return Frequency.WEEKLY; + case 'd': + return Frequency.DAILY; + default: + return; + } +}; + +const getDurationInMilliseconds = (duration: string): number => { + if (duration === '-1') { + return -1; + } + + const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; + + return moment + .duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor) + .asMilliseconds(); +}; + +export const transformCustomScheduleToRRule = ( + schedule: ScheduleRequest +): { + duration: number; + rRule: RRule | undefined; +} => { + const { recurring, duration, start, timezone } = schedule; + + const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; + const transformedFrequency = transformEveryToFrequency(frequency); + + const durationInMilliseconds = getDurationInMilliseconds(duration); + + return { + duration: durationInMilliseconds, + rRule: { + byweekday: recurring?.onWeekDay, + bymonthday: recurring?.onMonthDay, + bymonth: recurring?.onMonth, + until: recurring?.end, + count: recurring?.occurrences, + interval: interval ? parseInt(interval, 10) : undefined, + freq: transformedFrequency, + dtstart: start, + tzid: timezone ?? DEFAULT_TIMEZONE, + }, + }; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts new file mode 100644 index 0000000000000..5c5c95530aa39 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { transformRRuleToCustomSchedule } from './v1'; + +describe('transformRRuleToCustomSchedule', () => { + it('transforms start and duration correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: 7200000, + rRule: { + dtstart: '2021-05-10T00:00:00.000Z', + tzid: 'UTC', + }, + }, + ], + }) + ).toEqual({ duration: '2h', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); + }); + + it('transforms duration as indefinite correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: -1, + rRule: { + dtstart: '2021-05-10T00:00:00.000Z', + tzid: 'UTC', + }, + }, + ], + }) + ).toEqual({ duration: '-1', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); + }); + + it('transforms start date and timezone correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: 1500000, + rRule: { + dtstart: '2025-02-10T21:30:00.000Z', + tzid: 'America/New_York', + }, + }, + ], + }) + ).toEqual({ + duration: '25m', + start: '2025-02-10T21:30:00.000Z', + timezone: 'America/New_York', + }); + }); + + it('transforms recurring with weekday correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: 1800000, + rRule: { + byweekday: ['Mo', 'FR'], + dtstart: '2025-02-17T19:04:46.320Z', + freq: 3, + interval: 1, + tzid: 'UTC', + until: '2025-05-17T05:05:00.000Z', + }, + }, + ], + }) + ).toEqual({ + duration: '30m', + start: '2025-02-17T19:04:46.320Z', + recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, + timezone: 'UTC', + }); + }); + + it('transforms recurring with month and day correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: 18000000, + rRule: { + bymonth: [1, 3, 5], + bymonthday: [1, 31], + dtstart: '2025-02-17T19:04:46.320Z', + freq: 1, + interval: 1, + tzid: 'UTC', + until: '2025-12-17T05:05:00.000Z', + }, + }, + ], + }) + ).toEqual({ + duration: '5h', + start: '2025-02-17T19:04:46.320Z', + recurring: { + every: '1M', + end: '2025-12-17T05:05:00.000Z', + onMonthDay: [1, 31], + onMonth: [1, 3, 5], + }, + timezone: 'UTC', + }); + }); + + it('transforms recurring with occurrences correctly', () => { + expect( + transformRRuleToCustomSchedule({ + snoozeSchedule: [ + { + duration: 300000, + rRule: { + count: 3, + dtstart: '2025-01-14T05:05:00.000Z', + freq: 2, + interval: 2, + tzid: 'UTC', + }, + }, + ], + }) + ).toEqual({ + duration: '5m', + start: '2025-01-14T05:05:00.000Z', + recurring: { every: '2w', occurrences: 3 }, + timezone: 'UTC', + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts new file mode 100644 index 0000000000000..8a11dcd5989ea --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts @@ -0,0 +1,76 @@ +/* + * 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-timezone'; +import { isEmpty, isUndefined, omitBy } from 'lodash'; +import { Frequency } from '@kbn/rrule'; +import { RuleSnooze } from '@kbn/alerting-types'; +import { ScheduleRequest } from '../../types/v1'; + +const transformFrequencyToEvery = (frequency: Frequency) => { + switch (frequency) { + case Frequency.YEARLY: + return 'y'; + case Frequency.MONTHLY: + return 'M'; + case Frequency.WEEKLY: + return 'w'; + case Frequency.DAILY: + return 'd'; + default: + return; + } +}; + +const getDurationInString = (duration: number): string => { + if (duration === -1) { + return '-1'; + } + + const durationInHours = moment.duration(duration, 'milliseconds').asHours(); + if (durationInHours > 1) { + return `${durationInHours}h`; + } + + const durationInSeconds = moment.duration(duration, 'milliseconds').asSeconds(); + if (durationInSeconds % 60 === 0) { + return `${durationInSeconds / 60}m`; + } + return `${durationInSeconds}s`; +}; + +export const transformRRuleToCustomSchedule = ({ + snoozeSchedule, +}: { + snoozeSchedule: RuleSnooze; +}): ScheduleRequest | undefined => { + if (!snoozeSchedule?.length) { + return; + } + + const { rRule, duration } = snoozeSchedule[snoozeSchedule.length - 1]; + const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); + const transformedDuration = getDurationInString(duration); + + const recurring = { + end: rRule.until ? new Date(rRule.until).toISOString() : undefined, + every: rRule.interval ? `${rRule.interval}${transformedFrequency}` : undefined, + onWeekDay: rRule.byweekday === null ? undefined : (rRule.byweekday as string[]), + onMonthDay: rRule.bymonthday === null ? undefined : rRule.bymonthday, + onMonth: rRule.bymonth === null ? undefined : rRule.bymonth, + occurrences: rRule.count, + }; + + const filteredRecurring = omitBy(recurring, isUndefined); + + return { + duration: transformedDuration, + start: new Date(rRule.dtstart).toISOString(), + timezone: rRule.tzid, + ...(isEmpty(filteredRecurring) ? {} : { recurring: filteredRecurring }), + }; +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts deleted file mode 100644 index 2de2800276223..0000000000000 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/v1.ts +++ /dev/null @@ -1,135 +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. - */ - -import moment from 'moment-timezone'; -import { isEmpty, isUndefined, omitBy } from 'lodash'; -import { Frequency } from '@kbn/rrule'; -import { RuleSnooze } from '@kbn/alerting-types'; -import type { RRule } from '../../../../server/application/r_rule/types'; -import { ScheduleRequest } from '../types/v1'; -import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../constants'; - -const transformEveryToFrequency = (frequency?: string) => { - switch (frequency) { - case 'y': - return Frequency.YEARLY; - case 'M': - return Frequency.MONTHLY; - case 'w': - return Frequency.WEEKLY; - case 'd': - return Frequency.DAILY; - default: - return; - } -}; - -const transformFrequencyToEvery = (frequency: Frequency) => { - switch (frequency) { - case Frequency.YEARLY: - return 'y'; - case Frequency.MONTHLY: - return 'M'; - case Frequency.WEEKLY: - return 'w'; - case Frequency.DAILY: - return 'd'; - default: - return; - } -}; - -const getDurationInMilliseconds = (duration: string): number => { - if (duration === '-1') { - return -1; - } - - const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; - - return moment - .duration(durationNumber, durationUnit as moment.unitOfTime.DurationConstructor) - .asMilliseconds(); -}; - -const getDurationInString = (duration: number): string => { - if (duration === -1) { - return '-1'; - } - - const durationInHours = moment.duration(duration, 'milliseconds').asHours(); - if (durationInHours > 1) { - return `${durationInHours}h`; - } - - const durationInSeconds = moment.duration(duration, 'milliseconds').asSeconds(); - if (durationInSeconds % 60 === 0) { - return `${durationInSeconds / 60}m`; - } - return `${durationInSeconds}s`; -}; - -export const transformScheduleToRRule: (schedule: ScheduleRequest) => { - duration: number; - rRule: RRule | undefined; -} = (schedule) => { - const { recurring, duration, start, timezone } = schedule ?? {}; - - const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; - const transformedFrequency = transformEveryToFrequency(frequency); - - const durationInMilliseconds = getDurationInMilliseconds(duration); - - return { - duration: durationInMilliseconds, - rRule: { - byweekday: recurring?.onWeekDay, - bymonthday: recurring?.onMonthDay, - bymonth: recurring?.onMonth, - until: recurring?.end, - count: recurring?.occurrences, - interval: interval ? parseInt(interval, 10) : undefined, - freq: transformedFrequency, - dtstart: start, - tzid: timezone ?? DEFAULT_TIMEZONE, - }, - }; -}; - -export const transformRRuleToSchedule: ( - snoozeSchedule: RuleSnooze | undefined -) => Array = (snoozeSchedule) => { - if (!snoozeSchedule) { - return []; - } - - const result = snoozeSchedule.map((schedule) => { - const { rRule, duration, id } = schedule; - const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); - const transformedDuration = getDurationInString(duration); - - const recurring = { - end: rRule.until ? new Date(rRule.until).toISOString() : undefined, - every: rRule.interval ? `${rRule.interval}${transformedFrequency}` : undefined, - onWeekDay: rRule.byweekday === null ? undefined : (rRule.byweekday as string[]), - onMonthDay: rRule.bymonthday === null ? undefined : rRule.bymonthday, - onMonth: rRule.bymonth === null ? undefined : rRule.bymonth, - occurrences: rRule.count, - }; - - const filteredRecurring = omitBy(recurring, isUndefined); - - return { - id, - duration: transformedDuration, - start: new Date(rRule.dtstart).toISOString(), - timezone: rRule.tzid, - ...(isEmpty(filteredRecurring) ? {} : { recurring: filteredRecurring }), - }; - }); - - return result; -}; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts index 792680f3e3611..d2055644d23b9 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts @@ -70,7 +70,6 @@ const getSnoozeSchedule = ( override?: SnoozeRule['snoozeSchedule'] ): SnoozeRule['snoozeSchedule'] => { return { - id: '123', duration: 28800000, rRule: { dtstart: '2010-09-19T11:49:59.329Z', diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts index f10dee9a4413d..0abeb96dcb451 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts @@ -6,8 +6,8 @@ */ import Boom from '@hapi/boom'; +import { v4 as uuidV4 } from 'uuid'; 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'; @@ -29,15 +29,17 @@ import { transformRuleAttributesToRuleDomain, } from '../../../transforms'; import { snoozeParamsSchema } from '../../../../../../common/routes/rule/apis/snooze'; +import { ruleSnoozeScheduleSchema } from '../../../../../../common/routes/rule/request'; import type { SnoozeRule } from './types'; export async function snoozeRule( context: RulesClientContext, { id, snoozeSchedule }: SnoozeRule ): Promise> { + const snoozeScheduleId = uuidV4(); try { snoozeParamsSchema.validate({ id }); - ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule }); + ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule, id: snoozeScheduleId }); } catch (error) { throw Boom.badRequest(`Error validating snooze - ${error.message}`); } @@ -49,13 +51,13 @@ export async function snoozeRule( return await retryIfConflicts( context.logger, `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, - async () => await snoozeWithOCC(context, { id, snoozeSchedule }) + async () => await snoozeWithOCC(context, { id, snoozeSchedule, snoozeScheduleId }) ); } async function snoozeWithOCC( context: RulesClientContext, - { id, snoozeSchedule }: SnoozeRule + { id, snoozeSchedule, snoozeScheduleId }: SnoozeRule & { snoozeScheduleId: string } ): Promise> { const { attributes, version } = await withSpan( { name: 'getRuleSavedObject', type: 'rules' }, @@ -97,7 +99,10 @@ async function snoozeWithOCC( context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); + const newAttrs = getSnoozeAttributes(attributes, { + ...snoozeSchedule, + id: snoozeScheduleId, + }); try { verifySnoozeAttributeScheduleLimit(newAttrs); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts index 6a65f65b4a5e0..253ed4b82f31b 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts @@ -10,7 +10,6 @@ import type { RRule } from '../../../../r_rule/types'; export interface SnoozeRule { id: string; snoozeSchedule: { - id: string; duration: number; rRule: RRule; }; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index da68e2dd9d220..cc97c8b3ac2d4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -23,10 +23,12 @@ beforeEach(() => { }); const schedule = { - duration: '240h', - start: '2021-03-07T00:00:00.000Z', - recurring: { - occurrences: 1, + custom: { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + recurring: { + occurrences: 1, + }, }, }; @@ -109,7 +111,9 @@ describe('snoozeAlertRoute', () => { `); expect(res.ok).toHaveBeenCalledWith({ - body: [{ ...schedule, id: 'random-schedule-id', timezone: 'UTC' }], + body: { + schedule: { custom: { ...schedule.custom, timezone: 'UTC' }, id: 'random-schedule-id' }, + }, }); }); @@ -146,8 +150,10 @@ describe('snoozeAlertRoute', () => { }, body: { schedule: { - ...schedule, - duration: '-1', + custom: { + ...schedule.custom, + duration: '-1', + }, }, }, }, @@ -157,7 +163,7 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.duration).toMatchInlineSnapshot(`-1`); + expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.duration).toEqual(-1); expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` Object { "bymonth": undefined, @@ -173,7 +179,12 @@ describe('snoozeAlertRoute', () => { `); expect(res.ok).toHaveBeenCalledWith({ - body: [{ ...schedule, duration: '-1', id: 'random-schedule-id', timezone: 'UTC' }], + body: { + schedule: { + custom: { ...schedule.custom, timezone: 'UTC', duration: '-1' }, + id: 'random-schedule-id', + }, + }, }); }); @@ -213,12 +224,15 @@ describe('snoozeAlertRoute', () => { }, body: { schedule: { - ...schedule, - timezone: 'America/New_York', - recurring: { - every: '1w', - end: '2021-05-10T00:00:00.000Z', - onWeekDay: ['MO'], + custom: { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + timezone: 'America/New_York', + recurring: { + every: '1w', + end: '2021-05-10T00:00:00.000Z', + onWeekDay: ['MO'], + }, }, }, }, @@ -246,18 +260,21 @@ describe('snoozeAlertRoute', () => { `); expect(res.ok).toHaveBeenCalledWith({ - body: [ - { - ...schedule, - recurring: { - every: '1w', - end: '2021-05-10T00:00:00.000Z', - onWeekDay: ['MO'], + body: { + schedule: { + custom: { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + timezone: 'America/New_York', + recurring: { + every: '1w', + end: '2021-05-10T00:00:00.000Z', + onWeekDay: ['MO'], + }, }, id: 'random-schedule-id', - timezone: 'America/New_York', }, - ], + }, }); }); @@ -298,12 +315,15 @@ describe('snoozeAlertRoute', () => { }, body: { schedule: { - ...schedule, - recurring: { - every: '1y', - occurrences: 5, - onMonthDay: [5, 25], - onMonth: [2, 4, 6, 8, 10, 12], + custom: { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + recurring: { + every: '1y', + occurrences: 5, + onMonthDay: [5, 25], + onMonth: [2, 4, 6, 8, 10, 12], + }, }, }, }, @@ -339,19 +359,22 @@ describe('snoozeAlertRoute', () => { `); expect(res.ok).toHaveBeenCalledWith({ - body: [ - { - ...schedule, - recurring: { - every: '1y', - occurrences: 5, - onMonthDay: [5, 25], - onMonth: [2, 4, 6, 8, 10, 12], + body: { + schedule: { + custom: { + duration: '240h', + start: '2021-03-07T00:00:00.000Z', + recurring: { + every: '1y', + occurrences: 5, + onMonthDay: [5, 25], + onMonth: [2, 4, 6, 8, 10, 12], + }, + timezone: 'UTC', }, id: 'random-schedule-id', - timezone: 'UTC', }, - ], + }, }); }); @@ -367,7 +390,7 @@ describe('snoozeAlertRoute', () => { const [context, req, res] = mockHandlerArguments( { rulesClient }, - { params: { id: '1' }, body: { schedule: { duration: '1h' } } }, + { params: { id: '1' }, body: { schedule: { custom: { duration: '1h' } } } }, ['ok', 'forbidden'] ); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 13493fd6c1b3d..86a7744b6a35e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { v4 as uuidV4 } from 'uuid'; +import Boom from '@hapi/boom'; import { IRouter } from '@kbn/core/server'; import { type SnoozeParams, @@ -19,10 +19,10 @@ import { verifyAccessAndContext } from '../../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; import { - transformScheduleToRRule, - transformRRuleToSchedule, + transformCustomScheduleToRRule, + transformRRuleToCustomSchedule, } from '../../../../../../common/routes/schedule'; -import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; +import type { SnoozeRule } from '../../../../../application/rule/methods/snooze'; export const snoozeRuleRoute = ( router: IRouter, @@ -64,11 +64,17 @@ export const snoozeRuleRoute = ( const alertingContext = await context.alerting; const rulesClient = await alertingContext.getRulesClient(); const params: SnoozeParams = req.params; - const { rRule, duration } = transformScheduleToRRule(req.body.schedule); + const customSchedule = req.body.schedule?.custom; + + if (!customSchedule) { + throw Boom.badRequest('Custom schedule is required'); + } + + const { rRule, duration } = transformCustomScheduleToRRule(customSchedule); + const snoozeSchedule = { - id: uuidV4(), duration, - rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], + rRule: rRule as SnoozeRule['snoozeSchedule']['rRule'], }; try { @@ -77,8 +83,19 @@ export const snoozeRuleRoute = ( snoozeSchedule, }); + const createdSchedule = { + id: (snoozedRule?.snoozeSchedule?.length + ? snoozedRule?.snoozeSchedule[snoozedRule.snoozeSchedule.length - 1].id + : '') as string, + custom: transformRRuleToCustomSchedule({ + snoozeSchedule: snoozedRule.snoozeSchedule ?? [], + }), + }; + const response: SnoozeResponse = { - body: transformRRuleToSchedule(snoozedRule?.snoozeSchedule), + body: { + schedule: createdSchedule, + }, }; return res.ok(response); From 1b1fc538f2b8f13305ceded77c7a4e459da7420d Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Mon, 24 Feb 2025 18:21:35 +0000 Subject: [PATCH 12/32] integration tests --- .../methods/snooze/internal/snooze_rule.ts | 4 +- .../server/rules_client/common/index.ts | 1 + .../rules_client/common/snooze_utils.ts | 22 + .../common/lib/alert_utils.ts | 25 +- .../group4/tests/alerting/index.ts | 1 + .../group4/tests/alerting/snooze.ts | 124 ++-- .../group4/tests/alerting/snooze_internal.ts | 392 ++++++++++++ .../tests/alerting/group4/index.ts | 1 + .../tests/alerting/group4/snooze.ts | 329 +++++++--- .../tests/alerting/group4/snooze_internal.ts | 586 ++++++++++++++++++ 10 files changed, 1345 insertions(+), 140 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze_internal.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze_internal.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts index 44a461bdccb2e..19dc83db41d79 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts @@ -17,7 +17,7 @@ import { validateSnoozeStartDate } from '../../../../../lib/validate_snooze_date import { RuleMutedError } from '../../../../../lib/errors/rule_muted'; import { RulesClientContext } from '../../../../../rules_client/types'; import { - getSnoozeAttributes, + getInternalSnoozeAttributes, verifySnoozeAttributeScheduleLimit, } from '../../../../../rules_client/common'; import { updateRuleSo } from '../../../../../data/rule'; @@ -91,7 +91,7 @@ async function snoozeWithOCC( context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); + const newAttrs = getInternalSnoozeAttributes(attributes, snoozeSchedule); try { verifySnoozeAttributeScheduleLimit(newAttrs); diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts index a695e0a8bd6e9..aeaeecce4a35e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts @@ -32,6 +32,7 @@ export { includeFieldsRequiredForAuthentication } from './include_fields_require export { getAndValidateCommonBulkOptions } from './get_and_validate_common_bulk_options'; export { getSnoozeAttributes, + getInternalSnoozeAttributes, getBulkSnooze, getUnsnoozeAttributes, getBulkUnsnooze, diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts index 23bf37b4441aa..d2fa99e1c31f1 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts @@ -28,6 +28,28 @@ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleDom }; } +export function getInternalSnoozeAttributes( + attributes: RawRule, + snoozeSchedule: RuleDomainSnoozeSchedule +) { + // If duration is -1, instead mute all + const { id: snoozeId, duration } = snoozeSchedule; + + if (duration === -1) { + return { + muteAll: true, + snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes), + }; + } + + return { + snoozeSchedule: (snoozeId + ? clearScheduledSnoozesAttributesById(attributes, [snoozeId]) + : clearUnscheduledSnoozeAttributes(attributes) + ).concat(snoozeSchedule), + }; +} + export function getBulkSnooze( rule: RuleDomain, snoozeSchedule: RuleDomainSnoozeSchedule diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 4c693cfaa1b8d..0b3800b5426f8 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -12,6 +12,7 @@ import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; import { getTestRuleData } from './get_test_rule_data'; +import { SnoozeBody } from '@kbn/alerting-plugin/common/routes/rule/apis/snooze'; export interface AlertUtilsOpts { user?: User; @@ -54,6 +55,15 @@ const SNOOZE_SCHEDULE = { duration: 864000000, }; +const snoozeSchedule: SnoozeBody = { + schedule: { + custom: { + duration: '15m', + start: '2021-03-07T00:00:00.000Z', + }, + }, +}; + export class AlertUtils { private referenceCounter = 1; private readonly user?: User; @@ -115,7 +125,7 @@ export class AlertUtils { return request; } - public getSnoozeRequest(alertId: string) { + public getSnoozeInternalRequest(alertId: string) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/internal/alerting/rule/${alertId}/_snooze`) .set('kbn-xsrf', 'foo') @@ -130,6 +140,19 @@ export class AlertUtils { return request; } + public getSnoozeRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/snooze_schedule`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send(snoozeSchedule); + + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + public getUnsnoozeRequest(alertId: string) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/internal/alerting/rule/${alertId}/_unsnooze`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts index 8eb5a0c2006be..33861a7374fd3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts @@ -26,6 +26,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./health')); loadTestFile(require.resolve('./excluded')); loadTestFile(require.resolve('./snooze')); + loadTestFile(require.resolve('./snooze_internal')); loadTestFile(require.resolve('./unsnooze')); loadTestFile(require.resolve('./global_execution_log')); loadTestFile(require.resolve('./get_global_execution_kpi')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts index c880b18dd4b46..0c817ae8ab4f3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts @@ -19,12 +19,16 @@ import { } from '../../../../common/lib'; const NOW = new Date().toISOString(); -const SNOOZE_SCHEDULE = { - duration: 864000000, - rRule: { - dtstart: NOW, - tzid: 'UTC', - count: 1, + +const snoozeSchedule = { + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, }, }; @@ -73,9 +77,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils - .getSnoozeRequest(createdAlert.id) - .send({ snooze_schedule: SNOOZE_SCHEDULE }); + const response = await alertUtils.getSnoozeRequest(createdAlert.id).send(snoozeSchedule); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -99,8 +101,17 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snoozeSchedule.schedule, + id: response.body.schedule.id, + custom: { + ...snoozeSchedule.schedule.custom, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') @@ -109,7 +120,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(updatedAlert.snooze_schedule.length).to.eql(1); const { rRule, duration } = updatedAlert.snooze_schedule[0]; expect(rRule.dtstart).to.eql(NOW); - expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(duration).to.eql(864000000); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -138,9 +149,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils - .getSnoozeRequest(createdAlert.id) - .send({ snooze_schedule: SNOOZE_SCHEDULE }); + const response = await alertUtils.getSnoozeRequest(createdAlert.id).send(snoozeSchedule); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -161,8 +170,17 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snoozeSchedule.schedule, + id: response.body.schedule.id, + custom: { + ...snoozeSchedule.schedule.custom, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') @@ -171,7 +189,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(updatedAlert.snooze_schedule.length).to.eql(1); const { rRule, duration } = updatedAlert.snooze_schedule[0]; expect(rRule.dtstart).to.eql(NOW); - expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(duration).to.eql(864000000); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -200,9 +218,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils - .getSnoozeRequest(createdAlert.id) - .send({ snooze_schedule: SNOOZE_SCHEDULE }); + const response = await alertUtils.getSnoozeRequest(createdAlert.id).send(snoozeSchedule); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -223,8 +239,17 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snoozeSchedule.schedule, + id: response.body.schedule.id, + custom: { + ...snoozeSchedule.schedule.custom, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') @@ -233,7 +258,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(updatedAlert.snooze_schedule.length).to.eql(1); const { rRule, duration } = updatedAlert.snooze_schedule[0]; expect(rRule.dtstart).to.eql(NOW); - expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(duration).to.eql(864000000); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -262,9 +287,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils - .getSnoozeRequest(createdAlert.id) - .send({ snooze_schedule: SNOOZE_SCHEDULE }); + const response = await alertUtils.getSnoozeRequest(createdAlert.id).send(snoozeSchedule); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -281,8 +304,17 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snoozeSchedule.schedule, + id: response.body.schedule.id, + custom: { + ...snoozeSchedule.schedule.custom, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') @@ -291,7 +323,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(updatedAlert.snooze_schedule.length).to.eql(1); const { rRule, duration } = updatedAlert.snooze_schedule[0]; expect(rRule.dtstart).to.eql(NOW); - expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(duration).to.eql(864000000); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -337,9 +369,11 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); const response = await alertUtils.getSnoozeRequest(createdAlert.id).send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - duration: -1, + schedule: { + custom: { + duration: '-1', + start: NOW, + }, }, }); @@ -365,14 +399,32 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + custom: { + duration: '-1', + start: NOW, + timezone: 'UTC', + }, + id: response.body.schedule.id, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([]); + expect(updatedAlert.snooze_schedule).to.eql([ + { + duration: -1, + id: response.body.schedule.id, + rRule: { + dtstart: NOW, + tzid: 'UTC', + }, + }, + ]); expect(updatedAlert.mute_all).to.eql(true); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze_internal.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze_internal.ts new file mode 100644 index 0000000000000..8bc49d46dbf2b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze_internal.ts @@ -0,0 +1,392 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getUnauthorizedErrorMessage, +} from '../../../../common/lib'; + +const NOW = new Date().toISOString(); +const SNOOZE_SCHEDULE = { + duration: 864000000, + rRule: { + dtstart: NOW, + tzid: 'UTC', + count: 1, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('snooze internal', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle snooze rule request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeInternalRequest(createdAlert.id) + .send({ snooze_schedule: SNOOZE_SCHEDULE }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('snooze', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(rRule.dtstart).to.eql(NOW); + expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeInternalRequest(createdAlert.id) + .send({ snooze_schedule: SNOOZE_SCHEDULE }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage( + 'snooze', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(rRule.dtstart).to.eql(NOW); + expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeInternalRequest(createdAlert.id) + .send({ snooze_schedule: SNOOZE_SCHEDULE }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage( + 'snooze', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(rRule.dtstart).to.eql(NOW); + expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeInternalRequest(createdAlert.id) + .send({ snooze_schedule: SNOOZE_SCHEDULE }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('snooze', 'test.restricted-noop', 'alerts'), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(rRule.dtstart).to.eql(NOW); + expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when duration is -1', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils.getSnoozeInternalRequest(createdAlert.id).send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + duration: -1, + }, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('snooze', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts index b2753cb17245d..7032961998543 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -20,6 +20,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./notify_when')); loadTestFile(require.resolve('./muted_alerts')); loadTestFile(require.resolve('./event_log_alerts')); + loadTestFile(require.resolve('./snooze_internal')); loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); loadTestFile(require.resolve('./bulk_edit')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts index 1dfcf400172ed..e22b36607fa3c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { v4 as uuidv4 } from 'uuid'; import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; import { Spaces } from '../../../scenarios'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -20,12 +19,15 @@ import { } from '../../../../common/lib'; const NOW = new Date().toISOString(); -const SNOOZE_SCHEDULE = { - duration: 864000000, - rRule: { - dtstart: NOW, - tzid: 'UTC', - count: 1, +const snoozeSchedule = { + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, }, }; @@ -76,12 +78,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - const response = await alertUtils - .getSnoozeRequest(createdRule.id) - .send({ snooze_schedule: SNOOZE_SCHEDULE }); + const response = await alertUtils.getSnoozeRequest(createdRule.id).send(snoozeSchedule); - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snoozeSchedule.schedule, + id: response.body.schedule.id, + custom: { + ...snoozeSchedule.schedule.custom, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) .set('kbn-xsrf', 'foo') @@ -89,7 +98,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(updatedAlert.snooze_schedule.length).to.eql(1); const { rRule, duration } = updatedAlert.snooze_schedule[0]; expect(rRule.dtstart).to.eql(NOW); - expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(duration).to.eql(864000000); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -135,19 +144,39 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - duration: -1, + schedule: { + custom: { + duration: '-1', + start: NOW, + }, }, }); - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + id: response.body.schedule.id, + custom: { + duration: '-1', + start: NOW, + timezone: 'UTC', + }, + }, + }); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) .set('kbn-xsrf', 'foo') .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([]); + expect(updatedAlert.snooze_schedule).to.eql([ + { + duration: -1, + id: response.body.schedule.id, + rRule: { + dtstart: NOW, + tzid: 'UTC', + }, + }, + ]); expect(updatedAlert.mute_all).to.eql(true); // Ensure AAD isn't broken await checkAAD({ @@ -207,12 +236,13 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext const snoozeSeconds = 10; const snoozeDuration = snoozeSeconds * 1000; await alertUtils.getSnoozeRequest(createdRule.id).send({ - snooze_schedule: { - duration: snoozeDuration, - rRule: { - dtstart: now, - tzid: 'UTC', - count: 1, + schedule: { + custom: { + duration: snoozeDuration, + start: now, + recurring: { + occurrences: 1, + }, }, }, }); @@ -265,59 +295,73 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext // Creating 5 snooze schedules, using Promise.all is very flaky, therefore // the schedules are being created 1 at a time + await alertUtils.getSnoozeRequest(createdRule.id).send(snoozeSchedule).expect(200); await alertUtils .getSnoozeRequest(createdRule.id) .send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), - }, - }) - .expect(204); - await alertUtils - .getSnoozeRequest(createdRule.id) - .send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), + schedule: { + custom: { + duration: '20h', + start: NOW, + recurring: { + every: '1d', + }, + }, }, }) - .expect(204); + .expect(200); await alertUtils .getSnoozeRequest(createdRule.id) .send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), + schedule: { + custom: { + duration: '24h', + start: NOW, + recurring: { + every: '1w', + }, + }, }, }) - .expect(204); - + .expect(200); await alertUtils .getSnoozeRequest(createdRule.id) .send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), + schedule: { + custom: { + duration: '20h', + start: NOW, + recurring: { + every: '1M', + }, + }, }, }) - .expect(204); - + .expect(200); await alertUtils .getSnoozeRequest(createdRule.id) .send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), + schedule: { + custom: { + duration: '24h', + start: NOW, + recurring: { + every: '1y', + }, + }, }, }) - .expect(204); - + .expect(200); // Adding the 6th snooze schedule, should fail const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: uuidv4(), + schedule: { + custom: { + duration: '20h', + start: NOW, + recurring: { + occurrences: 2, + }, + }, }, }); expect(response.statusCode).to.eql(400); @@ -367,28 +411,36 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext const dateStart = new Date().toISOString(); const snooze = { - ...SNOOZE_SCHEDULE, - rRule: { - ...SNOOZE_SCHEDULE.rRule, - // updating the dtstart to the current time because otherwise the snooze might be over already - dtstart: dateStart, + schedule: { + custom: { + start: dateStart, + duration: '3s', + }, }, - duration: 3000, }; - const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ - snooze_schedule: snooze, - }); + const response = await alertUtils.getSnoozeRequest(createdRule.id).send(snooze); - expect(response.statusCode).to.eql(204); - expect(response.body).to.eql(''); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + schedule: { + ...snooze.schedule, + id: response.body.schedule.id, + custom: { + ...snooze.schedule.custom, + timezone: 'UTC', + }, + }, + }); await retry.try(async () => { const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) .set('kbn-xsrf', 'foo') .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([snooze]); + expect(updatedAlert.snooze_schedule).to.eql([ + { duration: 3000, rRule: { dtstart: dateStart, tzid: 'UTC' } }, + ]); }); log.info('wait for snoozing to end'); await retry.try(async () => { @@ -410,7 +462,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext describe('validation', function () { this.tags('skipFIPS'); - it('should return 400 if the id is not in a valid format', async () => { + it('should return 400 if the start is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -424,19 +476,21 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ - snooze_schedule: { - ...SNOOZE_SCHEDULE, - id: 'invalid key', + schedule: { + custom: { + start: 'invalid', + duration: '240h', + }, }, }); 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` + '[request body.schedule.custom.start]: Invalid snooze start date: invalid' ); }); - it('accepts a uuid as a key', async () => { + it('should return 400 if the duration is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -449,18 +503,80 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext 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', + const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ + schedule: { + custom: { + start: snoozeSchedule.schedule.custom.start, + duration: 'invalid', }, - }) - .expect(204); + }, + }); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + '[request body.schedule.custom.duration]: Invalid schedule duration format: invalid' + ); + }); + + it('should return 400 if the every 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({ + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { + every: 'invalid', + }, + }, + }, + }); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + `[request body.schedule.custom.recurring.every]: 'every' string of recurring schedule is not valid : invalid` + ); + }); + + it('should return 400 if the onWeekDay 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({ + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { onWeekDay: ['invalid'] }, + }, + }, + }); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + '[request body.schedule.custom.recurring.onWeekDay]: Invalid onWeekDay values in recurring schedule: invalid' + ); }); - it('should return 400 if the timezone is not valid', async () => { + it('should return 400 if the onMonthDay is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -474,19 +590,21 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext 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' }, + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { onMonthDay: [35] }, + }, }, }); 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' + '[request body.schedule.custom.recurring.onMonthDay.0]: Value must be equal to or lower than [31].' ); }); - it('should return 400 if the byweekday is not valid', async () => { + it('should return 400 if the onMonth is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -500,16 +618,21 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext 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'] }, + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { onMonth: [14] }, + }, }, }); expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + '[request body.schedule.custom.recurring.onMonth.0]: Value must be equal to or lower than [12].' + ); }); - it('should return 400 if the bymonthday is not valid', async () => { + it('should return 400 if the end date is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -523,19 +646,21 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext 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] }, + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { end: 'invalid' }, + }, }, }); 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].' + '[request body.schedule.custom.recurring.end]: Invalid snooze end date: invalid' ); }); - it('should return 400 if the bymonth is not valid', async () => { + it('should return 400 if the occurrences is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -549,15 +674,17 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext 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] }, + schedule: { + custom: { + ...snoozeSchedule.schedule.custom, + recurring: { occurrences: 0 }, + }, }, }); 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].' + '[request body.schedule.custom.recurring.occurrences]: Value must be equal to or greater than [1].' ); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze_internal.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze_internal.ts new file mode 100644 index 0000000000000..cbb6bbf538e83 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze_internal.ts @@ -0,0 +1,586 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; +import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getEventLog, +} from '../../../../common/lib'; + +const NOW = new Date().toISOString(); +const SNOOZE_SCHEDULE = { + duration: 864000000, + rRule: { + dtstart: NOW, + tzid: 'UTC', + count: 1, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const log = getService('log'); + const retry = getService('retry'); + + describe('snooze internal', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + describe('handle snooze rule request appropriately', function () { + this.tags('skipFIPS'); + it('should handle snooze rule request appropriately', async () => { + const { body: createdConnector } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY Connector', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdConnector.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ snooze_schedule: SNOOZE_SCHEDULE }); + + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(rRule.dtstart).to.eql(NOW); + expect(duration).to.eql(SNOOZE_SCHEDULE.duration); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdRule.id, + }); + }); + }); + + describe('handle snooze rule request appropriately when duration is -1', function () { + this.tags('skipFIPS'); + it('should handle snooze rule request appropriately when duration is -1', async () => { + const { body: createdConnector } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY Connector', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdConnector.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + const response = await alertUtils.getSnoozeInternalRequest(createdRule.id).send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + duration: -1, + }, + }); + + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.snooze_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdRule.id, + }); + }); + }); + + it('should not trigger actions when snoozed', async () => { + const { body: createdConnector, status: connStatus } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY Connector', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }); + expect(connStatus).to.be(200); + objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); + + log.info('creating rule'); + const { body: createdRule, status: ruleStatus } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + name: 'should not trigger actions when snoozed', + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: { + pattern: { instance: arrayOfTrues(100) }, + }, + actions: [ + { + id: createdConnector.id, + group: 'default', + params: {}, + }, + ], + }) + ); + expect(ruleStatus).to.be(200); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + // wait for an action to be triggered + log.info('wait for rule to trigger an action'); + await getRuleEvents(createdRule.id); + + log.info('start snoozing'); + const now = new Date().toISOString(); + const snoozeSeconds = 10; + const snoozeDuration = snoozeSeconds * 1000; + await alertUtils.getSnoozeInternalRequest(createdRule.id).send({ + snooze_schedule: { + duration: snoozeDuration, + rRule: { + dtstart: now, + tzid: 'UTC', + count: 1, + }, + }, + }); + + // could be an action execution while calling snooze, so set snooze start + // to a value that we know it will be in effect (after this call) + const snoozeStartDate = new Date(); + + // wait for 4 triggered actions - in case some fired before snooze went into effect + log.info('wait for snoozing to end'); + const ruleEvents = await getRuleEvents(createdRule.id, 4); + const snoozeStart = snoozeStartDate.valueOf(); + const snoozeEnd = snoozeStartDate.valueOf(); + let actionsBefore = 0; + let actionsDuring = 0; + let actionsAfter = 0; + + for (const event of ruleEvents) { + const timestamp = event?.['@timestamp']; + if (!timestamp) continue; + + const time = new Date(timestamp).valueOf(); + if (time < snoozeStart) { + actionsBefore++; + } else if (time > snoozeEnd) { + actionsAfter++; + } else { + actionsDuring++; + } + } + + expect(actionsBefore).to.be.greaterThan(0, 'no actions triggered before snooze'); + expect(actionsAfter).to.be.greaterThan(0, 'no actions triggered after snooze'); + expect(actionsDuring).to.be(0); + }); + + describe('prevent more than 5 schedules from being added to a rule', function () { + this.tags('skipFIPS'); + it('should prevent more than 5 schedules from being added to a rule', 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'); + + // Creating 5 snooze schedules, using Promise.all is very flaky, therefore + // the schedules are being created 1 at a time + await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }) + .expect(204); + await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }) + .expect(204); + await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }) + .expect(204); + + await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }) + .expect(204); + + await alertUtils + .getSnoozeInternalRequest(createdRule.id) + .send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }) + .expect(204); + + // Adding the 6th snooze schedule, should fail + const response = await alertUtils.getSnoozeInternalRequest(createdRule.id).send({ + snooze_schedule: { + ...SNOOZE_SCHEDULE, + id: uuidv4(), + }, + }); + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql('Rule cannot have more than 5 snooze schedules'); + }); + }); + + describe('clear the snooze after it expires', function () { + this.tags('skipFIPS'); + it('should clear the snooze after it expires', async () => { + const { body: createdConnector } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY Connector', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + name: 'should not trigger actions when snoozed', + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: { + pattern: { instance: arrayOfTrues(100) }, + }, + actions: [ + { + id: createdConnector.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + const dateStart = new Date().toISOString(); + const snooze = { + ...SNOOZE_SCHEDULE, + rRule: { + ...SNOOZE_SCHEDULE.rRule, + // updating the dtstart to the current time because otherwise the snooze might be over already + dtstart: dateStart, + }, + duration: 3000, + }; + + const response = await alertUtils.getSnoozeInternalRequest(createdRule.id).send({ + snooze_schedule: snooze, + }); + + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + + await retry.try(async () => { + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.snooze_schedule).to.eql([snooze]); + }); + log.info('wait for snoozing to end'); + await retry.try(async () => { + const { body: alertWithExpiredSnooze } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(alertWithExpiredSnooze.snooze_schedule).to.eql([]); + }); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdRule.id, + }); + }); + }); + + describe('validation', function () { + this.tags('skipFIPS'); + 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.getSnoozeInternalRequest(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 + .getSnoozeInternalRequest(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.getSnoozeInternalRequest(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.getSnoozeInternalRequest(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.getSnoozeInternalRequest(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.getSnoozeInternalRequest(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) { + return await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id, + provider: 'alerting', + actions: new Map([['execute-action', { gte: minActions }]]), + }); + }); + } +} + +function arrayOfTrues(length: number) { + const result = []; + for (let i = 0; i < length; i++) { + result.push(true); + } + return result; +} From bbd5eedf6ece42119b8a705bf0c987df4794d363 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 25 Feb 2025 18:16:40 +0000 Subject: [PATCH 13/32] cleanup --- .../rule/apis/snooze/external/schemas/v1.ts | 6 +- .../schedule/transforms/request/v1.test.ts | 23 +++ .../schedule/transforms/response/v1.test.ts | 141 +++++++++--------- .../routes/schedule/transforms/response/v1.ts | 13 +- .../validation/validate_timezone/v1.test.ts | 4 + .../validation/validation_schedule/v1.test.ts | 13 +- .../apis/snooze/external/snooze_rule_route.ts | 10 +- .../apis/snooze/internal/transforms/index.ts | 1 + 8 files changed, 125 insertions(+), 86 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts index ec4e9521a73e8..cb84d5a46df5f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/schemas/v1.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import { scheduleRequestSchemaV1 } from '../../../../../schedule'; export const snoozeParamsSchema = schema.object({ - id: schema.string(), + id: schema.string({ + meta: { + description: 'Identifier of the rule.', + }, + }), }); export const snoozeBodySchema = schema.object({ diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts index 0658a11c19b48..b0c47e1628010 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts @@ -69,6 +69,29 @@ describe('transformCustomScheduleToRRule', () => { }); }); + it('transforms recurring with every correctly', () => { + expect( + transformCustomScheduleToRRule({ + duration: '30m', + start: '2025-02-17T19:04:46.320Z', + recurring: { every: '4w' }, + }) + ).toEqual({ + duration: 1800000, + rRule: { + bymonth: undefined, + bymonthday: undefined, + byweekday: undefined, + count: undefined, + dtstart: '2025-02-17T19:04:46.320Z', + freq: 2, + interval: 4, + tzid: 'UTC', + until: undefined, + }, + }); + }); + it('transforms recurring with weekday correctly', () => { expect( transformCustomScheduleToRRule({ diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts index 5c5c95530aa39..8796df43d0482 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts @@ -8,50 +8,38 @@ import { transformRRuleToCustomSchedule } from './v1'; describe('transformRRuleToCustomSchedule', () => { - it('transforms start and duration correctly', () => { + it('transforms to start and duration correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: 7200000, - rRule: { - dtstart: '2021-05-10T00:00:00.000Z', - tzid: 'UTC', - }, - }, - ], + duration: 7200000, + rRule: { + dtstart: '2021-05-10T00:00:00.000Z', + tzid: 'UTC', + }, }) ).toEqual({ duration: '2h', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); }); - it('transforms duration as indefinite correctly', () => { + it('transforms to duration as indefinite correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: -1, - rRule: { - dtstart: '2021-05-10T00:00:00.000Z', - tzid: 'UTC', - }, - }, - ], + duration: -1, + rRule: { + dtstart: '2021-05-10T00:00:00.000Z', + tzid: 'UTC', + }, }) ).toEqual({ duration: '-1', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); }); - it('transforms start date and timezone correctly', () => { + it('transforms to start date and timezone correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: 1500000, - rRule: { - dtstart: '2025-02-10T21:30:00.000Z', - tzid: 'America/New_York', - }, - }, - ], + duration: 1500000, + rRule: { + dtstart: '2025-02-10T21:30:00.000Z', + tzid: 'America/New_York', + }, }) ).toEqual({ duration: '25m', @@ -60,22 +48,39 @@ describe('transformRRuleToCustomSchedule', () => { }); }); - it('transforms recurring with weekday correctly', () => { + it('transforms to recurring with every correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: 1800000, - rRule: { - byweekday: ['Mo', 'FR'], - dtstart: '2025-02-17T19:04:46.320Z', - freq: 3, - interval: 1, - tzid: 'UTC', - until: '2025-05-17T05:05:00.000Z', - }, - }, - ], + duration: 1800000, + rRule: { + byweekday: ['Mo', 'FR'], + dtstart: '2025-02-17T19:04:46.320Z', + freq: 1, + interval: 6, + tzid: 'UTC', + until: '2025-05-17T05:05:00.000Z', + }, + }) + ).toEqual({ + duration: '30m', + start: '2025-02-17T19:04:46.320Z', + recurring: { every: '6M', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, + timezone: 'UTC', + }); + }); + + it('transforms to recurring with weekday correctly', () => { + expect( + transformRRuleToCustomSchedule({ + duration: 1800000, + rRule: { + byweekday: ['Mo', 'FR'], + dtstart: '2025-02-17T19:04:46.320Z', + freq: 3, + interval: 1, + tzid: 'UTC', + until: '2025-05-17T05:05:00.000Z', + }, }) ).toEqual({ duration: '30m', @@ -85,23 +90,19 @@ describe('transformRRuleToCustomSchedule', () => { }); }); - it('transforms recurring with month and day correctly', () => { + it('transforms to recurring with month and day correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: 18000000, - rRule: { - bymonth: [1, 3, 5], - bymonthday: [1, 31], - dtstart: '2025-02-17T19:04:46.320Z', - freq: 1, - interval: 1, - tzid: 'UTC', - until: '2025-12-17T05:05:00.000Z', - }, - }, - ], + duration: 18000000, + rRule: { + bymonth: [1, 3, 5], + bymonthday: [1, 31], + dtstart: '2025-02-17T19:04:46.320Z', + freq: 1, + interval: 1, + tzid: 'UTC', + until: '2025-12-17T05:05:00.000Z', + }, }) ).toEqual({ duration: '5h', @@ -116,21 +117,17 @@ describe('transformRRuleToCustomSchedule', () => { }); }); - it('transforms recurring with occurrences correctly', () => { + it('transforms to recurring with occurrences correctly', () => { expect( transformRRuleToCustomSchedule({ - snoozeSchedule: [ - { - duration: 300000, - rRule: { - count: 3, - dtstart: '2025-01-14T05:05:00.000Z', - freq: 2, - interval: 2, - tzid: 'UTC', - }, - }, - ], + duration: 300000, + rRule: { + count: 3, + dtstart: '2025-01-14T05:05:00.000Z', + freq: 2, + interval: 2, + tzid: 'UTC', + }, }) ).toEqual({ duration: '5m', diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts index 8a11dcd5989ea..7e7e2682ab8b5 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { Frequency } from '@kbn/rrule'; -import { RuleSnooze } from '@kbn/alerting-types'; +import type { RRule } from '../../../../../server/application/r_rule/types'; import { ScheduleRequest } from '../../types/v1'; const transformFrequencyToEvery = (frequency: Frequency) => { @@ -43,16 +43,15 @@ const getDurationInString = (duration: number): string => { return `${durationInSeconds}s`; }; -export const transformRRuleToCustomSchedule = ({ - snoozeSchedule, -}: { - snoozeSchedule: RuleSnooze; +export const transformRRuleToCustomSchedule = (snoozeSchedule?: { + duration: number; + rRule: RRule; }): ScheduleRequest | undefined => { - if (!snoozeSchedule?.length) { + if (!snoozeSchedule) { return; } - const { rRule, duration } = snoozeSchedule[snoozeSchedule.length - 1]; + const { rRule, duration } = snoozeSchedule; const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); const transformedDuration = getDurationInString(duration); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts index dde7a22355af0..9e915760c80c5 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts @@ -17,6 +17,10 @@ describe('validateTimezone', () => { expect(validateTimezone()).toBeUndefined(); }); + it('throws error for invalid combination', () => { + expect(validateTimezone('Europe/India')).toEqual('Invalid snooze timezone: Europe/India'); + }); + it('throws error for invalid timezone', () => { expect(validateTimezone('invalid')).toEqual('Invalid snooze timezone: invalid'); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts index 3adbfc28e8e47..c9bf0a0603f39 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -33,6 +33,17 @@ describe('validateSchedule', () => { duration: '700h', recurring: { every: '1w' }, }) - ).toEqual(`Recurrence every 1w must be longer than the snooze duration 700h`); + ).toEqual('Recurrence every 1w must be longer than the snooze duration 700h'); + }); + + it('throws error when recurring schedule provided with indefinite duration', () => { + expect( + validateSchedule({ + duration: '-1', + recurring: { every: '1M' }, + }) + ).toEqual( + 'The duration of -1 snoozes the rule indefinitely. Recurring schedules cannot be set when the duration is -1.' + ); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 86a7744b6a35e..5d3dd548641b9 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -34,7 +34,7 @@ export const snoozeRuleRoute = ( security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'public', - summary: 'Snooze a rule', + summary: 'Schedule a snooze for the rule', tags: ['oas-tag:alerting'], }, validate: { @@ -54,7 +54,7 @@ export const snoozeRuleRoute = ( description: 'Indicates that this call is forbidden.', }, 404: { - description: 'Indicates a rule with the given ID does not exist.', + description: 'Indicates a rule with the given id does not exist.', }, }, }, @@ -87,9 +87,9 @@ export const snoozeRuleRoute = ( id: (snoozedRule?.snoozeSchedule?.length ? snoozedRule?.snoozeSchedule[snoozedRule.snoozeSchedule.length - 1].id : '') as string, - custom: transformRRuleToCustomSchedule({ - snoozeSchedule: snoozedRule.snoozeSchedule ?? [], - }), + custom: transformRRuleToCustomSchedule( + snoozedRule?.snoozeSchedule?.[snoozedRule.snoozeSchedule.length - 1] + ), }; const response: SnoozeResponse = { diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts index fce9858f6aa5c..fce05888d26fa 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/transforms/index.ts @@ -6,4 +6,5 @@ */ export { transformSnoozeBody } from './transform_snooze_body/latest'; + export { transformSnoozeBody as transformSnoozeBodyV1 } from './transform_snooze_body/v1'; From ea7d6041cf5dcb16e0a4d3d637b8d5fba74d626f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:30:54 +0000 Subject: [PATCH 14/32] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 313 +++++++++++++++++++++----------- oas_docs/bundle.serverless.json | 313 +++++++++++++++++++++----------- 2 files changed, 424 insertions(+), 202 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 381512386447f..d77ee0b3a1be7 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4274,9 +4274,9 @@ ] } }, - "/api/alerting/rule/{id}/_snooze": { + "/api/alerting/rule/{id}/_unmute_all": { "post": { - "operationId": "post-alerting-rule-id-snooze", + "operationId": "post-alerting-rule-id-unmute-all", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4289,6 +4289,7 @@ } }, { + "description": "The identifier for the rule.", "in": "path", "name": "id", "required": true, @@ -4297,96 +4298,12 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "schedule": { - "additionalProperties": false, - "properties": { - "duration": { - "description": "Duration of the snooze schedule.", - "type": "string" - }, - "recurring": { - "additionalProperties": false, - "properties": { - "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", - "type": "string" - }, - "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", - "type": "string" - }, - "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", - "minimum": 1, - "type": "number" - }, - "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", - "items": { - "maximum": 12, - "minimum": 1, - "type": "number" - }, - "minItems": 1, - "type": "array" - }, - "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", - "items": { - "maximum": 31, - "minimum": 1, - "type": "number" - }, - "minItems": 1, - "type": "array" - }, - "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - } - }, - "type": "object" - }, - "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", - "type": "string" - }, - "timezone": { - "description": "Timezone of the snooze schedule.", - "type": "string" - } - }, - "required": [ - "start", - "duration" - ], - "type": "object" - } - }, - "required": [ - "schedule" - ], - "type": "object" - } - } - } - }, "responses": { "204": { "description": "Indicates a successful call." }, "400": { - "description": "Indicates an invalid schema." + "description": "Indicates an invalid schema or parameters." }, "403": { "description": "Indicates that this call is forbidden." @@ -4395,15 +4312,15 @@ "description": "Indicates a rule with the given ID does not exist." } }, - "summary": "Snooze a rule", + "summary": "Unmute all alerts", "tags": [ "alerting" ] } }, - "/api/alerting/rule/{id}/_unmute_all": { + "/api/alerting/rule/{id}/_update_api_key": { "post": { - "operationId": "post-alerting-rule-id-unmute-all", + "operationId": "post-alerting-rule-id-update-api-key", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4437,17 +4354,20 @@ }, "404": { "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, - "summary": "Unmute all alerts", + "summary": "Update the API key for a rule", "tags": [ "alerting" ] } }, - "/api/alerting/rule/{id}/_update_api_key": { + "/api/alerting/rule/{id}/snooze_schedule": { "post": { - "operationId": "post-alerting-rule-id-update-api-key", + "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4460,7 +4380,7 @@ } }, { - "description": "The identifier for the rule.", + "description": "Identifier of the rule.", "in": "path", "name": "id", "required": true, @@ -4469,24 +4389,215 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "custom": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + } + }, + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + } + } + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "body": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "custom": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + }, + "id": { + "description": "Identifier of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + }, + "required": [ + "body" + ], + "type": "object" + } + } + }, "description": "Indicates a successful call." }, "400": { - "description": "Indicates an invalid schema or parameters." + "description": "Indicates an invalid schema." }, "403": { "description": "Indicates that this call is forbidden." }, "404": { - "description": "Indicates a rule with the given ID does not exist." - }, - "409": { - "description": "Indicates that the rule has already been updated by another user." + "description": "Indicates a rule with the given id does not exist." } }, - "summary": "Update the API key for a rule", + "summary": "Schedule a snooze for the rule", "tags": [ "alerting" ] diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index f6c0f945e6795..eaaa972c5026e 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4274,9 +4274,9 @@ ] } }, - "/api/alerting/rule/{id}/_snooze": { + "/api/alerting/rule/{id}/_unmute_all": { "post": { - "operationId": "post-alerting-rule-id-snooze", + "operationId": "post-alerting-rule-id-unmute-all", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4289,6 +4289,7 @@ } }, { + "description": "The identifier for the rule.", "in": "path", "name": "id", "required": true, @@ -4297,96 +4298,12 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "schedule": { - "additionalProperties": false, - "properties": { - "duration": { - "description": "Duration of the snooze schedule.", - "type": "string" - }, - "recurring": { - "additionalProperties": false, - "properties": { - "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", - "type": "string" - }, - "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", - "type": "string" - }, - "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", - "minimum": 1, - "type": "number" - }, - "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", - "items": { - "maximum": 12, - "minimum": 1, - "type": "number" - }, - "minItems": 1, - "type": "array" - }, - "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", - "items": { - "maximum": 31, - "minimum": 1, - "type": "number" - }, - "minItems": 1, - "type": "array" - }, - "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - } - }, - "type": "object" - }, - "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", - "type": "string" - }, - "timezone": { - "description": "Timezone of the snooze schedule.", - "type": "string" - } - }, - "required": [ - "start", - "duration" - ], - "type": "object" - } - }, - "required": [ - "schedule" - ], - "type": "object" - } - } - } - }, "responses": { "204": { "description": "Indicates a successful call." }, "400": { - "description": "Indicates an invalid schema." + "description": "Indicates an invalid schema or parameters." }, "403": { "description": "Indicates that this call is forbidden." @@ -4395,15 +4312,15 @@ "description": "Indicates a rule with the given ID does not exist." } }, - "summary": "Snooze a rule", + "summary": "Unmute all alerts", "tags": [ "alerting" ] } }, - "/api/alerting/rule/{id}/_unmute_all": { + "/api/alerting/rule/{id}/_update_api_key": { "post": { - "operationId": "post-alerting-rule-id-unmute-all", + "operationId": "post-alerting-rule-id-update-api-key", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4437,17 +4354,20 @@ }, "404": { "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, - "summary": "Unmute all alerts", + "summary": "Update the API key for a rule", "tags": [ "alerting" ] } }, - "/api/alerting/rule/{id}/_update_api_key": { + "/api/alerting/rule/{id}/snooze_schedule": { "post": { - "operationId": "post-alerting-rule-id-update-api-key", + "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { "description": "A required header to protect against CSRF attacks", @@ -4460,7 +4380,7 @@ } }, { - "description": "The identifier for the rule.", + "description": "Identifier of the rule.", "in": "path", "name": "id", "required": true, @@ -4469,24 +4389,215 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "custom": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + } + }, + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + } + } + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "body": { + "additionalProperties": false, + "properties": { + "schedule": { + "additionalProperties": false, + "properties": { + "custom": { + "additionalProperties": false, + "properties": { + "duration": { + "description": "Duration of the snooze schedule.", + "type": "string" + }, + "recurring": { + "additionalProperties": false, + "properties": { + "end": { + "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "every": { + "description": "Recurrence interval and frequency of the snooze schedule.", + "type": "string" + }, + "occurrences": { + "description": "Total number of recurrences of the snooze schedule.", + "minimum": 1, + "type": "number" + }, + "onMonth": { + "description": "Specific months for recurrence of the snooze schedule.", + "items": { + "maximum": 12, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onMonthDay": { + "description": "Specific days of the month for recurrence of the snooze schedule.", + "items": { + "maximum": 31, + "minimum": 1, + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + "onWeekDay": { + "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "start": { + "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "type": "string" + }, + "timezone": { + "description": "Timezone of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "start", + "duration" + ], + "type": "object" + }, + "id": { + "description": "Identifier of the snooze schedule.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "required": [ + "schedule" + ], + "type": "object" + } + }, + "required": [ + "body" + ], + "type": "object" + } + } + }, "description": "Indicates a successful call." }, "400": { - "description": "Indicates an invalid schema or parameters." + "description": "Indicates an invalid schema." }, "403": { "description": "Indicates that this call is forbidden." }, "404": { - "description": "Indicates a rule with the given ID does not exist." - }, - "409": { - "description": "Indicates that the rule has already been updated by another user." + "description": "Indicates a rule with the given id does not exist." } }, - "summary": "Update the API key for a rule", + "summary": "Schedule a snooze for the rule", "tags": [ "alerting" ] From 02bdcd504fc4982d2fb5d0a828693d91a8ec1cb8 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:45:52 +0000 Subject: [PATCH 15/32] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 170 +++++++++++++++++++++++++ oas_docs/output/kibana.yaml | 169 ++++++++++++++++++++++++ 2 files changed, 339 insertions(+) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index b0d5f29af3294..4bd6258d6e7a0 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -3403,6 +3403,176 @@ paths: tags: - alerting x-beta: true + /api/alerting/rule/{id}/snooze_schedule: + post: + operationId: post-alerting-rule-id-snooze-schedule + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - description: Identifier of the rule. + in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + schedule: + additionalProperties: false + type: object + properties: + custom: + additionalProperties: false + type: object + properties: + duration: + description: Duration of the snooze schedule. + type: string + recurring: + additionalProperties: false + type: object + properties: + end: + description: End date of recurrence of the snooze schedule in ISO 8601 format. + type: string + every: + description: Recurrence interval and frequency of the snooze schedule. + type: string + occurrences: + description: Total number of recurrences of the snooze schedule. + minimum: 1 + type: number + onMonth: + description: Specific months for recurrence of the snooze schedule. + items: + maximum: 12 + minimum: 1 + type: number + minItems: 1 + type: array + onMonthDay: + description: Specific days of the month for recurrence of the snooze schedule. + items: + maximum: 31 + minimum: 1 + type: number + minItems: 1 + type: array + onWeekDay: + description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + items: + type: string + minItems: 1 + type: array + start: + description: Start date and time of the snooze schedule in ISO 8601 format. + type: string + timezone: + description: Timezone of the snooze schedule. + type: string + required: + - start + - duration + required: + - schedule + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + body: + additionalProperties: false + type: object + properties: + schedule: + additionalProperties: false + type: object + properties: + custom: + additionalProperties: false + type: object + properties: + duration: + description: Duration of the snooze schedule. + type: string + recurring: + additionalProperties: false + type: object + properties: + end: + description: End date of recurrence of the snooze schedule in ISO 8601 format. + type: string + every: + description: Recurrence interval and frequency of the snooze schedule. + type: string + occurrences: + description: Total number of recurrences of the snooze schedule. + minimum: 1 + type: number + onMonth: + description: Specific months for recurrence of the snooze schedule. + items: + maximum: 12 + minimum: 1 + type: number + minItems: 1 + type: array + onMonthDay: + description: Specific days of the month for recurrence of the snooze schedule. + items: + maximum: 31 + minimum: 1 + type: number + minItems: 1 + type: array + onWeekDay: + description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + items: + type: string + minItems: 1 + type: array + start: + description: Start date and time of the snooze schedule in ISO 8601 format. + type: string + timezone: + description: Timezone of the snooze schedule. + type: string + required: + - start + - duration + id: + description: Identifier of the snooze schedule. + type: string + required: + - id + required: + - schedule + required: + - body + description: Indicates a successful call. + '400': + description: Indicates an invalid schema. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given id does not exist. + summary: Schedule a snooze for the rule + tags: + - alerting + x-beta: true /api/alerting/rule/{rule_id}/alert/{alert_id}/_mute: post: operationId: post-alerting-rule-rule-id-alert-alert-id-mute diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 53140ba997244..82f11eef3f819 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -3748,6 +3748,175 @@ paths: summary: Update the API key for a rule tags: - alerting + /api/alerting/rule/{id}/snooze_schedule: + post: + operationId: post-alerting-rule-id-snooze-schedule + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - description: Identifier of the rule. + in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + schedule: + additionalProperties: false + type: object + properties: + custom: + additionalProperties: false + type: object + properties: + duration: + description: Duration of the snooze schedule. + type: string + recurring: + additionalProperties: false + type: object + properties: + end: + description: End date of recurrence of the snooze schedule in ISO 8601 format. + type: string + every: + description: Recurrence interval and frequency of the snooze schedule. + type: string + occurrences: + description: Total number of recurrences of the snooze schedule. + minimum: 1 + type: number + onMonth: + description: Specific months for recurrence of the snooze schedule. + items: + maximum: 12 + minimum: 1 + type: number + minItems: 1 + type: array + onMonthDay: + description: Specific days of the month for recurrence of the snooze schedule. + items: + maximum: 31 + minimum: 1 + type: number + minItems: 1 + type: array + onWeekDay: + description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + items: + type: string + minItems: 1 + type: array + start: + description: Start date and time of the snooze schedule in ISO 8601 format. + type: string + timezone: + description: Timezone of the snooze schedule. + type: string + required: + - start + - duration + required: + - schedule + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + body: + additionalProperties: false + type: object + properties: + schedule: + additionalProperties: false + type: object + properties: + custom: + additionalProperties: false + type: object + properties: + duration: + description: Duration of the snooze schedule. + type: string + recurring: + additionalProperties: false + type: object + properties: + end: + description: End date of recurrence of the snooze schedule in ISO 8601 format. + type: string + every: + description: Recurrence interval and frequency of the snooze schedule. + type: string + occurrences: + description: Total number of recurrences of the snooze schedule. + minimum: 1 + type: number + onMonth: + description: Specific months for recurrence of the snooze schedule. + items: + maximum: 12 + minimum: 1 + type: number + minItems: 1 + type: array + onMonthDay: + description: Specific days of the month for recurrence of the snooze schedule. + items: + maximum: 31 + minimum: 1 + type: number + minItems: 1 + type: array + onWeekDay: + description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + items: + type: string + minItems: 1 + type: array + start: + description: Start date and time of the snooze schedule in ISO 8601 format. + type: string + timezone: + description: Timezone of the snooze schedule. + type: string + required: + - start + - duration + id: + description: Identifier of the snooze schedule. + type: string + required: + - id + required: + - schedule + required: + - body + description: Indicates a successful call. + '400': + description: Indicates an invalid schema. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given id does not exist. + summary: Schedule a snooze for the rule + tags: + - alerting /api/alerting/rule/{rule_id}/alert/{alert_id}/_mute: post: operationId: post-alerting-rule-rule-id-alert-alert-id-mute From 04b2cf9b41232882387b5cbc1b0674d62db97c5c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 28 Feb 2025 14:08:49 +0000 Subject: [PATCH 16/32] lint fix --- x-pack/test/alerting_api_integration/common/lib/alert_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 0b3800b5426f8..ed3479790389c 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -8,11 +8,11 @@ import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; import { AlertsFilter } from '@kbn/alerting-plugin/common/rule'; import { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; +import { SnoozeBody } from '@kbn/alerting-plugin/common/routes/rule/apis/snooze'; import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; import { getTestRuleData } from './get_test_rule_data'; -import { SnoozeBody } from '@kbn/alerting-plugin/common/routes/rule/apis/snooze'; export interface AlertUtilsOpts { user?: User; From ee44c6deb65e420e4aae4c74c5dac5a9db454c05 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Mon, 3 Mar 2025 18:17:05 +0000 Subject: [PATCH 17/32] Feedback 1 --- .../common/routes/schedule/schema/v1.test.ts | 190 ++++++++++++++++++ .../common/routes/schedule/schema/v1.ts | 21 +- .../validation/validate_end_date/v1.test.ts | 4 +- .../validation/validate_end_date/v1.ts | 4 +- .../validation/validate_start_date/v1.test.ts | 6 +- .../validation/validate_start_date/v1.ts | 4 +- .../validation/validate_timezone/v1.test.ts | 6 +- .../validation/validate_timezone/v1.ts | 2 +- .../validation/validation_schedule/v1.test.ts | 26 ++- .../validation/validation_schedule/v1.ts | 4 +- 10 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts new file mode 100644 index 0000000000000..11048c4a4673d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts @@ -0,0 +1,190 @@ +/* + * 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 { scheduleRequestSchema } from './v1'; + +const recurring = { + every: '1d', + onWeekDay: ['TU', 'TH'], + onMonthDay: [1, 31], + onMonth: [1, 3, 5, 12], + end: '2021-05-20T00:00:00.000Z', + occurrences: 25, +}; + +const defaultSchedule = { + start: '2021-05-10T00:00:00.000Z', + duration: '2h', + timezone: 'Europe/London', + recurring, +}; + +describe('scheduleRequestSchema', () => { + const mockCurrentDate = new Date('2021-05-05T00:00:00.000Z'); + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers().setSystemTime(mockCurrentDate); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('validates correctly with all fields', () => { + expect(scheduleRequestSchema.validate(defaultSchedule)).toMatchInlineSnapshot(` + Object { + "duration": "2h", + "recurring": Object { + "end": "2021-05-20T00:00:00.000Z", + "every": "1d", + "occurrences": 25, + "onMonth": Array [ + 1, + 3, + 5, + 12, + ], + "onMonthDay": Array [ + 1, + 31, + ], + "onWeekDay": Array [ + "TU", + "TH", + ], + }, + "start": "2021-05-10T00:00:00.000Z", + "timezone": "Europe/London", + } + `); + }); + + it('validates correctly without recurring fields', () => { + expect( + scheduleRequestSchema.validate({ + start: defaultSchedule.start, + duration: defaultSchedule.duration, + }) + ).toMatchInlineSnapshot(` + Object { + "duration": "2h", + "start": "2021-05-10T00:00:00.000Z", + } + `); + }); + + it('validates correctly with few of recurring fields', () => { + expect( + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { every: '1d', onWeekDay: ['TU'] }, + }) + ).toMatchInlineSnapshot(` + Object { + "duration": "2h", + "recurring": Object { + "every": "1d", + "onWeekDay": Array [ + "TU", + ], + }, + "start": "2021-05-10T00:00:00.000Z", + "timezone": "Europe/London", + } + `); + }); + + it(`throws an error when onWeekDay is empty`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onWeekDay: [] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onWeekDay]: array size is [0], but cannot be smaller than [1]"` + ); + }); + + it(`throws an error when onMonthDay is empty`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonthDay: [] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonthDay]: array size is [0], but cannot be smaller than [1]"` + ); + }); + + it(`throws an error when onMonthDay is less than 1`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonthDay: [0] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonthDay.0]: Value must be equal to or greater than [1]."` + ); + }); + + it(`throws an error when onMonthDay is more than 31`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonthDay: [32] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonthDay.0]: Value must be equal to or lower than [31]."` + ); + }); + + it(`throws an error when onMonth is empty`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonth: [] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonth]: array size is [0], but cannot be smaller than [1]"` + ); + }); + + it(`throws an error when onMonth is less than 1`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonth: [0] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonth.0]: Value must be equal to or greater than [1]."` + ); + }); + + it(`throws an error when onMonth is more than 12`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonth: [13] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonth.0]: Value must be equal to or lower than [12]."` + ); + }); + + it(`throws an error when occurrences is less than 0`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, occurrences: -1 }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.occurrences]: Value must be equal to or greater than [1]."` + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index fa2cd360cac5f..ffab171c69df2 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -21,20 +21,20 @@ export const scheduleRequestSchema = schema.object( start: schema.string({ validate: validateStartDateV1, meta: { - description: 'Start date and time of the snooze schedule in ISO 8601 format.', + description: 'Start date and time of the schedule in ISO 8601 format.', }, }), duration: schema.string({ validate: validateDurationV1, meta: { - description: 'Duration of the snooze schedule.', + description: `Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.`, }, }), timezone: schema.maybe( schema.string({ validate: validateTimezoneV1, meta: { - description: 'Timezone of the snooze schedule.', + description: 'Timezone of the schedule. The default timezone is UTC.', }, }) ), @@ -44,7 +44,7 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateEndDateV1, meta: { - description: 'End date of recurrence of the snooze schedule in ISO 8601 format.', + description: 'End date of recurrence of the schedule in ISO 8601 format.', }, }) ), @@ -52,7 +52,8 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateIntervalAndFrequencyV1, meta: { - description: 'Recurrence interval and frequency of the snooze schedule.', + description: + 'Recurrence interval and frequency of the schedule. It allows values like format. is one of d|w|M|y for days, weeks, months, years. Example: 5d, 2w, 3m, 1y.', }, }) ), @@ -61,8 +62,7 @@ export const scheduleRequestSchema = schema.object( minSize: 1, validate: validateOnWeekDayV1, meta: { - description: - 'Specific days of the week or nth day of month for recurrence of the snooze schedule.', + description: `Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.`, }, }) ), @@ -70,7 +70,8 @@ export const scheduleRequestSchema = schema.object( schema.arrayOf(schema.number({ min: 1, max: 31 }), { minSize: 1, meta: { - description: 'Specific days of the month for recurrence of the snooze schedule.', + description: + 'Specific days of the month for recurrence of the schedule. Valid values are 1-31.', }, }) ), @@ -78,7 +79,7 @@ export const scheduleRequestSchema = schema.object( schema.arrayOf(schema.number({ min: 1, max: 12 }), { minSize: 1, meta: { - description: 'Specific months for recurrence of the snooze schedule.', + description: 'Specific months for recurrence of the schedule. Valid values are 1-12.', }, }) ), @@ -91,7 +92,7 @@ export const scheduleRequestSchema = schema.object( }, min: 1, meta: { - description: 'Total number of recurrences of the snooze schedule.', + description: 'Total number of recurrences of the schedule.', }, }) ), diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts index d8f82753f6d4b..72bb4e8e28f45 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.test.ts @@ -24,13 +24,13 @@ describe('validateEndDate', () => { }); it('throws error for invalid date', () => { - expect(validateEndDate('invalid-date')).toEqual('Invalid snooze end date: invalid-date'); + expect(validateEndDate('invalid-date')).toEqual('Invalid schedule end date: invalid-date'); }); it('throws error for past date', () => { const pastDate = '2024-12-31T23:59:59.999Z'; expect(validateEndDate(pastDate)).toBe( - `Invalid snooze schedule end date as it is in the past: ${pastDate}` + `Invalid schedule end date as it is in the past: ${pastDate}` ); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts index 0de6ca889b697..444915b10b45b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_end_date/v1.ts @@ -11,10 +11,10 @@ export const validateEndDate = (date: string) => { const parsedValue = Date.parse(date); if (isNaN(parsedValue)) { - return `Invalid snooze end date: ${date}`; + return `Invalid schedule end date: ${date}`; } if (parsedValue <= Date.now()) { - return `Invalid snooze schedule end date as it is in the past: ${date}`; + return `Invalid schedule end date as it is in the past: ${date}`; } if (!ISO_DATE_REGEX.test(date)) { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts index a129f0fe7c979..8056b6dda892c 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.test.ts @@ -18,18 +18,18 @@ describe('validateStartDate', () => { }); it('throws error for invalid date', () => { - expect(validateStartDate('invalid-date')).toEqual('Invalid snooze start date: invalid-date'); + expect(validateStartDate('invalid-date')).toEqual('Invalid schedule start date: invalid-date'); }); it('throws error when not ISO format', () => { expect(validateStartDate('Feb 18 2025')).toEqual( - 'Invalid start date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + 'Invalid schedule start date format: Feb 18 2025. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' ); }); it('throws error for start date with offset', () => { expect(validateStartDate('2025-02-10T21:30:00.000+05:30')).toEqual( - 'Invalid start date format: 2025-02-10T21:30:00.000+05:30. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' + 'Invalid schedule start date format: 2025-02-10T21:30:00.000+05:30. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ' ); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts index 954739b5cca7b..b8d434213aa58 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_start_date/v1.ts @@ -11,10 +11,10 @@ export const validateStartDate = (date: string) => { const parsedValue = Date.parse(date); if (isNaN(parsedValue)) { - return `Invalid snooze start date: ${date}`; + return `Invalid schedule start date: ${date}`; } if (!ISO_DATE_REGEX.test(date)) { - return `Invalid start date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; + return `Invalid schedule start date format: ${date}. Use ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZ`; } return; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts index 9e915760c80c5..8b2d63ce26c9f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.test.ts @@ -18,14 +18,14 @@ describe('validateTimezone', () => { }); it('throws error for invalid combination', () => { - expect(validateTimezone('Europe/India')).toEqual('Invalid snooze timezone: Europe/India'); + expect(validateTimezone('Europe/India')).toEqual('Invalid schedule timezone: Europe/India'); }); it('throws error for invalid timezone', () => { - expect(validateTimezone('invalid')).toEqual('Invalid snooze timezone: invalid'); + expect(validateTimezone('invalid')).toEqual('Invalid schedule timezone: invalid'); }); it('throws error for empty string', () => { - expect(validateTimezone(' ')).toEqual('Invalid snooze timezone: '); + expect(validateTimezone(' ')).toEqual('Invalid schedule timezone: '); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts index 0163dcb51ad82..fda2a55613f9b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_timezone/v1.ts @@ -9,7 +9,7 @@ import moment from 'moment-timezone'; export const validateTimezone = (timezone?: string) => { if (timezone && moment.tz.zone(timezone) == null) { - return `Invalid snooze timezone: ${timezone}`; + return `Invalid schedule timezone: ${timezone}`; } return; }; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts index c9bf0a0603f39..252e69f6d1437 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -24,16 +24,34 @@ describe('validateSchedule', () => { duration: '2000m', recurring: { every: '1d' }, }) - ).toEqual('Recurrence every 1d must be longer than the snooze duration 2000m'); + ).toEqual('Recurrence every 1d must be longer than the duration 2000m'); }); it('throws error when duration in hours greater than interval', () => { expect( validateSchedule({ - duration: '700h', + duration: '169h', recurring: { every: '1w' }, }) - ).toEqual('Recurrence every 1w must be longer than the snooze duration 700h'); + ).toEqual('Recurrence every 1w must be longer than the duration 169h'); + }); + + it('throws error when duration in hours greater than interval in months', () => { + expect( + validateSchedule({ + duration: '740h', + recurring: { every: '1M' }, + }) + ).toEqual('Recurrence every 1M must be longer than the duration 740h'); + }); + + it('throws error when duration in hours greater than interval in years', () => { + expect( + validateSchedule({ + duration: '8761h', + recurring: { every: '1y' }, + }) + ).toEqual('Recurrence every 1y must be longer than the duration 8761h'); }); it('throws error when recurring schedule provided with indefinite duration', () => { @@ -43,7 +61,7 @@ describe('validateSchedule', () => { recurring: { every: '1M' }, }) ).toEqual( - 'The duration of -1 snoozes the rule indefinitely. Recurring schedules cannot be set when the duration is -1.' + 'The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.' ); }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts index ff02f3801344b..cb9a02bb08f4e 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts @@ -26,12 +26,12 @@ export const validateSchedule = (schedule: { .asDays(); if (intervalInDays && interval && durationInDays >= intervalInDays) { - return `Recurrence every ${recurring?.every} must be longer than the snooze duration ${duration}`; + return `Recurrence every ${recurring?.every} must be longer than the duration ${duration}`; } } if (duration === '-1' && recurring) { - return `The duration of -1 snoozes the rule indefinitely. Recurring schedules cannot be set when the duration is -1.`; + return `The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.`; } return; From 4e95eff7ba1b6651df2946c93eac6e6720ad86cd Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Mon, 3 Mar 2025 18:20:56 +0000 Subject: [PATCH 18/32] fix typo --- .../shared/alerting/common/routes/schedule/schema/v1.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index ffab171c69df2..9cfec135c5458 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -52,8 +52,7 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateIntervalAndFrequencyV1, meta: { - description: - 'Recurrence interval and frequency of the schedule. It allows values like format. is one of d|w|M|y for days, weeks, months, years. Example: 5d, 2w, 3m, 1y.', + description: `Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.`, }, }) ), From abb2f4e9b5c068ff1d73c1b01a63989d91734054 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:36:57 +0000 Subject: [PATCH 19/32] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 36 ++++++++++++++++----------------- oas_docs/bundle.serverless.json | 36 ++++++++++++++++----------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 410924a949adf..e4eedf2357bc7 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4402,27 +4402,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the snooze schedule.", + "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "description": "End date of recurrence of the schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", + "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", + "description": "Total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", + "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4432,7 +4432,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", + "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4442,7 +4442,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", "items": { "type": "string" }, @@ -4453,11 +4453,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "description": "Start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the snooze schedule.", + "description": "Timezone of the schedule. The default timezone is UTC.", "type": "string" } }, @@ -4496,27 +4496,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the snooze schedule.", + "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "description": "End date of recurrence of the schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", + "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", + "description": "Total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", + "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4526,7 +4526,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", + "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4536,7 +4536,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", "items": { "type": "string" }, @@ -4547,11 +4547,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "description": "Start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the snooze schedule.", + "description": "Timezone of the schedule. The default timezone is UTC.", "type": "string" } }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index d028f0a3a5fab..4408b8f386139 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4402,27 +4402,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the snooze schedule.", + "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "description": "End date of recurrence of the schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", + "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", + "description": "Total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", + "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4432,7 +4432,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", + "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4442,7 +4442,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", "items": { "type": "string" }, @@ -4453,11 +4453,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "description": "Start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the snooze schedule.", + "description": "Timezone of the schedule. The default timezone is UTC.", "type": "string" } }, @@ -4496,27 +4496,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the snooze schedule.", + "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the snooze schedule in ISO 8601 format.", + "description": "End date of recurrence of the schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the snooze schedule.", + "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the snooze schedule.", + "description": "Total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the snooze schedule.", + "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4526,7 +4526,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the snooze schedule.", + "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4536,7 +4536,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week or nth day of month for recurrence of the snooze schedule.", + "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", "items": { "type": "string" }, @@ -4547,11 +4547,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the snooze schedule in ISO 8601 format.", + "description": "Start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the snooze schedule.", + "description": "Timezone of the schedule. The default timezone is UTC.", "type": "string" } }, From 31c1058e63624f7c1cae9562acd37a10e515a813 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:53:29 +0000 Subject: [PATCH 20/32] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 36 +++++++++++++------------- oas_docs/output/kibana.yaml | 36 +++++++++++++------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 3260966511610..8ce3a362e9d93 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -3436,24 +3436,24 @@ paths: type: object properties: duration: - description: Duration of the snooze schedule. + description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the snooze schedule in ISO 8601 format. + description: End date of recurrence of the schedule in ISO 8601 format. type: string every: - description: Recurrence interval and frequency of the snooze schedule. + description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' type: string occurrences: - description: Total number of recurrences of the snooze schedule. + description: Total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the snooze schedule. + description: Specific months for recurrence of the schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3461,7 +3461,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the snooze schedule. + description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3469,16 +3469,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the snooze schedule in ISO 8601 format. + description: Start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the snooze schedule. + description: Timezone of the schedule. The default timezone is UTC. type: string required: - start @@ -3506,24 +3506,24 @@ paths: type: object properties: duration: - description: Duration of the snooze schedule. + description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the snooze schedule in ISO 8601 format. + description: End date of recurrence of the schedule in ISO 8601 format. type: string every: - description: Recurrence interval and frequency of the snooze schedule. + description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' type: string occurrences: - description: Total number of recurrences of the snooze schedule. + description: Total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the snooze schedule. + description: Specific months for recurrence of the schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3531,7 +3531,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the snooze schedule. + description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3539,16 +3539,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the snooze schedule in ISO 8601 format. + description: Start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the snooze schedule. + description: Timezone of the schedule. The default timezone is UTC. type: string required: - start diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 74adcf375268a..3249291c92e0f 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -3781,24 +3781,24 @@ paths: type: object properties: duration: - description: Duration of the snooze schedule. + description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the snooze schedule in ISO 8601 format. + description: End date of recurrence of the schedule in ISO 8601 format. type: string every: - description: Recurrence interval and frequency of the snooze schedule. + description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' type: string occurrences: - description: Total number of recurrences of the snooze schedule. + description: Total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the snooze schedule. + description: Specific months for recurrence of the schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3806,7 +3806,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the snooze schedule. + description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3814,16 +3814,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the snooze schedule in ISO 8601 format. + description: Start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the snooze schedule. + description: Timezone of the schedule. The default timezone is UTC. type: string required: - start @@ -3851,24 +3851,24 @@ paths: type: object properties: duration: - description: Duration of the snooze schedule. + description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the snooze schedule in ISO 8601 format. + description: End date of recurrence of the schedule in ISO 8601 format. type: string every: - description: Recurrence interval and frequency of the snooze schedule. + description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' type: string occurrences: - description: Total number of recurrences of the snooze schedule. + description: Total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the snooze schedule. + description: Specific months for recurrence of the schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3876,7 +3876,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the snooze schedule. + description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3884,16 +3884,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week or nth day of month for recurrence of the snooze schedule. + description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the snooze schedule in ISO 8601 format. + description: Start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the snooze schedule. + description: Timezone of the schedule. The default timezone is UTC. type: string required: - start From 23a1b2cbdad50d16ad60d9c24e200b62a7b31c44 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 4 Mar 2025 17:56:57 +0000 Subject: [PATCH 21/32] add check for end and occurrences --- .../common/routes/schedule/schema/v1.test.ts | 35 +++++++++++++- .../common/routes/schedule/schema/v1.ts | 48 ++++++++++++------- .../validation/validate_integer/v1.test.ts | 22 +++++++++ .../validation/validate_integer/v1.ts | 12 +++++ .../validation/validation_schedule/v1.test.ts | 17 +++++-- .../validation/validation_schedule/v1.ts | 13 +++-- 6 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts index 11048c4a4673d..4c86e2dc6b8d5 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.test.ts @@ -13,7 +13,6 @@ const recurring = { onMonthDay: [1, 31], onMonth: [1, 3, 5, 12], end: '2021-05-20T00:00:00.000Z', - occurrences: 25, }; const defaultSchedule = { @@ -43,7 +42,6 @@ describe('scheduleRequestSchema', () => { "recurring": Object { "end": "2021-05-20T00:00:00.000Z", "every": "1d", - "occurrences": 25, "onMonth": Array [ 1, 3, @@ -144,6 +142,17 @@ describe('scheduleRequestSchema', () => { ); }); + it(`throws an error when onMonthDay is not an integer`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonthDay: [25.5] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonthDay.0]: schedule onMonthDay must be a positive integer."` + ); + }); + it(`throws an error when onMonth is empty`, async () => { expect(() => scheduleRequestSchema.validate({ @@ -177,6 +186,17 @@ describe('scheduleRequestSchema', () => { ); }); + it(`throws an error when onMonth is not an integer`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, onMonth: [3.2] }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.onMonth.0]: schedule onMonth must be a positive integer."` + ); + }); + it(`throws an error when occurrences is less than 0`, async () => { expect(() => scheduleRequestSchema.validate({ @@ -187,4 +207,15 @@ describe('scheduleRequestSchema', () => { `"[recurring.occurrences]: Value must be equal to or greater than [1]."` ); }); + + it(`throws an error when occurrences is not an integer`, async () => { + expect(() => + scheduleRequestSchema.validate({ + ...defaultSchedule, + recurring: { ...recurring, occurrences: 1.5 }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[recurring.occurrences]: schedule occurrences must be a positive integer."` + ); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 9cfec135c5458..03c501fc79637 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -15,6 +15,7 @@ import { validateTimezoneV1, validateScheduleV1, } from '../validation'; +import { validateInteger } from '../validation/validate_integer/v1'; export const scheduleRequestSchema = schema.object( { @@ -66,29 +67,40 @@ export const scheduleRequestSchema = schema.object( }) ), onMonthDay: schema.maybe( - schema.arrayOf(schema.number({ min: 1, max: 31 }), { - minSize: 1, - meta: { - description: - 'Specific days of the month for recurrence of the schedule. Valid values are 1-31.', - }, - }) + schema.arrayOf( + schema.number({ + min: 1, + max: 31, + validate: (value: number) => validateInteger(value, 'onMonthDay'), + }), + { + minSize: 1, + meta: { + description: + 'Specific days of the month for recurrence of the schedule. Valid values are 1-31.', + }, + } + ) ), onMonth: schema.maybe( - schema.arrayOf(schema.number({ min: 1, max: 12 }), { - minSize: 1, - meta: { - description: 'Specific months for recurrence of the schedule. Valid values are 1-12.', - }, - }) + schema.arrayOf( + schema.number({ + min: 1, + max: 12, + validate: (value: number) => validateInteger(value, 'onMonth'), + }), + { + minSize: 1, + meta: { + description: + 'Specific months for recurrence of the schedule. Valid values are 1-12.', + }, + } + ) ), occurrences: schema.maybe( schema.number({ - validate: (occurrences: number) => { - if (!Number.isInteger(occurrences)) { - return 'schedule occurrences must be a positive integer'; - } - }, + validate: (occurrences: number) => validateInteger(occurrences, 'occurrences'), min: 1, meta: { description: 'Total number of recurrences of the schedule.', diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.test.ts new file mode 100644 index 0000000000000..938872e239b8a --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { validateInteger } from './v1'; + +describe('validateInteger', () => { + it('validates integer correctly', () => { + expect(validateInteger(5, 'foo')).toBeUndefined(); + }); + + it('throws error for non integer', () => { + expect(validateInteger(4.5, 'foo')).toEqual('schedule foo must be a positive integer.'); + }); + + it('throws error for string', () => { + // @ts-expect-error: testing invalid params + expect(validateInteger('7', 'foo')).toEqual('schedule foo must be a positive integer.'); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.ts new file mode 100644 index 0000000000000..85b93bcdc944f --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_integer/v1.ts @@ -0,0 +1,12 @@ +/* + * 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 validateInteger = (value: number, fieldName: string) => { + if (!Number.isInteger(value)) { + return `schedule ${fieldName} must be a positive integer.`; + } +}; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts index 252e69f6d1437..23ad564b604d3 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -24,7 +24,7 @@ describe('validateSchedule', () => { duration: '2000m', recurring: { every: '1d' }, }) - ).toEqual('Recurrence every 1d must be longer than the duration 2000m'); + ).toEqual('Recurrence every 1d must be longer than the duration 2000m.'); }); it('throws error when duration in hours greater than interval', () => { @@ -33,7 +33,7 @@ describe('validateSchedule', () => { duration: '169h', recurring: { every: '1w' }, }) - ).toEqual('Recurrence every 1w must be longer than the duration 169h'); + ).toEqual('Recurrence every 1w must be longer than the duration 169h.'); }); it('throws error when duration in hours greater than interval in months', () => { @@ -42,7 +42,7 @@ describe('validateSchedule', () => { duration: '740h', recurring: { every: '1M' }, }) - ).toEqual('Recurrence every 1M must be longer than the duration 740h'); + ).toEqual('Recurrence every 1M must be longer than the duration 740h.'); }); it('throws error when duration in hours greater than interval in years', () => { @@ -51,7 +51,7 @@ describe('validateSchedule', () => { duration: '8761h', recurring: { every: '1y' }, }) - ).toEqual('Recurrence every 1y must be longer than the duration 8761h'); + ).toEqual('Recurrence every 1y must be longer than the duration 8761h.'); }); it('throws error when recurring schedule provided with indefinite duration', () => { @@ -64,4 +64,13 @@ describe('validateSchedule', () => { 'The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.' ); }); + + it('throws error when recurring end and occurrences both are provided', () => { + expect( + validateSchedule({ + duration: '5h', + recurring: { every: '1w', end: '2021-05-10T00:00:00.000Z', occurrences: 5 }, + }) + ).toEqual(`Only one of 'end' or 'occurrences' can be set for recurring schedules.`); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts index cb9a02bb08f4e..7cac24fc5fe8f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts @@ -10,11 +10,12 @@ import { DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants'; export const validateSchedule = (schedule: { duration: string; - recurring?: { every?: string }; + recurring?: { every?: string; end?: string; occurrences?: number }; }) => { const { duration, recurring } = schedule; - if (recurring?.every && duration !== '-1') { - const [, interval, frequency] = recurring?.every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; + const { end, occurrences, every } = recurring ?? {}; + if (every && duration !== '-1') { + const [, interval, frequency] = every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; const intervalInDays = moment @@ -26,7 +27,7 @@ export const validateSchedule = (schedule: { .asDays(); if (intervalInDays && interval && durationInDays >= intervalInDays) { - return `Recurrence every ${recurring?.every} must be longer than the duration ${duration}`; + return `Recurrence every ${every} must be longer than the duration ${duration}.`; } } @@ -34,5 +35,9 @@ export const validateSchedule = (schedule: { return `The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.`; } + if (end && occurrences) { + return `Only one of 'end' or 'occurrences' can be set for recurring schedules.`; + } + return; }; From 190edbbd93ec163b235703f155bd4d7099cf8022 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 6 Mar 2025 17:10:21 +0000 Subject: [PATCH 22/32] remove clear snooze integration test, add days in duration regex --- .../common/routes/schedule/constants.ts | 2 +- .../validation/validate_duration/v1.test.ts | 4 + .../validation/validation_schedule/v1.test.ts | 9 ++ .../tests/alerting/group4/snooze.ts | 95 +------------------ 4 files changed, 16 insertions(+), 94 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts index a94e6f3133eb4..3b8d8b6da21d4 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -7,6 +7,6 @@ export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; export const WEEKDAY_REGEX = '^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'; -export const DURATION_REGEX = /(\d+)(s|m|h)/; +export const DURATION_REGEX = /(\d+)(s|m|h|d)/; export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|M|y)/; export const DEFAULT_TIMEZONE = 'UTC'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts index be656f5455b70..8196569c8e58f 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts @@ -19,6 +19,10 @@ describe('validateDuration', () => { expect(validateDuration('1000s')).toBeUndefined(); }); + it('validates duration in days correctly', () => { + expect(validateDuration('5d')).toBeUndefined(); + }); + it('validates indefinite duration correctly', () => { expect(validateDuration('-1')).toBeUndefined(); }); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts index 23ad564b604d3..edeceb9e935a9 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -54,6 +54,15 @@ describe('validateSchedule', () => { ).toEqual('Recurrence every 1y must be longer than the duration 8761h.'); }); + it('throws error when duration in days and greater than interval', () => { + expect( + validateSchedule({ + duration: '2d', + recurring: { every: '1d' }, + }) + ).toEqual('Recurrence every 1d must be longer than the duration 2d.'); + }); + it('throws error when recurring schedule provided with indefinite duration', () => { expect( validateSchedule({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts index e22b36607fa3c..2b9629b0e8195 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts @@ -369,97 +369,6 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext }); }); - describe('clear the snooze after it expires', function () { - this.tags('skipFIPS'); - it('should clear the snooze after it expires', async () => { - const { body: createdConnector } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY Connector', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); - - const { body: createdRule } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'should not trigger actions when snoozed', - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - notify_when: 'onActiveAlert', - params: { - pattern: { instance: arrayOfTrues(100) }, - }, - actions: [ - { - id: createdConnector.id, - group: 'default', - params: {}, - }, - ], - }) - ) - .expect(200); - objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - - const dateStart = new Date().toISOString(); - const snooze = { - schedule: { - custom: { - start: dateStart, - duration: '3s', - }, - }, - }; - - const response = await alertUtils.getSnoozeRequest(createdRule.id).send(snooze); - - expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - schedule: { - ...snooze.schedule, - id: response.body.schedule.id, - custom: { - ...snooze.schedule.custom, - timezone: 'UTC', - }, - }, - }); - - await retry.try(async () => { - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([ - { duration: 3000, rRule: { dtstart: dateStart, tzid: 'UTC' } }, - ]); - }); - log.info('wait for snoozing to end'); - await retry.try(async () => { - const { body: alertWithExpiredSnooze } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(alertWithExpiredSnooze.snooze_schedule).to.eql([]); - }); - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdRule.id, - }); - }); - }); - describe('validation', function () { this.tags('skipFIPS'); it('should return 400 if the start is not valid', async () => { @@ -486,7 +395,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(400); expect(response.body.message).to.eql( - '[request body.schedule.custom.start]: Invalid snooze start date: invalid' + '[request body.schedule.custom.start]: Invalid schedule start date: invalid' ); }); @@ -656,7 +565,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(400); expect(response.body.message).to.eql( - '[request body.schedule.custom.recurring.end]: Invalid snooze end date: invalid' + '[request body.schedule.custom.recurring.end]: Invalid schedule end date: invalid' ); }); From 054f4a577a4d81a29c6b5e13c3b836370b7d0766 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Mon, 10 Mar 2025 12:59:03 +0000 Subject: [PATCH 23/32] remove indefinite duration, use common snooze rule method --- .../alerting/common/routes/schedule/index.ts | 8 +- .../{request => custom_to_rrule}/latest.ts | 0 .../{request => custom_to_rrule}/v1.test.ts | 19 --- .../{request => custom_to_rrule}/v1.ts | 4 - .../{response => rrule_to_custom}/latest.ts | 0 .../{response => rrule_to_custom}/v1.test.ts | 12 -- .../{response => rrule_to_custom}/v1.ts | 5 +- .../validation/validate_duration/v1.test.ts | 4 +- .../validation/validate_duration/v1.ts | 3 - .../validation/validation_schedule/v1.test.ts | 11 -- .../validation/validation_schedule/v1.ts | 6 +- .../snooze/external/snooze_rule.test.ts | 81 ------------ .../rule/methods/snooze/external/types.ts | 16 --- .../application/rule/methods/snooze/index.ts | 7 +- .../methods/snooze/internal/snooze_rule.ts | 112 ---------------- .../snooze/{internal => }/schemas/index.ts | 0 .../schemas/snooze_rule_body_schema.ts | 2 +- .../schemas/snooze_rule_params_schema.ts | 0 .../snooze/{internal => }/snooze_rule.test.ts | 2 +- .../snooze/{external => }/snooze_rule.ts | 56 ++++---- .../snooze/{internal => }/types/index.ts | 0 .../types/snooze_rule_options.ts | 2 +- .../snooze/external/snooze_rule_route.test.ts | 125 ++++++------------ .../apis/snooze/external/snooze_rule_route.ts | 30 ++--- .../snooze/internal/snooze_rule_route.test.ts | 49 +++++-- .../apis/snooze/internal/snooze_rule_route.ts | 2 +- .../alerting/server/rules_client.mock.ts | 1 - .../server/rules_client/common/index.ts | 1 - .../rules_client/common/snooze_utils.ts | 16 --- .../server/rules_client/rules_client.ts | 10 +- .../group4/tests/alerting/snooze.ts | 105 +-------------- .../tests/alerting/group4/snooze.ts | 107 ++++----------- 32 files changed, 169 insertions(+), 627 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{request => custom_to_rrule}/latest.ts (100%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{request => custom_to_rrule}/v1.test.ts (90%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{request => custom_to_rrule}/v1.ts (97%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{response => rrule_to_custom}/latest.ts (100%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{response => rrule_to_custom}/v1.test.ts (90%) rename x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/{response => rrule_to_custom}/v1.ts (94%) delete mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts delete mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts delete mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/schemas/index.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/schemas/snooze_rule_body_schema.ts (88%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/schemas/snooze_rule_params_schema.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/snooze_rule.test.ts (97%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{external => }/snooze_rule.ts (65%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/types/index.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/{internal => }/types/snooze_rule_options.ts (88%) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts index 0c7cbd5c51af2..35cc110e0fee4 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/index.ts @@ -6,11 +6,11 @@ */ export { scheduleRequestSchema } from './schema/latest'; -export { transformCustomScheduleToRRule } from './transforms/request/latest'; -export { transformRRuleToCustomSchedule } from './transforms/response/latest'; +export { transformCustomScheduleToRRule } from './transforms/custom_to_rrule/latest'; +export { transformRRuleToCustomSchedule } from './transforms/rrule_to_custom/latest'; export type { ScheduleRequest } from './types/latest'; export { scheduleRequestSchema as scheduleRequestSchemaV1 } from './schema/v1'; -export { transformCustomScheduleToRRule as transformCustomScheduleToRRuleV1 } from './transforms/request/v1'; -export { transformRRuleToCustomSchedule as transformRRuleToCustomScheduleV1 } from './transforms/response/v1'; +export { transformCustomScheduleToRRule as transformCustomScheduleToRRuleV1 } from './transforms/custom_to_rrule/v1'; +export { transformRRuleToCustomSchedule as transformRRuleToCustomScheduleV1 } from './transforms/rrule_to_custom/v1'; export type { ScheduleRequest as ScheduleRequestV1 } from './types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.test.ts similarity index 90% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.test.ts index b0c47e1628010..f74e19a55500a 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.test.ts @@ -27,25 +27,6 @@ describe('transformCustomScheduleToRRule', () => { }); }); - it('transforms duration as indefinite correctly', () => { - expect( - transformCustomScheduleToRRule({ duration: '-1', start: '2021-05-10T00:00:00.000Z' }) - ).toEqual({ - duration: -1, - rRule: { - bymonth: undefined, - bymonthday: undefined, - byweekday: undefined, - count: undefined, - dtstart: '2021-05-10T00:00:00.000Z', - freq: undefined, - interval: undefined, - tzid: 'UTC', - until: undefined, - }, - }); - }); - it('transforms start date and tzid correctly', () => { expect( transformCustomScheduleToRRule({ diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts similarity index 97% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts index 4936809724536..394cc26e87d52 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/request/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts @@ -27,10 +27,6 @@ const transformEveryToFrequency = (frequency?: string) => { }; const getDurationInMilliseconds = (duration: string): number => { - if (duration === '-1') { - return -1; - } - const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; return moment diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts similarity index 90% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts index 8796df43d0482..f8c88354bbf48 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts @@ -20,18 +20,6 @@ describe('transformRRuleToCustomSchedule', () => { ).toEqual({ duration: '2h', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); }); - it('transforms to duration as indefinite correctly', () => { - expect( - transformRRuleToCustomSchedule({ - duration: -1, - rRule: { - dtstart: '2021-05-10T00:00:00.000Z', - tzid: 'UTC', - }, - }) - ).toEqual({ duration: '-1', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); - }); - it('transforms to start date and timezone correctly', () => { expect( transformRRuleToCustomSchedule({ diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts similarity index 94% rename from x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts index 7e7e2682ab8b5..5729c16d9bc22 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/response/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts @@ -27,8 +27,9 @@ const transformFrequencyToEvery = (frequency: Frequency) => { }; const getDurationInString = (duration: number): string => { - if (duration === -1) { - return '-1'; + const durationInDays = moment.duration(duration, 'milliseconds').asDays(); + if (durationInDays > 1) { + return `${durationInDays}d`; } const durationInHours = moment.duration(duration, 'milliseconds').asHours(); diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts index 8196569c8e58f..08661ee815d78 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.test.ts @@ -23,8 +23,8 @@ describe('validateDuration', () => { expect(validateDuration('5d')).toBeUndefined(); }); - it('validates indefinite duration correctly', () => { - expect(validateDuration('-1')).toBeUndefined(); + it('throws error when duration is -1', () => { + expect(validateDuration('-1')).toEqual('Invalid schedule duration format: -1'); }); it('throws error when invalid unit', () => { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts index 3d6fa5be35e6f..9487212a3538b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_duration/v1.ts @@ -10,9 +10,6 @@ import { DURATION_REGEX } from '../../constants'; export const validateDuration = (duration: string) => { const durationRegexp = new RegExp(DURATION_REGEX, 'g'); - if (duration === '-1') { - return; - } if (!durationRegexp.test(duration)) { return `Invalid schedule duration format: ${duration}`; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts index edeceb9e935a9..2d1727f3fccb0 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.test.ts @@ -63,17 +63,6 @@ describe('validateSchedule', () => { ).toEqual('Recurrence every 1d must be longer than the duration 2d.'); }); - it('throws error when recurring schedule provided with indefinite duration', () => { - expect( - validateSchedule({ - duration: '-1', - recurring: { every: '1M' }, - }) - ).toEqual( - 'The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.' - ); - }); - it('throws error when recurring end and occurrences both are provided', () => { expect( validateSchedule({ diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts index 7cac24fc5fe8f..0cef6a8a5d828 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validation_schedule/v1.ts @@ -14,7 +14,7 @@ export const validateSchedule = (schedule: { }) => { const { duration, recurring } = schedule; const { end, occurrences, every } = recurring ?? {}; - if (every && duration !== '-1') { + if (every) { const [, interval, frequency] = every?.match(INTERVAL_FREQUENCY_REGEXP) ?? []; const [, durationNumber, durationUnit] = duration.match(DURATION_REGEX) ?? []; @@ -31,10 +31,6 @@ export const validateSchedule = (schedule: { } } - if (duration === '-1' && recurring) { - return `The duration of -1 represents indefinite schedule. Recurring schedules cannot be set when the duration is -1.`; - } - if (end && occurrences) { return `Only one of 'end' or 'occurrences' can be set for recurring schedules.`; } diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts deleted file mode 100644 index d2055644d23b9..0000000000000 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.test.ts +++ /dev/null @@ -1,81 +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. - */ - -import { RulesClientContext } from '../../../../../rules_client'; -import { snoozeRule } from './snooze_rule'; -import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks'; -import type { SnoozeRule } 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?: SnoozeRule['snoozeSchedule'] -): SnoozeRule['snoozeSchedule'] => { - return { - duration: 28800000, - rRule: { - dtstart: '2010-09-19T11:49:59.329Z', - count: 1, - tzid: 'UTC', - }, - ...override, - }; -}; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts deleted file mode 100644 index 253ed4b82f31b..0000000000000 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/types.ts +++ /dev/null @@ -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. - */ - -import type { RRule } from '../../../../r_rule/types'; - -export interface SnoozeRule { - id: string; - snoozeSchedule: { - duration: number; - rRule: RRule; - }; -} diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts index 242a5cacf0b82..31f63f3aa4dcc 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export type { SnoozeRuleOptions } from './internal/types'; -export type { SnoozeRule } from './external/types'; -export { snoozeRule as snoozeRuleInternal } from './internal/snooze_rule'; -export { snoozeRule } from './external/snooze_rule'; +export type { SnoozeRuleOptions } from './types'; + +export { snoozeRule } from './snooze_rule'; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts deleted file mode 100644 index 19dc83db41d79..0000000000000 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.ts +++ /dev/null @@ -1,112 +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. - */ - -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'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../../../../authorization'; -import { retryIfConflicts } from '../../../../../lib/retry_if_conflicts'; -import { validateSnoozeStartDate } from '../../../../../lib/validate_snooze_date'; -import { RuleMutedError } from '../../../../../lib/errors/rule_muted'; -import { RulesClientContext } from '../../../../../rules_client/types'; -import { - getInternalSnoozeAttributes, - verifySnoozeAttributeScheduleLimit, -} from '../../../../../rules_client/common'; -import { updateRuleSo } from '../../../../../data/rule'; -import { updateMetaAttributes } from '../../../../../rules_client/lib/update_meta_attributes'; -import { snoozeRuleParamsSchema } from './schemas'; -import type { SnoozeRuleOptions } from './types'; - -export async function snoozeRule( - context: RulesClientContext, - { id, snoozeSchedule }: SnoozeRuleOptions -): Promise { - try { - snoozeRuleParamsSchema.validate({ id }); - ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule }); - } catch (error) { - throw Boom.badRequest(`Error validating snooze - ${error.message}`); - } - const snoozeDateValidationMsg = validateSnoozeStartDate(snoozeSchedule.rRule.dtstart); - if (snoozeDateValidationMsg) { - throw new RuleMutedError(snoozeDateValidationMsg); - } - - return await retryIfConflicts( - context.logger, - `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, - async () => await snoozeWithOCC(context, { id, snoozeSchedule }) - ); -} - -async function snoozeWithOCC( - context: RulesClientContext, - { id, snoozeSchedule }: SnoozeRuleOptions -) { - const { attributes, version } = await withSpan( - { name: 'getRuleSavedObject', type: 'rules' }, - () => - getRuleSavedObject(context, { - ruleId: id, - }) - ); - - try { - await context.authorization.ensureAuthorized({ - ruleTypeId: attributes.alertTypeId, - consumer: attributes.consumer, - operation: WriteOperations.Snooze, - entity: AlertingAuthorizationEntity.Rule, - }); - - if (attributes.actions.length) { - await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' }); - } - } catch (error) { - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - savedObject: { type: RULE_SAVED_OBJECT_TYPE, id, name: attributes.name }, - error, - }) - ); - throw error; - } - - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.SNOOZE, - outcome: 'unknown', - savedObject: { type: RULE_SAVED_OBJECT_TYPE, id, name: attributes.name }, - }) - ); - - context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - - const newAttrs = getInternalSnoozeAttributes(attributes, snoozeSchedule); - - try { - verifySnoozeAttributeScheduleLimit(newAttrs); - } catch (error) { - throw Boom.badRequest(error.message); - } - - await updateRuleSo({ - savedObjectsClient: context.unsecuredSavedObjectsClient, - savedObjectsUpdateOptions: { version }, - id, - updateRuleAttributes: updateMetaAttributes(context, { - ...newAttrs, - updatedBy: await context.getUserName(), - updatedAt: new Date().toISOString(), - }), - }); -} diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/index.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/index.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts similarity index 88% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts index 7b7b85dfa4fe8..180e69fb33ad1 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_body_schema.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_body_schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../../common/routes/rule/request'; +import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../common/routes/rule/request'; export const snoozeRuleBodySchema = schema.object({ snoozeSchedule: ruleSnoozeScheduleRequestSchema, diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_params_schema.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_params_schema.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/schemas/snooze_rule_params_schema.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/schemas/snooze_rule_params_schema.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts similarity index 97% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts index 29e56a7782a40..9e86aeab24fc4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/snooze_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RulesClientContext } from '../../../../../rules_client'; +import { RulesClientContext } from '../../../../rules_client'; import { snoozeRule } from './snooze_rule'; import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks'; import { SnoozeRuleOptions } from './types'; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts similarity index 65% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts index 0abeb96dcb451..7a1490a4c84a8 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/external/snooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts @@ -6,40 +6,35 @@ */ import Boom from '@hapi/boom'; -import { v4 as uuidV4 } from 'uuid'; import { withSpan } from '@kbn/apm-utils'; -import { RULE_SAVED_OBJECT_TYPE } from '../../../../../saved_objects'; -import { getRuleSavedObject } from '../../../../../rules_client/lib'; -import { ruleAuditEvent, RuleAuditAction } from '../../../../../rules_client/common/audit_events'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../../../../authorization'; -import { retryIfConflicts } from '../../../../../lib/retry_if_conflicts'; -import { validateSnoozeStartDate } from '../../../../../lib/validate_snooze_date'; -import { RuleMutedError } from '../../../../../lib/errors/rule_muted'; -import { RulesClientContext } from '../../../../../rules_client/types'; -import { RawRule, SanitizedRule } from '../../../../../types'; +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'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; +import { retryIfConflicts } from '../../../../lib/retry_if_conflicts'; +import { validateSnoozeStartDate } from '../../../../lib/validate_snooze_date'; +import { RuleMutedError } from '../../../../lib/errors/rule_muted'; +import { RulesClientContext } from '../../../../rules_client/types'; +import { RawRule, SanitizedRule } from '../../../../types'; import { getSnoozeAttributes, verifySnoozeAttributeScheduleLimit, -} from '../../../../../rules_client/common'; -import { updateRuleSo } from '../../../../../data/rule'; -import { updateMetaAttributes } from '../../../../../rules_client/lib/update_meta_attributes'; -import { RuleParams } from '../../../types'; -import { - transformRuleDomainToRule, - transformRuleAttributesToRuleDomain, -} from '../../../transforms'; -import { snoozeParamsSchema } from '../../../../../../common/routes/rule/apis/snooze'; -import { ruleSnoozeScheduleSchema } from '../../../../../../common/routes/rule/request'; -import type { SnoozeRule } from './types'; +} from '../../../../rules_client/common'; +import { updateRuleSo } from '../../../../data/rule'; +import { updateMetaAttributes } from '../../../../rules_client/lib/update_meta_attributes'; +import { RuleParams } from '../../types'; +import { transformRuleDomainToRule, transformRuleAttributesToRuleDomain } from '../../transforms'; +import { snoozeRuleParamsSchema } from './schemas'; +import type { SnoozeRuleOptions } from './types'; export async function snoozeRule( context: RulesClientContext, - { id, snoozeSchedule }: SnoozeRule + { id, snoozeSchedule }: SnoozeRuleOptions ): Promise> { - const snoozeScheduleId = uuidV4(); try { - snoozeParamsSchema.validate({ id }); - ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule, id: snoozeScheduleId }); + snoozeRuleParamsSchema.validate({ id }); + ruleSnoozeScheduleSchema.validate({ ...snoozeSchedule }); } catch (error) { throw Boom.badRequest(`Error validating snooze - ${error.message}`); } @@ -51,14 +46,14 @@ export async function snoozeRule( return await retryIfConflicts( context.logger, `rulesClient.snooze('${id}', ${JSON.stringify(snoozeSchedule, null, 4)})`, - async () => await snoozeWithOCC(context, { id, snoozeSchedule, snoozeScheduleId }) + async () => await snoozeWithOCC(context, { id, snoozeSchedule }) ); } async function snoozeWithOCC( context: RulesClientContext, - { id, snoozeSchedule, snoozeScheduleId }: SnoozeRule & { snoozeScheduleId: string } -): Promise> { + { id, snoozeSchedule }: SnoozeRuleOptions +) { const { attributes, version } = await withSpan( { name: 'getRuleSavedObject', type: 'rules' }, () => @@ -99,10 +94,7 @@ async function snoozeWithOCC( context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); - const newAttrs = getSnoozeAttributes(attributes, { - ...snoozeSchedule, - id: snoozeScheduleId, - }); + const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); try { verifySnoozeAttributeScheduleLimit(newAttrs); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/index.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/index.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/index.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts similarity index 88% rename from x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts rename to x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts index 3d0c506bc157e..77d077b6ee0e5 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/internal/types/snooze_rule_options.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/types/snooze_rule_options.ts @@ -5,7 +5,7 @@ * 2.0. */ import { TypeOf } from '@kbn/config-schema'; -import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../../common/routes/rule/request'; +import { ruleSnoozeScheduleSchema as ruleSnoozeScheduleRequestSchema } from '../../../../../../common/routes/rule/request'; export interface SnoozeRuleOptions { id: string; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index cc97c8b3ac2d4..f790f7e49e3ca 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -10,21 +10,22 @@ import { licenseStateMock } from '../../../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; +import { SanitizedRule } from '../../../../../../common'; import { snoozeRuleRoute } from './snooze_rule_route'; -import { SanitizedRule } from '@kbn/alerting-types'; const rulesClient = rulesClientMock.create(); +const mockedUUID = 'schedule-id-1'; jest.mock('../../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); -beforeEach(() => { - jest.resetAllMocks(); -}); +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('schedule-id-1'), +})); const schedule = { custom: { - duration: '240h', + duration: '10d', start: '2021-03-07T00:00:00.000Z', recurring: { occurrences: 1, @@ -52,7 +53,7 @@ const mockedRule = { snoozeSchedule: [ { duration: 864000000, - id: 'random-schedule-id', + id: mockedUUID, rRule: { count: 1, tzid: 'UTC', @@ -65,6 +66,10 @@ const mockedRule = { rulesClient.update.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule); describe('snoozeAlertRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('snoozes an alert', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); @@ -112,78 +117,7 @@ describe('snoozeAlertRoute', () => { expect(res.ok).toHaveBeenCalledWith({ body: { - schedule: { custom: { ...schedule.custom, timezone: 'UTC' }, id: 'random-schedule-id' }, - }, - }); - }); - - it('also snoozes an alert when passed snoozeEndTime of -1', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - snoozeRuleRoute(router, licenseState); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`); - - rulesClient.snooze.mockResolvedValueOnce({ - ...mockedRule, - snoozeSchedule: [ - { - duration: -1, - id: 'random-schedule-id', - rRule: { - count: 1, - tzid: 'UTC', - dtstart: '2021-03-07T00:00:00.000Z', - }, - }, - ], - } as unknown as SanitizedRule); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - body: { - schedule: { - custom: { - ...schedule.custom, - duration: '-1', - }, - }, - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.snooze).toHaveBeenCalledTimes(1); - expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.duration).toEqual(-1); - expect(rulesClient.snooze.mock.calls[0][0].snoozeSchedule.rRule).toMatchInlineSnapshot(` - Object { - "bymonth": undefined, - "bymonthday": undefined, - "byweekday": undefined, - "count": 1, - "dtstart": "2021-03-07T00:00:00.000Z", - "freq": undefined, - "interval": undefined, - "tzid": "UTC", - "until": undefined, - } - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: { - schedule: { - custom: { ...schedule.custom, timezone: 'UTC', duration: '-1' }, - id: 'random-schedule-id', - }, + schedule: { custom: { ...schedule.custom, timezone: 'UTC' }, id: mockedUUID }, }, }); }); @@ -203,7 +137,7 @@ describe('snoozeAlertRoute', () => { snoozeSchedule: [ { duration: 864000000, - id: 'random-schedule-id', + id: mockedUUID, rRule: { tzid: 'America/New_York', dtstart: '2021-03-07T00:00:00.000Z', @@ -225,7 +159,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { custom: { - duration: '240h', + duration: '10d', start: '2021-03-07T00:00:00.000Z', timezone: 'America/New_York', recurring: { @@ -263,7 +197,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { custom: { - duration: '240h', + duration: '10d', start: '2021-03-07T00:00:00.000Z', timezone: 'America/New_York', recurring: { @@ -272,7 +206,7 @@ describe('snoozeAlertRoute', () => { onWeekDay: ['MO'], }, }, - id: 'random-schedule-id', + id: mockedUUID, }, }, }); @@ -293,7 +227,7 @@ describe('snoozeAlertRoute', () => { snoozeSchedule: [ { duration: 864000000, - id: 'random-schedule-id', + id: mockedUUID, rRule: { count: 5, dtstart: '2021-03-07T00:00:00.000Z', @@ -316,7 +250,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { custom: { - duration: '240h', + duration: '10d', start: '2021-03-07T00:00:00.000Z', recurring: { every: '1y', @@ -362,7 +296,7 @@ describe('snoozeAlertRoute', () => { body: { schedule: { custom: { - duration: '240h', + duration: '10d', start: '2021-03-07T00:00:00.000Z', recurring: { every: '1y', @@ -372,12 +306,31 @@ describe('snoozeAlertRoute', () => { }, timezone: 'UTC', }, - id: 'random-schedule-id', + id: mockedUUID, }, }, }); }); + it('throws error when body does not include custom schedule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { params: { id: '1' }, body: { schedule: { duration: '1h' } } }, + ['ok', 'forbidden'] + ); + + await expect(handler(context, req, res)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Custom schedule is required"` + ); + }); + it('ensures the rule type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 5d3dd548641b9..640a7753497b5 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import { v4 } from 'uuid'; import { IRouter } from '@kbn/core/server'; import { type SnoozeParams, @@ -22,7 +23,7 @@ import { transformCustomScheduleToRRule, transformRRuleToCustomSchedule, } from '../../../../../../common/routes/schedule'; -import type { SnoozeRule } from '../../../../../application/rule/methods/snooze'; +import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; export const snoozeRuleRoute = ( router: IRouter, @@ -72,29 +73,28 @@ export const snoozeRuleRoute = ( const { rRule, duration } = transformCustomScheduleToRRule(customSchedule); - const snoozeSchedule = { - duration, - rRule: rRule as SnoozeRule['snoozeSchedule']['rRule'], - }; + const snoozeScheduleId = v4(); try { const snoozedRule = await rulesClient.snooze({ ...params, - snoozeSchedule, + snoozeSchedule: { + duration, + rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], + id: snoozeScheduleId, + }, }); - const createdSchedule = { - id: (snoozedRule?.snoozeSchedule?.length - ? snoozedRule?.snoozeSchedule[snoozedRule.snoozeSchedule.length - 1].id - : '') as string, - custom: transformRRuleToCustomSchedule( - snoozedRule?.snoozeSchedule?.[snoozedRule.snoozeSchedule.length - 1] - ), - }; + const createdSchedule = snoozedRule.snoozeSchedule?.find( + (schedule) => schedule.id === snoozeScheduleId + ); const response: SnoozeResponse = { body: { - schedule: createdSchedule, + schedule: { + id: snoozeScheduleId, + custom: transformRRuleToCustomSchedule(createdSchedule), + }, }, }; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts index 6d88a37ad52b0..5f3c4eb7905df 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { snoozeRuleRoute } from './snooze_rule_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; +import { SanitizedRule } from '../../../../../../common'; +import { snoozeRuleRoute } from './snooze_rule_route'; const rulesClient = rulesClientMock.create(); jest.mock('../../../../../lib/license_api_access', () => ({ @@ -30,6 +31,36 @@ const SNOOZE_SCHEDULE = { duration: 864000000, }; +const mockedRule = { + apiKeyOwner: 'api-key-owner', + consumer: 'bar', + createdBy: 'elastic', + updatedBy: 'elastic', + enabled: true, + id: '1', + name: 'abc', + alertTypeId: '1', + tags: ['foo'], + throttle: '10m', + schedule: { interval: '12s' }, + params: { + otherField: false, + }, + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + snoozeSchedule: [ + { + duration: 864000000, + id: 'random-schedule-id', + rRule: { + count: 1, + tzid: 'UTC', + dtstart: '2021-03-07T00:00:00.000Z', + }, + }, + ], +}; + describe('snoozeAlertRoute', () => { it('snoozes an alert', async () => { const licenseState = licenseStateMock.create(); @@ -41,7 +72,7 @@ describe('snoozeAlertRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); - rulesClient.snoozeInternal.mockResolvedValueOnce(); + rulesClient.snooze.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -58,8 +89,8 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.snoozeInternal).toHaveBeenCalledTimes(1); - expect(rulesClient.snoozeInternal.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -88,7 +119,7 @@ describe('snoozeAlertRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); - rulesClient.snoozeInternal.mockResolvedValueOnce(); + rulesClient.snooze.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -108,8 +139,8 @@ describe('snoozeAlertRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.snoozeInternal).toHaveBeenCalledTimes(1); - expect(rulesClient.snoozeInternal.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -136,9 +167,7 @@ describe('snoozeAlertRoute', () => { const [, handler] = router.post.mock.calls[0]; - rulesClient.snoozeInternal.mockRejectedValue( - new RuleTypeDisabledError('Fail', 'license_invalid') - ); + rulesClient.snooze.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ 'ok', diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts index 4345f48d6d8f8..a40d79f5f067d 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts @@ -40,7 +40,7 @@ export const snoozeRuleRoute = ( const params: SnoozeRuleRequestInternalParamsV1 = req.params; const body = transformSnoozeBodyV1(req.body); try { - await rulesClient.snoozeInternal({ ...params, ...body }); + await rulesClient.snooze({ ...params, ...body }); return res.noContent(); } catch (e) { if (e instanceof RuleMutedError) { diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts index 5c72afdc277b0..5bf7ed4dee82b 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client.mock.ts @@ -51,7 +51,6 @@ const createRulesClientMock = () => { bulkEnableRules: jest.fn(), bulkDisableRules: jest.fn(), snooze: jest.fn(), - snoozeInternal: jest.fn(), unsnooze: jest.fn(), runSoon: jest.fn(), clone: jest.fn(), diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts index aeaeecce4a35e..a695e0a8bd6e9 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/index.ts @@ -32,7 +32,6 @@ export { includeFieldsRequiredForAuthentication } from './include_fields_require export { getAndValidateCommonBulkOptions } from './get_and_validate_common_bulk_options'; export { getSnoozeAttributes, - getInternalSnoozeAttributes, getBulkSnooze, getUnsnoozeAttributes, getBulkUnsnooze, diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts index d2fa99e1c31f1..6e8e08bb3827f 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/common/snooze_utils.ts @@ -19,22 +19,6 @@ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleDom // If duration is -1, instead mute all const { id: snoozeId, duration } = snoozeSchedule; - return { - ...(duration === -1 ? { muteAll: true } : {}), - snoozeSchedule: (snoozeId - ? clearScheduledSnoozesAttributesById(attributes, [snoozeId]) - : clearUnscheduledSnoozeAttributes(attributes) - ).concat(snoozeSchedule), - }; -} - -export function getInternalSnoozeAttributes( - attributes: RawRule, - snoozeSchedule: RuleDomainSnoozeSchedule -) { - // If duration is -1, instead mute all - const { id: snoozeId, duration } = snoozeSchedule; - if (duration === -1) { return { muteAll: true, diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts index 292c8a01c6332..f8c6b92aee9b5 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts @@ -14,12 +14,7 @@ import { RulesClientContext } from './types'; import { cloneRule, CloneRuleParams } from '../application/rule/methods/clone'; import { createRule, CreateRuleParams } from '../application/rule/methods/create'; import { updateRule, UpdateRuleParams } from '../application/rule/methods/update'; -import { - snoozeRule, - snoozeRuleInternal, - type SnoozeRuleOptions, - type SnoozeRule, -} from '../application/rule/methods/snooze'; +import { snoozeRule, type SnoozeRuleOptions } from '../application/rule/methods/snooze'; import { unsnoozeRule, UnsnoozeParams } from '../application/rule/methods/unsnooze'; import { getRule, GetRuleParams } from '../application/rule/methods/get'; import { resolveRule, ResolveParams } from '../application/rule/methods/resolve'; @@ -183,8 +178,7 @@ export class RulesClient { public disableRule = (params: DisableRuleParams) => disableRule(this.context, params); public enableRule = (params: EnableRuleParams) => enableRule(this.context, params); - public snoozeInternal = (options: SnoozeRuleOptions) => snoozeRuleInternal(this.context, options); - public snooze = (options: SnoozeRule) => snoozeRule(this.context, options); + public snooze = (options: SnoozeRuleOptions) => snoozeRule(this.context, options); public unsnooze = (options: UnsnoozeParams) => unsnoozeRule(this.context, options); public muteAll = (options: { id: string }) => muteAll(this.context, options); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts index 0c817ae8ab4f3..292c36091f59b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts @@ -108,6 +108,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, + duration: '10d', timezone: 'UTC', }, }, @@ -177,6 +178,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, + duration: '10d', timezone: 'UTC', }, }, @@ -246,6 +248,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, + duration: '10d', timezone: 'UTC', }, }, @@ -311,6 +314,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, + duration: '10d', timezone: 'UTC', }, }, @@ -337,107 +341,6 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - - it('should handle snooze rule request appropriately when duration is -1', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - enabled: false, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ) - .expect(200); - objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - - const response = await alertUtils.getSnoozeRequest(createdAlert.id).send({ - schedule: { - custom: { - duration: '-1', - start: NOW, - }, - }, - }); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all at space2': - case 'global_read at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getUnauthorizedErrorMessage('snooze', 'test.noop', 'alertsFixture'), - statusCode: 403, - }); - break; - case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to execute actions`, - statusCode: 403, - }); - break; - case 'superuser at space1': - case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - schedule: { - custom: { - duration: '-1', - start: NOW, - timezone: 'UTC', - }, - id: response.body.schedule.id, - }, - }); - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([ - { - duration: -1, - id: response.body.schedule.id, - rRule: { - dtstart: NOW, - tzid: 'UTC', - }, - }, - ]); - expect(updatedAlert.mute_all).to.eql(true); - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: space.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts index 2b9629b0e8195..cd754beb7b0f9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts @@ -87,6 +87,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, + duration: '10d', timezone: 'UTC', }, }, @@ -110,84 +111,6 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext }); }); - describe('handle snooze rule request appropriately when duration is -1', function () { - this.tags('skipFIPS'); - it('should handle snooze rule request appropriately when duration is -1', async () => { - const { body: createdConnector } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY Connector', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdConnector.id, 'connector', 'actions'); - - const { body: createdRule } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - enabled: false, - actions: [ - { - id: createdConnector.id, - group: 'default', - params: {}, - }, - ], - }) - ) - .expect(200); - objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - - const response = await alertUtils.getSnoozeRequest(createdRule.id).send({ - schedule: { - custom: { - duration: '-1', - start: NOW, - }, - }, - }); - - expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - schedule: { - id: response.body.schedule.id, - custom: { - duration: '-1', - start: NOW, - timezone: 'UTC', - }, - }, - }); - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.snooze_schedule).to.eql([ - { - duration: -1, - id: response.body.schedule.id, - rRule: { - dtstart: NOW, - tzid: 'UTC', - }, - }, - ]); - expect(updatedAlert.mute_all).to.eql(true); - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdRule.id, - }); - }); - }); - it('should not trigger actions when snoozed', async () => { const { body: createdConnector, status: connStatus } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) @@ -427,6 +350,34 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext ); }); + it('should return 400 if the duration is -1', 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({ + schedule: { + custom: { + start: snoozeSchedule.schedule.custom.start, + duration: '-1', + }, + }, + }); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + '[request body.schedule.custom.duration]: Invalid schedule duration format: -1' + ); + }); + it('should return 400 if the every is not valid', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) From 16ee5a5f68ede6500b1eb70ffc3cd035324de4f6 Mon Sep 17 00:00:00 2001 From: lcawl Date: Mon, 10 Mar 2025 16:28:01 -0700 Subject: [PATCH 24/32] Edit descriptions --- oas_docs/bundle.json | 37 ++++++++++--------- oas_docs/bundle.serverless.json | 37 ++++++++++--------- oas_docs/output/kibana.serverless.yaml | 37 ++++++++++--------- oas_docs/output/kibana.yaml | 37 ++++++++++--------- .../common/routes/schedule/schema/v1.ts | 18 ++++----- .../apis/snooze/external/snooze_rule_route.ts | 5 +++ 6 files changed, 90 insertions(+), 81 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index ebc327302511e..069b3f4435dc2 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4367,6 +4367,7 @@ }, "/api/alerting/rule/{id}/snooze_schedule": { "post": { + "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.", "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { @@ -4402,27 +4403,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", + "description": "The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the schedule.", + "description": "The total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", + "description": "The specific months for a recurring schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4432,7 +4433,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", + "description": "The specific days of the month for a recurring schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4442,7 +4443,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", + "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4453,11 +4454,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the schedule. The default timezone is UTC.", + "description": "The timezone of the schedule. The default timezone is UTC.", "type": "string" } }, @@ -4496,27 +4497,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", + "description": "The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the schedule.", + "description": "The total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", + "description": "The specific months for a recurring schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4526,7 +4527,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", + "description": "The specific days of the month for a recurring schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4536,7 +4537,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", + "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4547,11 +4548,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the schedule. The default timezone is UTC.", + "description": "The timezone of the schedule. The default timezone is UTC.", "type": "string" } }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index c637def1899bb..dd161ac312c3e 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4367,6 +4367,7 @@ }, "/api/alerting/rule/{id}/snooze_schedule": { "post": { + "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.", "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { @@ -4402,27 +4403,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", + "description": "The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the schedule.", + "description": "The total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", + "description": "The specific months for a recurring schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4432,7 +4433,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", + "description": "The specific days of the month for a recurring schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4442,7 +4443,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", + "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4453,11 +4454,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the schedule. The default timezone is UTC.", + "description": "The timezone of the schedule. The default timezone is UTC.", "type": "string" } }, @@ -4496,27 +4497,27 @@ "additionalProperties": false, "properties": { "duration": { - "description": "Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "End date of recurrence of the schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule in ISO 8601 format.", "type": "string" }, "every": { - "description": "Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.", + "description": "The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.", "type": "string" }, "occurrences": { - "description": "Total number of recurrences of the schedule.", + "description": "The total number of recurrences of the schedule.", "minimum": 1, "type": "number" }, "onMonth": { - "description": "Specific months for recurrence of the schedule. Valid values are 1-12.", + "description": "The specific months for a recurring schedule. Valid values are 1-12.", "items": { "maximum": 12, "minimum": 1, @@ -4526,7 +4527,7 @@ "type": "array" }, "onMonthDay": { - "description": "Specific days of the month for recurrence of the schedule. Valid values are 1-31.", + "description": "The specific days of the month for a recurring schedule. Valid values are 1-31.", "items": { "maximum": 31, "minimum": 1, @@ -4536,7 +4537,7 @@ "type": "array" }, "onWeekDay": { - "description": "Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.", + "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4547,11 +4548,11 @@ "type": "object" }, "start": { - "description": "Start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule in ISO 8601 format.", "type": "string" }, "timezone": { - "description": "Timezone of the schedule. The default timezone is UTC.", + "description": "The timezone of the schedule. The default timezone is UTC.", "type": "string" } }, diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 868093d595b91..9b1a201830ff0 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -3407,6 +3407,7 @@ paths: x-beta: true /api/alerting/rule/{id}/snooze_schedule: post: + description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes. operationId: post-alerting-rule-id-snooze-schedule parameters: - description: A required header to protect against CSRF attacks @@ -3438,24 +3439,24 @@ paths: type: object properties: duration: - description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the schedule in ISO 8601 format. + description: The end date of a recurring schedule in ISO 8601 format. type: string every: - description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' + description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' type: string occurrences: - description: Total number of recurrences of the schedule. + description: The total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the schedule. Valid values are 1-12. + description: The specific months for a recurring schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3463,7 +3464,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. + description: The specific days of the month for a recurring schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3471,16 +3472,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. + description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the schedule in ISO 8601 format. + description: The start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the schedule. The default timezone is UTC. + description: The timezone of the schedule. The default timezone is UTC. type: string required: - start @@ -3508,24 +3509,24 @@ paths: type: object properties: duration: - description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the schedule in ISO 8601 format. + description: The end date of a recurring schedule in ISO 8601 format. type: string every: - description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' + description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' type: string occurrences: - description: Total number of recurrences of the schedule. + description: The total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the schedule. Valid values are 1-12. + description: The specific months for a recurring schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3533,7 +3534,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. + description: The specific days of the month for a recurring schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3541,16 +3542,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. + description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the schedule in ISO 8601 format. + description: The start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the schedule. The default timezone is UTC. + description: The timezone of the schedule. The default timezone is UTC. type: string required: - start diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 638c566bcd040..2ae024a7127c8 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -3752,6 +3752,7 @@ paths: - alerting /api/alerting/rule/{id}/snooze_schedule: post: + description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes. operationId: post-alerting-rule-id-snooze-schedule parameters: - description: A required header to protect against CSRF attacks @@ -3783,24 +3784,24 @@ paths: type: object properties: duration: - description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the schedule in ISO 8601 format. + description: The end date of a recurring schedule in ISO 8601 format. type: string every: - description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' + description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' type: string occurrences: - description: Total number of recurrences of the schedule. + description: The total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the schedule. Valid values are 1-12. + description: The specific months for a recurring schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3808,7 +3809,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. + description: The specific days of the month for a recurring schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3816,16 +3817,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. + description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the schedule in ISO 8601 format. + description: The start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the schedule. The default timezone is UTC. + description: The timezone of the schedule. The default timezone is UTC. type: string required: - start @@ -3853,24 +3854,24 @@ paths: type: object properties: duration: - description: 'Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: ''5h'', ''30m'', ''5000s''.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: End date of recurrence of the schedule in ISO 8601 format. + description: The end date of a recurring schedule in ISO 8601 format. type: string every: - description: 'Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: ''15d'', ''2w'', ''3m'', ''1y''.' + description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' type: string occurrences: - description: Total number of recurrences of the schedule. + description: The total number of recurrences of the schedule. minimum: 1 type: number onMonth: - description: Specific months for recurrence of the schedule. Valid values are 1-12. + description: The specific months for a recurring schedule. Valid values are 1-12. items: maximum: 12 minimum: 1 @@ -3878,7 +3879,7 @@ paths: minItems: 1 type: array onMonthDay: - description: Specific days of the month for recurrence of the schedule. Valid values are 1-31. + description: The specific days of the month for a recurring schedule. Valid values are 1-31. items: maximum: 31 minimum: 1 @@ -3886,16 +3887,16 @@ paths: minItems: 1 type: array onWeekDay: - description: Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule. + description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: Start date and time of the schedule in ISO 8601 format. + description: The start date and time of the schedule in ISO 8601 format. type: string timezone: - description: Timezone of the schedule. The default timezone is UTC. + description: The timezone of the schedule. The default timezone is UTC. type: string required: - start diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 03c501fc79637..35c1792f038f8 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -22,20 +22,20 @@ export const scheduleRequestSchema = schema.object( start: schema.string({ validate: validateStartDateV1, meta: { - description: 'Start date and time of the schedule in ISO 8601 format.', + description: 'The start date and time of the schedule in ISO 8601 format.', }, }), duration: schema.string({ validate: validateDurationV1, meta: { - description: `Duration of the schedule. It allows values in format. is one of h|m|s for hours, minutes, seconds. Examples: '5h', '30m', '5000s'.`, + description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.', }, }), timezone: schema.maybe( schema.string({ validate: validateTimezoneV1, meta: { - description: 'Timezone of the schedule. The default timezone is UTC.', + description: 'The timezone of the schedule. The default timezone is UTC.', }, }) ), @@ -45,7 +45,7 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateEndDateV1, meta: { - description: 'End date of recurrence of the schedule in ISO 8601 format.', + description: 'The end date of a recurring schedule in ISO 8601 format.', }, }) ), @@ -53,7 +53,7 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateIntervalAndFrequencyV1, meta: { - description: `Recurrence interval and frequency of the schedule. It allows values in format. is one of d|w|M|y for days, weeks, months, years. Example: '15d', '2w', '3m', '1y'.`, + description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.', }, }) ), @@ -62,7 +62,7 @@ export const scheduleRequestSchema = schema.object( minSize: 1, validate: validateOnWeekDayV1, meta: { - description: `Specific days of the week ['MO','TU'...] or nth day of month ['+1MO', '-3FR', '+2WE', '-4SA'] for recurrence of the schedule.`, + description: 'The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.', }, }) ), @@ -77,7 +77,7 @@ export const scheduleRequestSchema = schema.object( minSize: 1, meta: { description: - 'Specific days of the month for recurrence of the schedule. Valid values are 1-31.', + 'The specific days of the month for a recurring schedule. Valid values are 1-31.', }, } ) @@ -93,7 +93,7 @@ export const scheduleRequestSchema = schema.object( minSize: 1, meta: { description: - 'Specific months for recurrence of the schedule. Valid values are 1-12.', + 'The specific months for a recurring schedule. Valid values are 1-12.', }, } ) @@ -103,7 +103,7 @@ export const scheduleRequestSchema = schema.object( validate: (occurrences: number) => validateInteger(occurrences, 'occurrences'), min: 1, meta: { - description: 'Total number of recurrences of the schedule.', + description: 'The total number of recurrences of the schedule.', }, }) ), diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 640a7753497b5..993488d3bc6d7 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -36,7 +36,12 @@ export const snoozeRuleRoute = ( options: { access: 'public', summary: 'Schedule a snooze for the rule', + description: 'When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.', tags: ['oas-tag:alerting'], + availability: { + since: '8.19.0', + stability: 'stable', + }, }, validate: { request: { From 3cec82b3a597cd88473f69b0b3e612aef0599bd6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:49:46 +0000 Subject: [PATCH 25/32] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../alerting/common/routes/schedule/schema/v1.ts | 12 +++++++----- .../rule/apis/snooze/external/snooze_rule_route.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 35c1792f038f8..661fd0f09529a 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -28,7 +28,8 @@ export const scheduleRequestSchema = schema.object( duration: schema.string({ validate: validateDurationV1, meta: { - description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.', + description: + 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.', }, }), timezone: schema.maybe( @@ -53,7 +54,8 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateIntervalAndFrequencyV1, meta: { - description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.', + description: + 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.', }, }) ), @@ -62,7 +64,8 @@ export const scheduleRequestSchema = schema.object( minSize: 1, validate: validateOnWeekDayV1, meta: { - description: 'The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.', + description: + 'The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.', }, }) ), @@ -92,8 +95,7 @@ export const scheduleRequestSchema = schema.object( { minSize: 1, meta: { - description: - 'The specific months for a recurring schedule. Valid values are 1-12.', + description: 'The specific months for a recurring schedule. Valid values are 1-12.', }, } ) diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 993488d3bc6d7..008e5c5f52100 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -36,7 +36,8 @@ export const snoozeRuleRoute = ( options: { access: 'public', summary: 'Schedule a snooze for the rule', - description: 'When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.', + description: + 'When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.', tags: ['oas-tag:alerting'], availability: { since: '8.19.0', From 7ada4958d6f8ab975122ad8383a4532eda9a5d2c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 12 Mar 2025 10:08:40 +0000 Subject: [PATCH 26/32] resolve snooze rule route internal conflict --- .../rule/apis/snooze/internal/snooze_rule_route.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts index f7c497d1264fb..cd2f7375a9617 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts @@ -11,12 +11,11 @@ import { snoozeBodyInternalSchema, snoozeParamsInternalSchema, } from '../../../../../../common/routes/rule/apis/snooze'; -import { type ILicenseState, RuleMutedError } from '../../../../../lib'; +import type { ILicenseState } from '../../../../../lib'; +import { RuleMutedError } from '../../../../../lib'; import { verifyAccessAndContext } from '../../../../lib'; -import { - type AlertingRequestHandlerContext, - INTERNAL_ALERTING_SNOOZE_RULE, -} from '../../../../../types'; +import type { AlertingRequestHandlerContext } from '../../../../../types'; +import { INTERNAL_ALERTING_SNOOZE_RULE } from '../../../../../types'; import { transformSnoozeBodyV1 } from './transforms'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; From af5ca48a4be99a21f6e3a8d07c91ce777e7a4275 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:35:47 +0000 Subject: [PATCH 27/32] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../common/routes/rule/apis/snooze/external/types/v1.ts | 4 ++-- .../routes/schedule/transforms/custom_to_rrule/v1.ts | 2 +- .../routes/schedule/transforms/rrule_to_custom/v1.ts | 2 +- .../shared/alerting/common/routes/schedule/types/v1.ts | 2 +- .../server/application/rule/methods/snooze/snooze_rule.ts | 4 ++-- .../rule/apis/snooze/external/snooze_rule_route.test.ts | 2 +- .../routes/rule/apis/snooze/external/snooze_rule_route.ts | 8 +++++--- .../rule/apis/snooze/internal/snooze_rule_route.test.ts | 2 +- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts index 6b0abd5f99cfa..651acbb6252ca 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/snooze/external/types/v1.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { TypeOf } from '@kbn/config-schema'; -import { snoozeParamsSchemaV1, snoozeBodySchemaV1, snoozeResponseSchemaV1 } from '../..'; +import type { TypeOf } from '@kbn/config-schema'; +import type { snoozeParamsSchemaV1, snoozeBodySchemaV1, snoozeResponseSchemaV1 } from '../..'; export type SnoozeParams = TypeOf; export type SnoozeBody = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts index 394cc26e87d52..4ebf6b6f7b67a 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { Frequency } from '@kbn/rrule'; import type { RRule } from '../../../../../server/application/r_rule/types'; -import { ScheduleRequest } from '../../types/v1'; +import type { ScheduleRequest } from '../../types/v1'; import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants'; const transformEveryToFrequency = (frequency?: string) => { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts index 5729c16d9bc22..fbd20f2b576f1 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts @@ -9,7 +9,7 @@ import moment from 'moment-timezone'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { Frequency } from '@kbn/rrule'; import type { RRule } from '../../../../../server/application/r_rule/types'; -import { ScheduleRequest } from '../../types/v1'; +import type { ScheduleRequest } from '../../types/v1'; const transformFrequencyToEvery = (frequency: Frequency) => { switch (frequency) { diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts index 20a870fbd7fd1..88e40e164da61 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/types/v1.ts @@ -5,6 +5,6 @@ * 2.0. */ import type { TypeOf } from '@kbn/config-schema'; -import { scheduleRequestSchemaV1 } from '..'; +import type { scheduleRequestSchemaV1 } from '..'; export type ScheduleRequest = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts index f393d194baf5a..0bf61df9d7213 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/snooze/snooze_rule.ts @@ -16,14 +16,14 @@ import { retryIfConflicts } from '../../../../lib/retry_if_conflicts'; import { validateSnoozeStartDate } from '../../../../lib/validate_snooze_date'; import { RuleMutedError } from '../../../../lib/errors/rule_muted'; import type { RulesClientContext } from '../../../../rules_client/types'; -import { RawRule, SanitizedRule } from '../../../../types'; +import type { RawRule, SanitizedRule } from '../../../../types'; import { getSnoozeAttributes, verifySnoozeAttributeScheduleLimit, } from '../../../../rules_client/common'; import { updateRuleSo } from '../../../../data/rule'; import { updateMetaAttributes } from '../../../../rules_client/lib/update_meta_attributes'; -import { RuleParams } from '../../types'; +import type { RuleParams } from '../../types'; import { transformRuleDomainToRule, transformRuleAttributesToRuleDomain } from '../../transforms'; import { snoozeRuleParamsSchema } from './schemas'; import type { SnoozeRuleOptions } from './types'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index f790f7e49e3ca..c85a5cfad94b7 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -10,7 +10,7 @@ import { licenseStateMock } from '../../../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; -import { SanitizedRule } from '../../../../../../common'; +import type { SanitizedRule } from '../../../../../../common'; import { snoozeRuleRoute } from './snooze_rule_route'; const rulesClient = rulesClientMock.create(); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 008e5c5f52100..8037909550e85 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import { v4 } from 'uuid'; -import { IRouter } from '@kbn/core/server'; +import type { IRouter } from '@kbn/core/server'; import { type SnoozeParams, type SnoozeResponse, @@ -15,9 +15,11 @@ import { snoozeParamsSchema, snoozeResponseSchema, } from '../../../../../../common/routes/rule/apis/snooze'; -import { ILicenseState, RuleMutedError } from '../../../../../lib'; +import type { ILicenseState } from '../../../../../lib'; +import { RuleMutedError } from '../../../../../lib'; import { verifyAccessAndContext } from '../../../../lib'; -import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; +import type { AlertingRequestHandlerContext } from '../../../../../types'; +import { BASE_ALERTING_API_PATH } from '../../../../../types'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; import { transformCustomScheduleToRRule, diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts index 5f3c4eb7905df..063bad35d9062 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts @@ -10,7 +10,7 @@ import { licenseStateMock } from '../../../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; -import { SanitizedRule } from '../../../../../../common'; +import type { SanitizedRule } from '../../../../../../common'; import { snoozeRuleRoute } from './snooze_rule_route'; const rulesClient = rulesClientMock.create(); From 461050d8413e774c59e00f8495575ac10b176836 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 12 Mar 2025 12:57:23 +0000 Subject: [PATCH 28/32] feedback 2 --- .../common/routes/schedule/constants.ts | 2 +- .../common/routes/schedule/schema/v1.ts | 10 ++++--- .../schedule/transforms/custom_to_rrule/v1.ts | 4 +-- .../validation/validate_on_weekday/v1.test.ts | 17 +++++++++-- .../snooze/external/snooze_rule_route.test.ts | 4 +-- .../apis/snooze/external/snooze_rule_route.ts | 29 +++++++++---------- .../apis/snooze/internal/snooze_rule_route.ts | 10 +++---- 7 files changed, 44 insertions(+), 32 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts index 3b8d8b6da21d4..32e9cf973211b 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/constants.ts @@ -6,7 +6,7 @@ */ export const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; -export const WEEKDAY_REGEX = '^(((\\+|-)[1-4])?(MO|TU|WE|TH|FR|SA|SU))$'; +export const WEEKDAY_REGEX = '^(((\\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$'; export const DURATION_REGEX = /(\d+)(s|m|h|d)/; export const INTERVAL_FREQUENCY_REGEXP = /(\d+)(d|w|M|y)/; export const DEFAULT_TIMEZONE = 'UTC'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts index 661fd0f09529a..3d680f28c6f5e 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/schema/v1.ts @@ -22,14 +22,15 @@ export const scheduleRequestSchema = schema.object( start: schema.string({ validate: validateStartDateV1, meta: { - description: 'The start date and time of the schedule in ISO 8601 format.', + description: + 'The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.', }, }), duration: schema.string({ validate: validateDurationV1, meta: { description: - 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.', + 'The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.', }, }), timezone: schema.maybe( @@ -46,7 +47,8 @@ export const scheduleRequestSchema = schema.object( schema.string({ validate: validateEndDateV1, meta: { - description: 'The end date of a recurring schedule in ISO 8601 format.', + description: + 'The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.', }, }) ), @@ -65,7 +67,7 @@ export const scheduleRequestSchema = schema.object( validate: validateOnWeekDayV1, meta: { description: - 'The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.', + 'The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.', }, }) ), diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts index 4ebf6b6f7b67a..2ab18334f32f3 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/custom_to_rrule/v1.ts @@ -7,7 +7,7 @@ import moment from 'moment-timezone'; import { Frequency } from '@kbn/rrule'; -import type { RRule } from '../../../../../server/application/r_rule/types'; +import type { RRuleRequestV1 } from '../../../r_rule'; import type { ScheduleRequest } from '../../types/v1'; import { DEFAULT_TIMEZONE, DURATION_REGEX, INTERVAL_FREQUENCY_REGEXP } from '../../constants'; @@ -38,7 +38,7 @@ export const transformCustomScheduleToRRule = ( schedule: ScheduleRequest ): { duration: number; - rRule: RRule | undefined; + rRule: RRuleRequestV1; } => { const { recurring, duration, start, timezone } = schedule; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts index 88d0a621c0b26..9a51ebe4f99a5 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/validation/validate_on_weekday/v1.test.ts @@ -12,7 +12,18 @@ describe('validateOnWeekDay', () => { }); it('validates properly formed onWeekDay strings', () => { - const weekdays = ['+1MO', '+2TU', '+3WE', '+4TH', '-4FR', '-3SA', '-2SU', '-1MO']; + const weekdays = [ + '+1MO', + '+2TU', + '+3WE', + '+4TH', + '+5FR', + '-5SA', + '-4SU', + '-3MO', + '-2TU', + '-1WE', + ]; expect(validateOnWeekDay(weekdays)).toBeUndefined(); }); @@ -24,8 +35,8 @@ describe('validateOnWeekDay', () => { }); it('validates invalid week numbers in onWeekDay strings', () => { - expect(validateOnWeekDay(['+5MO', '+3WE', '-7FR'])).toEqual( - 'Invalid onWeekDay values in recurring schedule: +5MO,-7FR' + expect(validateOnWeekDay(['+15MO', '+3WE', '-7FR'])).toEqual( + 'Invalid onWeekDay values in recurring schedule: +15MO,-7FR' ); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts index c85a5cfad94b7..bbd965c65a33d 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts @@ -70,7 +70,7 @@ describe('snoozeAlertRoute', () => { jest.clearAllMocks(); }); - it('snoozes an alert', async () => { + it('snoozes a rule', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); @@ -327,7 +327,7 @@ describe('snoozeAlertRoute', () => { ); await expect(handler(context, req, res)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Custom schedule is required"` + `"A schedule is required"` ); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index 8037909550e85..c98c5bd8a537b 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -9,11 +9,11 @@ import Boom from '@hapi/boom'; import { v4 } from 'uuid'; import type { IRouter } from '@kbn/core/server'; import { - type SnoozeParams, - type SnoozeResponse, - snoozeBodySchema, - snoozeParamsSchema, - snoozeResponseSchema, + type SnoozeParamsV1, + type SnoozeResponseV1, + snoozeBodySchemaV1, + snoozeParamsSchemaV1, + snoozeResponseSchemaV1, } from '../../../../../../common/routes/rule/apis/snooze'; import type { ILicenseState } from '../../../../../lib'; import { RuleMutedError } from '../../../../../lib'; @@ -25,7 +25,6 @@ import { transformCustomScheduleToRRule, transformRRuleToCustomSchedule, } from '../../../../../../common/routes/schedule'; -import type { SnoozeRuleOptions } from '../../../../../application/rule/methods/snooze'; export const snoozeRuleRoute = ( router: IRouter, @@ -39,7 +38,7 @@ export const snoozeRuleRoute = ( access: 'public', summary: 'Schedule a snooze for the rule', description: - 'When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.', + 'When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time and schedule single or recurring downtimes.', tags: ['oas-tag:alerting'], availability: { since: '8.19.0', @@ -48,12 +47,12 @@ export const snoozeRuleRoute = ( }, validate: { request: { - params: snoozeParamsSchema, - body: snoozeBodySchema, + params: snoozeParamsSchemaV1, + body: snoozeBodySchemaV1, }, response: { 200: { - body: () => snoozeResponseSchema, + body: () => snoozeResponseSchemaV1, description: 'Indicates a successful call.', }, 400: { @@ -72,11 +71,11 @@ export const snoozeRuleRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const alertingContext = await context.alerting; const rulesClient = await alertingContext.getRulesClient(); - const params: SnoozeParams = req.params; + const params: SnoozeParamsV1 = req.params; const customSchedule = req.body.schedule?.custom; if (!customSchedule) { - throw Boom.badRequest('Custom schedule is required'); + throw Boom.badRequest('A schedule is required'); } const { rRule, duration } = transformCustomScheduleToRRule(customSchedule); @@ -85,10 +84,10 @@ export const snoozeRuleRoute = ( try { const snoozedRule = await rulesClient.snooze({ - ...params, + id: params.id, snoozeSchedule: { duration, - rRule: rRule as SnoozeRuleOptions['snoozeSchedule']['rRule'], + rRule, id: snoozeScheduleId, }, }); @@ -97,7 +96,7 @@ export const snoozeRuleRoute = ( (schedule) => schedule.id === snoozeScheduleId ); - const response: SnoozeResponse = { + const response: SnoozeResponseV1 = { body: { schedule: { id: snoozeScheduleId, diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts index cd2f7375a9617..1ae03955de2cb 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts @@ -8,8 +8,8 @@ import type { TypeOf } from '@kbn/config-schema'; import type { IRouter } from '@kbn/core/server'; import { - snoozeBodyInternalSchema, - snoozeParamsInternalSchema, + snoozeBodyInternalSchemaV1, + snoozeParamsInternalSchemaV1, } from '../../../../../../common/routes/rule/apis/snooze'; import type { ILicenseState } from '../../../../../lib'; import { RuleMutedError } from '../../../../../lib'; @@ -19,7 +19,7 @@ import { INTERNAL_ALERTING_SNOOZE_RULE } from '../../../../../types'; import { transformSnoozeBodyV1 } from './transforms'; import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; -export type SnoozeRuleRequestInternalParamsV1 = TypeOf; +export type SnoozeRuleRequestInternalParamsV1 = TypeOf; export const snoozeRuleRoute = ( router: IRouter, @@ -31,8 +31,8 @@ export const snoozeRuleRoute = ( security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'internal' }, validate: { - params: snoozeParamsInternalSchema, - body: snoozeBodyInternalSchema, + params: snoozeParamsInternalSchemaV1, + body: snoozeBodyInternalSchemaV1, }, }, router.handleLegacyErrors( From 6ae2c297f16c22b6a1b6182aaffbe94ec5cf3b7b Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:12:47 +0000 Subject: [PATCH 29/32] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 18 +++++++++--------- oas_docs/bundle.serverless.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 6c497ff115bc1..b59b17f78e821 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4367,7 +4367,7 @@ }, "/api/alerting/rule/{id}/snooze_schedule": { "post": { - "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.", + "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time and schedule single or recurring downtimes.", "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { @@ -4403,14 +4403,14 @@ "additionalProperties": false, "properties": { "duration": { - "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "The end date of a recurring schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.", "type": "string" }, "every": { @@ -4443,7 +4443,7 @@ "type": "array" }, "onWeekDay": { - "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", + "description": "The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4454,7 +4454,7 @@ "type": "object" }, "start": { - "description": "The start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", "type": "string" }, "timezone": { @@ -4497,14 +4497,14 @@ "additionalProperties": false, "properties": { "duration": { - "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "The end date of a recurring schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.", "type": "string" }, "every": { @@ -4537,7 +4537,7 @@ "type": "array" }, "onWeekDay": { - "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", + "description": "The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4548,7 +4548,7 @@ "type": "object" }, "start": { - "description": "The start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", "type": "string" }, "timezone": { diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 9361a513439b1..894eefb0663b3 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4367,7 +4367,7 @@ }, "/api/alerting/rule/{id}/snooze_schedule": { "post": { - "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes.", + "description": "When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time and schedule single or recurring downtimes.", "operationId": "post-alerting-rule-id-snooze-schedule", "parameters": [ { @@ -4403,14 +4403,14 @@ "additionalProperties": false, "properties": { "duration": { - "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "The end date of a recurring schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.", "type": "string" }, "every": { @@ -4443,7 +4443,7 @@ "type": "array" }, "onWeekDay": { - "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", + "description": "The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4454,7 +4454,7 @@ "type": "object" }, "start": { - "description": "The start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", "type": "string" }, "timezone": { @@ -4497,14 +4497,14 @@ "additionalProperties": false, "properties": { "duration": { - "description": "The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.", + "description": "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", "type": "string" }, "recurring": { "additionalProperties": false, "properties": { "end": { - "description": "The end date of a recurring schedule in ISO 8601 format.", + "description": "The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.", "type": "string" }, "every": { @@ -4537,7 +4537,7 @@ "type": "array" }, "onWeekDay": { - "description": "The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule.", + "description": "The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.", "items": { "type": "string" }, @@ -4548,7 +4548,7 @@ "type": "object" }, "start": { - "description": "The start date and time of the schedule in ISO 8601 format.", + "description": "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", "type": "string" }, "timezone": { From 17f8115996b03f0f2b8d97a17bcf381d7e9bf4c5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:31:57 +0000 Subject: [PATCH 30/32] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 18 +++++++++--------- oas_docs/output/kibana.yaml | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 456503dbae4dc..341f686288403 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -3407,7 +3407,7 @@ paths: x-beta: true /api/alerting/rule/{id}/snooze_schedule: post: - description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes. + description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time and schedule single or recurring downtimes. operationId: post-alerting-rule-id-snooze-schedule parameters: - description: A required header to protect against CSRF attacks @@ -3439,14 +3439,14 @@ paths: type: object properties: duration: - description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: The end date of a recurring schedule in ISO 8601 format. + description: 'The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.' type: string every: description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' @@ -3472,13 +3472,13 @@ paths: minItems: 1 type: array onWeekDay: - description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. + description: The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: The start date and time of the schedule in ISO 8601 format. + description: 'The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.' type: string timezone: description: The timezone of the schedule. The default timezone is UTC. @@ -3509,14 +3509,14 @@ paths: type: object properties: duration: - description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: The end date of a recurring schedule in ISO 8601 format. + description: 'The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.' type: string every: description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' @@ -3542,13 +3542,13 @@ paths: minItems: 1 type: array onWeekDay: - description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. + description: The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: The start date and time of the schedule in ISO 8601 format. + description: 'The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.' type: string timezone: description: The timezone of the schedule. The default timezone is UTC. diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 8ce47e3f260b3..6223e2cdfb854 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -3761,7 +3761,7 @@ paths: - alerting /api/alerting/rule/{id}/snooze_schedule: post: - description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time or indefinitely and schedule single or recurring downtimes. + description: When you snooze a rule, the rule checks continue to run but alerts will not generate actions. You can snooze for a specified period of time and schedule single or recurring downtimes. operationId: post-alerting-rule-id-snooze-schedule parameters: - description: A required header to protect against CSRF attacks @@ -3793,14 +3793,14 @@ paths: type: object properties: duration: - description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: The end date of a recurring schedule in ISO 8601 format. + description: 'The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.' type: string every: description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' @@ -3826,13 +3826,13 @@ paths: minItems: 1 type: array onWeekDay: - description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. + description: The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: The start date and time of the schedule in ISO 8601 format. + description: 'The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.' type: string timezone: description: The timezone of the schedule. The default timezone is UTC. @@ -3863,14 +3863,14 @@ paths: type: object properties: duration: - description: 'The duration of the schedule. It allows values in `` format. `` is one of `h`, `m`, or `s` for hours, minutes, seconds. For example: `5h`, `30m`, `5000s`.' + description: 'The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.' type: string recurring: additionalProperties: false type: object properties: end: - description: The end date of a recurring schedule in ISO 8601 format. + description: 'The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`.' type: string every: description: 'The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.' @@ -3896,13 +3896,13 @@ paths: minItems: 1 type: array onWeekDay: - description: The specific days of the week (`[MO,TU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA]`) for a recurring schedule. + description: The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. items: type: string minItems: 1 type: array start: - description: The start date and time of the schedule in ISO 8601 format. + description: 'The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.' type: string timezone: description: The timezone of the schedule. The default timezone is UTC. From df69e33d1afbb36c5904a4f09e6d74ff7de2adb1 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 13 Mar 2025 10:19:03 +0000 Subject: [PATCH 31/32] update response duration to same as request format --- .../transforms/rrule_to_custom/v1.test.ts | 12 +++++----- .../schedule/transforms/rrule_to_custom/v1.ts | 22 +------------------ .../apis/snooze/external/snooze_rule_route.ts | 7 +++++- .../group4/tests/alerting/snooze.ts | 8 +++---- .../tests/alerting/group4/snooze.ts | 2 +- 5 files changed, 18 insertions(+), 33 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts index f8c88354bbf48..cbabb1ff33b54 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts @@ -17,7 +17,7 @@ describe('transformRRuleToCustomSchedule', () => { tzid: 'UTC', }, }) - ).toEqual({ duration: '2h', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); + ).toEqual({ duration: '7200000', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); }); it('transforms to start date and timezone correctly', () => { @@ -30,7 +30,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '25m', + duration: '1500000', start: '2025-02-10T21:30:00.000Z', timezone: 'America/New_York', }); @@ -50,7 +50,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '30m', + duration: '1800000', start: '2025-02-17T19:04:46.320Z', recurring: { every: '6M', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, timezone: 'UTC', @@ -71,7 +71,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '30m', + duration: '1800000', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, timezone: 'UTC', @@ -93,7 +93,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '5h', + duration: '18000000', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1M', @@ -118,7 +118,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '5m', + duration: '300000', start: '2025-01-14T05:05:00.000Z', recurring: { every: '2w', occurrences: 3 }, timezone: 'UTC', diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts index fbd20f2b576f1..2173b44447f3d 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment-timezone'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { Frequency } from '@kbn/rrule'; import type { RRule } from '../../../../../server/application/r_rule/types'; @@ -26,24 +25,6 @@ const transformFrequencyToEvery = (frequency: Frequency) => { } }; -const getDurationInString = (duration: number): string => { - const durationInDays = moment.duration(duration, 'milliseconds').asDays(); - if (durationInDays > 1) { - return `${durationInDays}d`; - } - - const durationInHours = moment.duration(duration, 'milliseconds').asHours(); - if (durationInHours > 1) { - return `${durationInHours}h`; - } - - const durationInSeconds = moment.duration(duration, 'milliseconds').asSeconds(); - if (durationInSeconds % 60 === 0) { - return `${durationInSeconds / 60}m`; - } - return `${durationInSeconds}s`; -}; - export const transformRRuleToCustomSchedule = (snoozeSchedule?: { duration: number; rRule: RRule; @@ -54,7 +35,6 @@ export const transformRRuleToCustomSchedule = (snoozeSchedule?: { const { rRule, duration } = snoozeSchedule; const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); - const transformedDuration = getDurationInString(duration); const recurring = { end: rRule.until ? new Date(rRule.until).toISOString() : undefined, @@ -68,7 +48,7 @@ export const transformRRuleToCustomSchedule = (snoozeSchedule?: { const filteredRecurring = omitBy(recurring, isUndefined); return { - duration: transformedDuration, + duration: duration.toString(), start: new Date(rRule.dtstart).toISOString(), timezone: rRule.tzid, ...(isEmpty(filteredRecurring) ? {} : { recurring: filteredRecurring }), diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts index c98c5bd8a537b..c8aea9aaa63ac 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts @@ -96,11 +96,16 @@ export const snoozeRuleRoute = ( (schedule) => schedule.id === snoozeScheduleId ); + const transformedCustomSchedule = transformRRuleToCustomSchedule(createdSchedule); + const response: SnoozeResponseV1 = { body: { schedule: { id: snoozeScheduleId, - custom: transformRRuleToCustomSchedule(createdSchedule), + custom: transformedCustomSchedule && { + ...transformedCustomSchedule, + duration: customSchedule.duration, + }, }, }, }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts index 292c36091f59b..244a5bb7f2893 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/snooze.ts @@ -108,7 +108,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, - duration: '10d', + duration: '240h', timezone: 'UTC', }, }, @@ -178,7 +178,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, - duration: '10d', + duration: '240h', timezone: 'UTC', }, }, @@ -248,7 +248,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, - duration: '10d', + duration: '240h', timezone: 'UTC', }, }, @@ -314,7 +314,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, - duration: '10d', + duration: '240h', timezone: 'UTC', }, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts index cd754beb7b0f9..330db65dd2ca4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts @@ -87,7 +87,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: response.body.schedule.id, custom: { ...snoozeSchedule.schedule.custom, - duration: '10d', + duration: '240h', timezone: 'UTC', }, }, From e0597495ea5864215012b673d36088de2672ccd4 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 13 Mar 2025 12:46:27 +0000 Subject: [PATCH 32/32] add transform duration logic back --- .../transforms/rrule_to_custom/v1.test.ts | 12 +++++----- .../schedule/transforms/rrule_to_custom/v1.ts | 22 ++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts index cbabb1ff33b54..f8c88354bbf48 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.test.ts @@ -17,7 +17,7 @@ describe('transformRRuleToCustomSchedule', () => { tzid: 'UTC', }, }) - ).toEqual({ duration: '7200000', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); + ).toEqual({ duration: '2h', start: '2021-05-10T00:00:00.000Z', timezone: 'UTC' }); }); it('transforms to start date and timezone correctly', () => { @@ -30,7 +30,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '1500000', + duration: '25m', start: '2025-02-10T21:30:00.000Z', timezone: 'America/New_York', }); @@ -50,7 +50,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '1800000', + duration: '30m', start: '2025-02-17T19:04:46.320Z', recurring: { every: '6M', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, timezone: 'UTC', @@ -71,7 +71,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '1800000', + duration: '30m', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1d', end: '2025-05-17T05:05:00.000Z', onWeekDay: ['Mo', 'FR'] }, timezone: 'UTC', @@ -93,7 +93,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '18000000', + duration: '5h', start: '2025-02-17T19:04:46.320Z', recurring: { every: '1M', @@ -118,7 +118,7 @@ describe('transformRRuleToCustomSchedule', () => { }, }) ).toEqual({ - duration: '300000', + duration: '5m', start: '2025-01-14T05:05:00.000Z', recurring: { every: '2w', occurrences: 3 }, timezone: 'UTC', diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts index 2173b44447f3d..fbd20f2b576f1 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/schedule/transforms/rrule_to_custom/v1.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment-timezone'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { Frequency } from '@kbn/rrule'; import type { RRule } from '../../../../../server/application/r_rule/types'; @@ -25,6 +26,24 @@ const transformFrequencyToEvery = (frequency: Frequency) => { } }; +const getDurationInString = (duration: number): string => { + const durationInDays = moment.duration(duration, 'milliseconds').asDays(); + if (durationInDays > 1) { + return `${durationInDays}d`; + } + + const durationInHours = moment.duration(duration, 'milliseconds').asHours(); + if (durationInHours > 1) { + return `${durationInHours}h`; + } + + const durationInSeconds = moment.duration(duration, 'milliseconds').asSeconds(); + if (durationInSeconds % 60 === 0) { + return `${durationInSeconds / 60}m`; + } + return `${durationInSeconds}s`; +}; + export const transformRRuleToCustomSchedule = (snoozeSchedule?: { duration: number; rRule: RRule; @@ -35,6 +54,7 @@ export const transformRRuleToCustomSchedule = (snoozeSchedule?: { const { rRule, duration } = snoozeSchedule; const transformedFrequency = transformFrequencyToEvery(rRule.freq as Frequency); + const transformedDuration = getDurationInString(duration); const recurring = { end: rRule.until ? new Date(rRule.until).toISOString() : undefined, @@ -48,7 +68,7 @@ export const transformRRuleToCustomSchedule = (snoozeSchedule?: { const filteredRecurring = omitBy(recurring, isUndefined); return { - duration: duration.toString(), + duration: transformedDuration, start: new Date(rRule.dtstart).toISOString(), timezone: rRule.tzid, ...(isEmpty(filteredRecurring) ? {} : { recurring: filteredRecurring }),