mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 23:23:21 +00:00
Reject cron steps that would silently run hourly (#8607)
* fix(schedules): reject ignored numeric cron steps * fix(schedules): preserve saved numeric steps during recovery Reject unsupported numeric steps at create/update while keeping saved schedules executable with their previous singleton cadence. Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com> --------- Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,37 @@ describe("cron expression parser", () => {
|
||||
expect(cron.dayOfWeek.matches(0)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a step after a standalone number", () => {
|
||||
expect(validateCronExpression("0/15 * * * *")).toBe(
|
||||
"Invalid cron minute step: use * or a range before /",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["5/1 * * * *", "minute"],
|
||||
["0,5/15 * * * *", "minute"],
|
||||
["* 1/2 * * *", "hour"],
|
||||
["* * 1/2 * *", "day-of-month"],
|
||||
["* * * 1/2 *", "month"],
|
||||
["* * * * 1/2", "day-of-week"],
|
||||
])("rejects ignored numeric steps in %s", (expression, field) => {
|
||||
expect(validateCronExpression(expression)).toBe(
|
||||
`Invalid cron ${field} step: use * or a range before /`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ expression: "*/15 * * * *", expected: [0, 15, 30, 45] },
|
||||
{ expression: "0-59/15 * * * *", expected: [0, 15, 30, 45] },
|
||||
{ expression: "5-59/15 * * * *", expected: [5, 20, 35, 50] },
|
||||
])("preserves supported steps in $expression", ({ expression, expected }) => {
|
||||
const cron = parseCronExpression(expression);
|
||||
const minutes = Array.from({ length: 60 }, (_, minute) => minute);
|
||||
const matches = minutes.filter((minute) => cron.minute.matches(minute));
|
||||
|
||||
expect(matches).toEqual(expected);
|
||||
});
|
||||
|
||||
it("reports invalid expressions with server-facing copy", () => {
|
||||
expect(validateCronExpression("* * *")).toBe("Cron expressions must have 5 fields");
|
||||
expect(validateCronExpression("*/5/2 * * * *")).toBe("Invalid cron minute step");
|
||||
|
||||
@@ -90,6 +90,9 @@ function parseField(source: string, bounds: CronFieldBounds): CronFieldMatcher {
|
||||
if (!/^\d+$/.test(base)) {
|
||||
throw new Error(`Invalid cron ${bounds.name} value`);
|
||||
}
|
||||
// COMPAT(cronNumericSteps): added in v0.12.0, remove after 2027-04-17 once
|
||||
// persisted numeric steps are migrated. Validation rejects new numeric steps;
|
||||
// existing schedules retain their historical singleton match.
|
||||
const value = Number.parseInt(base, 10);
|
||||
if (!Number.isInteger(value) || value < bounds.min || value > bounds.max) {
|
||||
throw new Error(`Invalid cron ${bounds.name} value`);
|
||||
@@ -122,6 +125,12 @@ export function parseCronExpression(expression: string): ParsedCronExpression {
|
||||
export function validateCronExpression(expression: string): string | null {
|
||||
try {
|
||||
parseCronExpression(expression);
|
||||
const fields = expression.trim().split(/\s+/);
|
||||
for (const [index, field] of fields.entries()) {
|
||||
if (field.split(",").some((part) => /^\d+\//.test(part))) {
|
||||
return `Invalid cron ${CRON_FIELD_BOUNDS[index].name} step: use * or a range before /`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : "Invalid cron expression";
|
||||
|
||||
Reference in New Issue
Block a user