package routine import ( "testing" "time" ) func TestValidate(t *testing.T) { ok := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро"}} if err := Validate(ok); err != nil { t.Fatalf("valid routine rejected: %v", err) } cases := []struct { name string r Routine }{ {"missing name", Routine{Cron: "0 8 * * *", Body: "x"}}, {"missing body", Routine{Name: "m", Cron: "0 8 * * *"}}, {"bad cron", Routine{Name: "m", Cron: "not a cron", Body: "x"}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if err := Validate([]Routine{c.r}); err == nil { t.Error("expected an error, got nil") } }) } } func TestDue(t *testing.T) { // a routine that fires at 08:00 every day. rs := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро", Severity: 1}} t.Run("first sight seeds without firing (cold-start guard)", func(t *testing.T) { last := map[string]time.Time{} now := time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC) // exactly on schedule got := Due(rs, last, now) if len(got) != 0 { t.Errorf("routine fired on first sight: %v", got) } if _, seen := last["morning"]; !seen { t.Error("first sight did not seed the last-fired map") } }) t.Run("fires once the schedule crosses", func(t *testing.T) { last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 30, 0, 0, time.UTC)} now := time.Date(2026, 7, 6, 8, 0, 30, 0, time.UTC) // just past 08:00 got := Due(rs, last, now) if len(got) != 1 || got[0].Name != "morning" { t.Fatalf("expected morning to fire, got %v", got) } if !last["morning"].Equal(now) { t.Errorf("last-fired not advanced to now: %v", last["morning"]) } }) t.Run("does not refire before the next crossing", func(t *testing.T) { last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC)} now := time.Date(2026, 7, 6, 8, 5, 0, 0, time.UTC) // same morning, later if got := Due(rs, last, now); len(got) != 0 { t.Errorf("routine refired within the same window: %v", got) } }) t.Run("missed schedule on restart fires at most once, not per-tick", func(t *testing.T) { // booted at 07:00, seeded then; next tick is 09:00 (08:00 already passed). last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)} now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC) if got := Due(rs, last, now); len(got) != 1 { t.Fatalf("missed 08:00 should fire once at 09:00, got %v", got) } // immediately after, it must not fire again. if got := Due(rs, last, now.Add(time.Minute)); len(got) != 0 { t.Errorf("routine fired twice for one missed schedule: %v", got) } }) t.Run("bad cron is skipped, not fired every tick", func(t *testing.T) { bad := []Routine{{Name: "broken", Cron: "nonsense", Body: "x"}} last := map[string]time.Time{"broken": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)} now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC) if got := Due(bad, last, now); len(got) != 0 { t.Errorf("unparseable cron fired: %v", got) } }) }