package declaration import ( "testing" "time" ) // A scheduled step is due when its cron says so, and the host both refuses a malformed cron on // arrival and computes the next due minute the scheduler fires on (novox/hq ADR 0053). These test // the parsing and the evaluation; the firing is tested against the scheduler. func TestParseCronRefusesMalformed(t *testing.T) { for _, expr := range []string{ "", // nothing "* * * *", // four fields "* * * * * *", // six fields "60 * * * *", // minute out of range "* 24 * * *", // hour out of range "* * 0 * *", // day-of-month below 1 "* * 32 * *", // day-of-month above 31 "* * * 13 *", // month out of range "* * * * 8", // day-of-week above 7 "a * * * *", // not a number "*/0 * * * *", // zero step "5-1 * * * *", // inverted range "1,,2 * * * *", // empty element } { if _, err := ParseCron(expr); err == nil { t.Errorf("a malformed cron %q was accepted", expr) } } } func TestParseCronAcceptsTheOrdinaryForms(t *testing.T) { for _, expr := range []string{ "* * * * *", // every minute "0 3 * * *", // 03:00 daily "*/15 * * * *", // every 15 minutes "0 0 1 1 *", // new year "0 9-17 * * 1-5", // business hours, weekdays "0 0 * * 7", // Sunday as 7 "0,30 * * * *", // twice an hour } { if _, err := ParseCron(expr); err != nil { t.Errorf("a valid cron %q was refused: %v", expr, err) } } } func TestCronNextIsTheNextMatchingMinute(t *testing.T) { // A Monday at 12:00:30, so "next" must round up to a whole minute and land on the first match // strictly after now — never re-firing the minute we are already in. now := time.Date(2026, 9, 7, 12, 0, 30, 0, time.UTC) // 2026-09-07 is a Monday cases := []struct { expr string want time.Time }{ {"* * * * *", time.Date(2026, 9, 7, 12, 1, 0, 0, time.UTC)}, {"0 3 * * *", time.Date(2026, 9, 8, 3, 0, 0, 0, time.UTC)}, {"*/15 * * * *", time.Date(2026, 9, 7, 12, 15, 0, 0, time.UTC)}, {"0 0 1 1 *", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, } for _, c := range cases { cron, err := ParseCron(c.expr) if err != nil { t.Fatalf("%q: %v", c.expr, err) } got, ok := cron.Next(now) if !ok { t.Errorf("%q: no next time found", c.expr) continue } if !got.Equal(c.want) { t.Errorf("%q: next is %s, want %s", c.expr, got, c.want) } } } func TestCronDayFieldsAreOredWhenBothRestricted(t *testing.T) { // The standard cron surprise: when day-of-month and day-of-week are both set, a day matches if // EITHER does. "1 * 13 * 5" is due on the 13th OR on any Friday. 2026-09-07 is a Monday the 7th // — neither — and 2026-09-11 is a Friday, and 2026-11-13 is the 13th. cron, err := ParseCron("0 0 13 * 5") if err != nil { t.Fatal(err) } friday := time.Date(2026, 9, 11, 0, 0, 0, 0, time.UTC) if !cron.Matches(friday) { t.Error("a Friday did not match a cron restricted to the 13th OR Friday") } thirteenth := time.Date(2026, 11, 13, 0, 0, 0, 0, time.UTC) // a non-Friday 13th if !cron.Matches(thirteenth) { t.Error("the 13th did not match a cron restricted to the 13th OR Friday") } neither := time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC) if cron.Matches(neither) { t.Error("a Monday the 7th matched a cron restricted to the 13th OR Friday") } }