Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,100 @@ describe('Handle request to schedule', () => {
);
});

test('creates a scheduled_report saved object and rrule dtstart', async () => {
const report = await requestHandler.enqueueJob({
exportTypeId: 'printablePdfV2',
jobParams: mockJobParams,
schedule: {
rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2, tzid: 'UTC' },
},
});

const { id, created_at: _created_at, payload, ...snapObj } = report;
expect(snapObj).toMatchInlineSnapshot(`
Object {
"created_by": "testymcgee",
"jobtype": "printable_pdf_v2",
"meta": Object {
"isDeprecated": false,
"layout": "preserve_layout",
"objectType": "cool_object_type",
},
"migration_version": "unknown",
"notification": undefined,
"schedule": Object {
"rrule": Object {
"dtstart": "2025-06-23T14:17:19.765Z",
"freq": 1,
"interval": 2,
"tzid": "UTC",
},
},
}
`);
expect(payload).toMatchInlineSnapshot(`
Object {
"browserTimezone": "UTC",
"isDeprecated": false,
"layout": Object {
"id": "preserve_layout",
},
"locatorParams": Array [],
"objectType": "cool_object_type",
"title": "cool_title",
"version": "unknown",
}
`);

expect(auditLogger.log).toHaveBeenCalledWith({
event: {
action: 'scheduled_report_schedule',
category: ['database'],
outcome: 'unknown',
type: ['creation'],
},
kibana: {
saved_object: { id: 'mock-report-id', name: 'cool_title', type: 'scheduled_report' },
},
message: 'User is creating scheduled report [id=mock-report-id] [name=cool_title]',
});

expect(soClient.create).toHaveBeenCalledWith(
'scheduled_report',
{
jobType: 'printable_pdf_v2',
createdAt: expect.any(String),
createdBy: 'testymcgee',
title: 'cool_title',
enabled: true,
payload: JSON.stringify(payload),
schedule: {
rrule: {
dtstart: '2025-06-23T14:17:19.765Z',
freq: 1,
interval: 2,
tzid: 'UTC',
},
},
migrationVersion: 'unknown',
meta: {
objectType: 'cool_object_type',
layout: 'preserve_layout',
isDeprecated: false,
},
},
{ id: 'mock-report-id' }
);

expect(reportingCore.scheduleRecurringTask).toHaveBeenCalledWith(mockRequest, {
id: 'foo',
jobtype: 'printable_pdf_v2',
schedule: {
rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2, tzid: 'UTC' },
},
});
});

test('throws errors from so client create', async () => {
soClient.create = jest.fn().mockImplementationOnce(async () => {
throw new Error('SO create error');
Expand Down Expand Up @@ -400,6 +494,34 @@ describe('Handle request to schedule', () => {
expect(requestHandler.getSchedule()).toEqual({ rrule: { freq: 1, interval: 2 } });
});

test('parse schedule with dtstart from body', () => {
// @ts-ignore body is a read-only property
mockRequest.body = {
jobParams: rison.encode(mockJobParams),
schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } },
};
expect(requestHandler.getSchedule()).toEqual({
rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 },
});
});

test('handles invalid rrule.dtstart string', () => {
let error: { statusCode: number; body: string } | undefined;
try {
// @ts-ignore body is a read-only property
mockRequest.body = {
jobParams: rison.encode(mockJobParams),
schedule: { rrule: { dtstart: 'i am not a date', freq: 1, interval: 2 } },
};
requestHandler.getSchedule();
} catch (err) {
error = err;
}

expect(error?.statusCode).toBe(400);
expect(error?.body).toBe('Invalid startedAt date: i am not a date');
});

test('handles missing schedule', () => {
let error: { statusCode: number; body: string } | undefined;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import moment from 'moment';

import { schema } from '@kbn/config-schema';
import { isEmpty, omit } from 'lodash';
import { RruleSchedule, scheduleRruleSchemaV1 } from '@kbn/task-manager-plugin/server';
import { RruleSchedule, scheduleRruleSchemaV2 } from '@kbn/task-manager-plugin/server';
import { SavedObjectsUtils } from '@kbn/core/server';
import { IKibanaResponse } from '@kbn/core/server';
import { RawNotification } from '../../../saved_objects/scheduled_report/schemas/latest';
Expand All @@ -34,7 +34,7 @@ const MAX_ALLOWED_EMAILS = 30;
const validation = {
params: schema.object({ exportType: schema.string({ minLength: 2 }) }),
body: schema.object({
schedule: scheduleRruleSchemaV1,
schedule: scheduleRruleSchemaV2,
notification: schema.maybe(rawNotificationSchema),
jobParams: schema.string(),
}),
Expand Down Expand Up @@ -85,6 +85,13 @@ export class ScheduleRequestHandler extends RequestHandler<
});
}

if (rruleDef.dtstart && !moment(rruleDef.dtstart).isValid()) {
throw res.customError({
statusCode: 400,
body: `Invalid startedAt date: ${rruleDef.dtstart}`,
});
}

return schedule;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,85 @@ describe('transformResponse', () => {
});
});

it('should correctly transform the responses with rrule.dtstart field', () => {
expect(
transformResponse(
mockLogger,
{
...soResponse,
saved_objects: savedObjects.map((so) => ({
...so,
attributes: {
...so.attributes,
schedule: {
...so.attributes.schedule,
rrule: {
...so.attributes.schedule.rrule,
dtstart: new Date().toISOString(),
},
},
},
score: 0,
})),
},
lastRunResponse
)
).toEqual({
page: 1,
per_page: 10,
total: 2,
data: [
{
id: 'aa8b6fb3-cf61-4903-bce3-eec9ddc823ca',
created_at: '2025-05-06T21:10:17.137Z',
created_by: 'elastic',
enabled: true,
jobtype: 'printable_pdf_v2',
last_run: '2025-05-06T12:00:00.500Z',
next_run: expect.any(String),
payload: jsonPayload,
schedule: {
rrule: {
dtstart: expect.any(String),
freq: 3,
interval: 3,
byhour: [12],
byminute: [0],
tzid: 'UTC',
},
},
space_id: 'a-space',
title: '[Logs] Web Traffic',
},
{
id: '2da1cb75-04c7-4202-a9f0-f8bcce63b0f4',
created_at: '2025-05-06T21:12:06.584Z',
created_by: 'not-elastic',
enabled: true,
jobtype: 'PNGV2',
last_run: '2025-05-06T21:12:07.198Z',
next_run: expect.any(String),
notification: {
email: {
to: ['user@elastic.co'],
},
},
payload: jsonPayload,
title: 'Another cool dashboard',
schedule: {
rrule: {
dtstart: expect.any(String),
freq: 1,
interval: 3,
tzid: 'UTC',
},
},
space_id: 'a-space',
},
],
});
});

it('handles malformed payload', () => {
const malformedSo = {
...savedObjects[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,28 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => {
);
});

it('returns 400 on invalid rrule.dtstart date', async () => {
registerScheduleRoutesInternal(reportingCore, mockLogger);

await server.start();

await supertest(httpSetup.server.listener)
.post(`${INTERNAL_ROUTES.SCHEDULE_PREFIX}/printablePdfV2`)
.send({
jobParams: rison.encode({ browserTimezone: 'America/Amsterdam', title: `abc` }),
schedule: { rrule: { dtstart: '2025-06-23T14:1719.765Z', freq: 1, interval: 2 } },
})
.expect(400)
.then(({ body }) =>
expect(body.message).toMatchInlineSnapshot(`
"[request body.schedule.rrule]: types that failed validation:
- [request body.schedule.rrule.0.dtstart]: Invalid date: 2025-06-23T14:1719.765Z
- [request body.schedule.rrule.1.freq]: expected value to equal [2]
- [request body.schedule.rrule.2.freq]: expected value to equal [3]"
`)
);
});

it('returns 400 on invalid notification list', async () => {
registerScheduleRoutesInternal(reportingCore, mockLogger);

Expand Down Expand Up @@ -333,7 +355,7 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => {
bcc: ['single@email.com'],
},
},
schedule: { rrule: { freq: 1, interval: 2 } },
schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } },
})
.expect(200)
.then(({ body }) => {
Expand All @@ -351,7 +373,7 @@ describe(`POST ${INTERNAL_ROUTES.SCHEDULE_PREFIX}`, () => {
title: 'abc',
version: '7.14.0',
},
schedule: { rrule: { freq: 1, interval: 2 } },
schedule: { rrule: { dtstart: '2025-06-23T14:17:19.765Z', freq: 1, interval: 2 } },
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,95 @@ describe('getFirstRunAt', () => {
expect(firstRunAtDate).toEqual(new Date('2025-04-16T12:15:00Z'));
});

test('should return the calculated runAt from fixed dtstart when an rrule with fixed time and dtstart is provided', () => {
const taskInstance = {
id: 'id',
params: {},
state: {},
taskType: 'report',
schedule: {
rrule: {
dtstart: '2025-06-15T13:01:02Z',
freq: 3,
interval: 1,
tzid: 'UTC',
byhour: [12],
byminute: [15],
},
},
};
const firstRunAt = getFirstRunAt({ taskInstance, logger });
const firstRunAtDate = new Date(firstRunAt);
// The next day from 2025-06-15 is 2025-06-16
// The time is set to 12:15
expect(firstRunAtDate).toEqual(new Date('2025-06-16T12:15:02.000Z'));
});

test('should return the calculated runAt from now if using fixed dtstart calculates runAt in the past', () => {
const taskInstance = {
id: 'id',
params: {},
state: {},
taskType: 'report',
schedule: {
rrule: {
dtstart: '2025-03-10T13:01:02Z',
freq: 3,
interval: 1,
tzid: 'UTC',
byhour: [12],
byminute: [15],
},
},
};
const firstRunAt = getFirstRunAt({ taskInstance, logger });
const firstRunAtDate = new Date(firstRunAt);
// The next day from 2025-03-10 is 2025-03-11 which is in the past so the first runAt is set
// based on now which is fixed to '2025-04-15T13:01:02Z'
// The time is set to 12:15
expect(firstRunAtDate).toEqual(new Date('2025-04-16T12:15:02Z'));
});

test('should return the dtstart as the calculated runAt when dtstart is provided with no other fields', () => {
const taskInstance = {
id: 'id',
params: {},
state: {},
taskType: 'report',
schedule: {
rrule: {
dtstart: '2025-06-15T13:01:02Z',
freq: 3,
interval: 1,
tzid: 'UTC',
},
},
};
const firstRunAt = getFirstRunAt({ taskInstance, logger });
const firstRunAtDate = new Date(firstRunAt);
expect(firstRunAtDate).toEqual(new Date('2025-06-15T13:01:02.000Z'));
});

test('should return the now as the calculated runAt when dtstart is provided but is in the past', () => {
const taskInstance = {
id: 'id',
params: {},
state: {},
taskType: 'report',
schedule: {
rrule: {
dtstart: '2025-03-10T13:01:02Z',
freq: 3,
interval: 1,
tzid: 'UTC',
},
},
};
const firstRunAt = getFirstRunAt({ taskInstance, logger });
const firstRunAtDate = new Date(firstRunAt);
expect(firstRunAtDate).toEqual(new Date('2025-04-15T13:01:02.000Z'));
});

test('should return the calculated runAt when an rrule with only byhour is provided', () => {
const taskInstance = {
id: 'id',
Expand Down
Loading