import { describe, expect, it } from "vitest"; import { parseCron, cronMatchesTime } from "./cron.js"; import { ImpossibleScheduleError, InvalidCronError } from "./errors.js"; describe("parseCron — valid syntax", () => { it("parses the every-minute expression", () => { const c = parseCron("* * * * *"); expect(c.minute.values.size).toBe(60); expect(c.minute.restricted).toBe(false); expect(c.dayOfMonth.restricted).toBe(false); expect(c.dayOfWeek.restricted).toBe(false); }); it("parses steps, ranges and lists", () => { const c = parseCron("*/15 8-18/2 * * 1,3,5"); expect([...c.minute.values]).toEqual([0, 15, 30, 45]); expect([...c.hour.values]).toEqual([8, 10, 12, 14, 16, 18]); expect([...c.dayOfWeek.values]).toEqual([1, 3, 5]); expect(c.minute.restricted).toBe(true); expect(c.hour.restricted).toBe(true); expect(c.dayOfWeek.restricted).toBe(true); }); it("parses month and dow names case-insensitively", () => { const c = parseCron("0 0 1 jan-mar mon-fri"); expect([...c.month.values]).toEqual([1, 2, 3]); expect([...c.dayOfWeek.values]).toEqual([1, 2, 3, 4, 5]); }); it("accepts a list element of one value", () => { expect([...parseCron("5,10 * * * *").minute.values]).toEqual([5, 10]); }); it("treats n/step as n..max/step", () => { expect([...parseCron("20/15 * * * *").minute.values]).toEqual([20, 35, 50]); }); }); describe("parseCron — rejection", () => { const bad = [ "", " ", "* * * *", "* * * * * *", "60 * * * *", "* 24 * * *", "* * 0 * *", "* * 32 * *", "* * * 0 *", "* * * 13 *", "* * * * 7", "* * * * MON-BR", "a * * * *", "*/0 * * * *", "*/-1 * * * *", "5-1 * * * *", "1--2 * * * *", "@reboot", "@daily", "? * * * *", "* * ? * *", "* * * * * 2025", "0 0 * * MON#1", "0 0 L * *", "0 0 1 W *", "1/2/3 * * * *", "*/ * * * *", "1,,2 * * * *", "-5 * * * *", ]; it.each(bad.map((e) => [e]))("rejects %s", (expr) => { expect(() => parseCron(expr)).toThrow(InvalidCronError); }); it("rejects non-string input", () => { expect(() => parseCron(undefined as unknown as string)).toThrow(InvalidCronError); expect(() => parseCron(5 as unknown as string)).toThrow(InvalidCronError); }); it("rejects impossible schedules", () => { expect(() => parseCron("0 0 31 2 *")).toThrow(ImpossibleScheduleError); expect(() => parseCron("0 0 30 2 *")).toThrow(ImpossibleScheduleError); expect(() => parseCron("0 0 31 4 *")).toThrow(ImpossibleScheduleError); }); it("accepts Feb 29 (leap years exist)", () => { expect(() => parseCron("0 0 29 2 *")).not.toThrow(); }); it("accepts the 31st in long months", () => { expect(() => parseCron("0 0 31 1 *")).not.toThrow(); expect(() => parseCron("0 0 31 * *")).not.toThrow(); }); }); describe("cronMatchesTime", () => { const cron = parseCron("*/15 9 * * *"); it("matches on the quarter hour in the 9 hour", () => { expect(cronMatchesTime(cron, 9, 0, 0)).toBe(true); expect(cronMatchesTime(cron, 9, 45, 0)).toBe(true); }); it("rejects other minutes/hours and any nonzero second", () => { expect(cronMatchesTime(cron, 9, 20, 0)).toBe(false); expect(cronMatchesTime(cron, 10, 0, 0)).toBe(false); expect(cronMatchesTime(cron, 9, 0, 1)).toBe(false); }); });