package feedwatch import ( "context" "errors" "path/filepath" "strings" "testing" "time" "pete/internal/config" "pete/internal/storage" ) func setup(t *testing.T) { t.Helper() storage.Close() if err := storage.Init(filepath.Join(t.TempDir(), "feedwatch.db")); err != nil { t.Fatal(err) } t.Cleanup(func() { storage.Close() }) } func src(names ...string) []config.SourceConfig { var out []config.SourceConfig for _, n := range names { out = append(out, config.SourceConfig{Name: n, Enabled: true}) } return out } func fail(name string, n int) { for range n { storage.RecordPollResult(name, false, 0, errors.New("feed fetch https://x/feed: status 403")) } } // setLastNew backdates a source's last new story. func setLastNew(t *testing.T, name string, at time.Time) { t.Helper() if _, err := storage.Get().Exec(`UPDATE source_health SET last_new_at = ? WHERE source = ?`, at.Unix(), name); err != nil { t.Fatal(err) } } // capture is a Send that records what it was asked to deliver. type capture struct { msgs []string err error } func (c *capture) send(_ context.Context, msg string) error { if c.err != nil { return c.err } c.msgs = append(c.msgs, msg) return nil } func TestDownIsReportedOnceAndBackIsReported(t *testing.T) { setup(t) sources := src("Broken", "Fine") now := time.Now() storage.RecordPollResult("Fine", true, 10, nil) fail("Broken", DownAfter-1) c := &capture{} pass(context.Background(), now, sources, c.send) if len(c.msgs) != 0 { t.Fatalf("alerted below the threshold: %q", c.msgs) } fail("Broken", 1) pass(context.Background(), now, sources, c.send) if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Broken: 5 failed polls in a row, has never fetched successfully") { t.Fatalf("down alert = %q", c.msgs) } if strings.Contains(c.msgs[0], "Fine") { t.Errorf("a healthy feed was mentioned: %q", c.msgs[0]) } // Still down on the next pass: nothing new to say. fail("Broken", 3) pass(context.Background(), now.Add(Every), sources, c.send) if len(c.msgs) != 1 { t.Fatalf("down alert repeated: %q", c.msgs[1:]) } // It comes back. The success has to be stamped after the alert, so step // past the second the alert was recorded in. time.Sleep(1100 * time.Millisecond) storage.RecordPollResult("Broken", true, 5, nil) pass(context.Background(), time.Now(), sources, c.send) if len(c.msgs) != 2 || !strings.Contains(c.msgs[1], "Broken is fetching again") { t.Fatalf("recovery = %q", c.msgs) } pass(context.Background(), time.Now(), sources, c.send) if len(c.msgs) != 2 { t.Fatalf("recovery repeated: %q", c.msgs[2:]) } } func TestUndeliveredAlertIsRetried(t *testing.T) { setup(t) sources := src("Broken") fail("Broken", DownAfter) c := &capture{err: errors.New("homeserver unreachable")} pass(context.Background(), time.Now(), sources, c.send) c.err = nil pass(context.Background(), time.Now(), sources, c.send) if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Broken") { t.Fatalf("alert after a failed send = %q, want it delivered on the retry", c.msgs) } } func TestQuietFeedIsReportedOnce(t *testing.T) { setup(t) sources := src("Sleepy", "Busy") now := time.Now() storage.RecordPollResult("Sleepy", true, 10, nil) storage.RecordPollResult("Busy", true, 10, nil) setLastNew(t, "Sleepy", now.Add(-9*24*time.Hour)) c := &capture{} pass(context.Background(), now, sources, c.send) if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Sleepy fetches fine but hasn't had anything new in 9 days") { t.Fatalf("quiet alert = %q", c.msgs) } if strings.Contains(c.msgs[0], "Busy") { t.Errorf("a busy feed was called quiet: %q", c.msgs[0]) } pass(context.Background(), now.Add(Every), sources, c.send) if len(c.msgs) != 1 { t.Fatalf("quiet alert repeated: %q", c.msgs[1:]) } // New stories end the quiet spell without a message, and a later quiet // spell is a new episode that gets reported again. storage.RecordNewStories("Sleepy", 2) pass(context.Background(), now, sources, c.send) if len(c.msgs) != 1 { t.Fatalf("end of a quiet spell was announced: %q", c.msgs[1:]) } setLastNew(t, "Sleepy", now.Add(-8*24*time.Hour)) pass(context.Background(), now, sources, c.send) if len(c.msgs) != 2 { t.Fatalf("second quiet spell not reported: %q", c.msgs) } } // A feed that has just been added has no history, so it must not be called // quiet before it has had a full window to produce something. func TestNewSourceGetsAGracePeriod(t *testing.T) { setup(t) storage.RecordPollResult("Fresh", true, 0, nil) r, err := Check(time.Now().Add(QuietAfter-time.Hour), src("Fresh")) if err != nil { t.Fatal(err) } if !r.Empty() { t.Fatalf("new source reported inside its grace period: %+v", r) } } func TestDisabledSourcesNeverAlert(t *testing.T) { setup(t) fail("Retired", DownAfter*2) sources := []config.SourceConfig{{Name: "Retired", Enabled: false}} r, err := Check(time.Now(), sources) if err != nil { t.Fatal(err) } if !r.Empty() { t.Fatalf("disabled source alerted: %+v", r) } }