package main import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func newMonitor(f *fake) *monitor { m := &monitor{relay: f.relay, sources: []*source{f.source}, to: f.source.to} m.lastPoll.Store(time.Now().UnixNano()) return m } func TestMonitorPingsAndReportsOnlyStateChanges(t *testing.T) { f := fakes(t) var pings atomic.Int32 heartbeat := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("heartbeat method %s", r.Method) } pings.Add(1) })) t.Cleanup(heartbeat.Close) m := newMonitor(f) m.heartbeat, m.client = heartbeat.URL, heartbeat.Client() ctx := t.Context() m.check(ctx) if pings.Load() != 1 || len(f.alerted()) != 0 { t.Fatalf("healthy: pings %d, alerts %q", pings.Load(), f.alerted()) } // The bot lost its admin rights in MAX: one message, no heartbeat f.maxAdmin.Store(false) m.check(ctx) m.check(ctx) if alerts := f.alerted(); pings.Load() != 1 || len(alerts) != 1 || !strings.Contains(alerts[0], "не администратор канала MAX") { t.Fatalf("failing: pings %d, alerts %q", pings.Load(), alerts) } f.maxAdmin.Store(true) m.check(ctx) if alerts := f.alerted(); pings.Load() != 2 || len(alerts) != 2 || !strings.Contains(alerts[1], "снова проходит") { t.Fatalf("recovered: pings %d, alerts %q", pings.Load(), alerts) } } func TestMonitorNoticesRemovedTelegramAdmin(t *testing.T) { f := fakes(t) f.tgAdmin.Store(false) newMonitor(f).check(t.Context()) if alerts := f.alerted(); len(alerts) != 1 || !strings.Contains(alerts[0], "не администратор канала Telegram «Канал»") { t.Fatalf("alerts %q", alerts) } } func TestPollingMonitorNoticesForeignWebhook(t *testing.T) { f := fakes(t) address := "https://other.example.test/webhook/secret" f.tgWebhook.Store(&address) m := newMonitor(f) m.check(t.Context()) alerts := f.alerted() if !m.failing || len(alerts) != 1 || !strings.Contains(alerts[0], "настроен вебхук") { t.Fatalf("foreign webhook not reported: failing=%t, alerts=%q", m.failing, alerts) } if strings.Contains(alerts[0], "/webhook/secret") { t.Fatalf("webhook secret leaked: %q", alerts) } } func TestPollingMonitorStopsHeartbeatUntilPollingRecovers(t *testing.T) { f := fakes(t) var pings atomic.Int32 heartbeat := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { pings.Add(1) })) t.Cleanup(heartbeat.Close) m := newMonitor(f) m.heartbeat, m.client = heartbeat.URL, heartbeat.Client() m.lastPoll.Store(time.Now().Add(-2*healthInterval - time.Second).UnixNano()) m.check(t.Context()) m.check(t.Context()) if alerts := f.alerted(); pings.Load() != 0 || len(alerts) != 1 || !strings.Contains(alerts[0], "приём постов Telegram не отвечает") { t.Fatalf("stalled poller: pings=%d, alerts=%q", pings.Load(), alerts) } m.lastPoll.Store(time.Now().UnixNano()) m.check(t.Context()) if alerts := f.alerted(); pings.Load() != 1 || len(alerts) != 2 || !strings.Contains(alerts[1], "снова проходит") { t.Fatalf("recovered poller: pings=%d, alerts=%q", pings.Load(), alerts) } } func TestWebhookMonitorDoesNotRequirePolling(t *testing.T) { f := fakes(t) address := "https://our.example.test/webhook" f.tgWebhook.Store(&address) m := newMonitor(f) m.tgWebhookURL = address m.lastPoll.Store(0) m.check(t.Context()) if m.failing || len(f.alerted()) != 0 { t.Fatalf("webhook mode needs no polling: failing=%t, alerts=%q", m.failing, f.alerted()) } } func TestChecksRunInParallelWithOwnTimeouts(t *testing.T) { var running, peak atomic.Int32 checks := make([]func(context.Context) error, 20) for i := range checks { checks[i] = func(ctx context.Context) error { n := running.Add(1) for p := peak.Load(); n > p && !peak.CompareAndSwap(p, n); p = peak.Load() { } defer running.Add(-1) if i%5 == 0 { // hangs: only its own timeout stops it <-ctx.Done() return fmt.Errorf("check %d: %w", i, ctx.Err()) } time.Sleep(10 * time.Millisecond) return nil } } problems := runChecks(t.Context(), checks, 4, 100*time.Millisecond) want := []string{"check 0", "check 5", "check 10", "check 15"} if len(problems) != len(want) { t.Fatalf("problems %q", problems) } for i, p := range problems { if !strings.HasPrefix(p, want[i]) { t.Fatalf("problems %q, want in order %q", problems, want) } } if n := peak.Load(); n > 4 || n < 2 { t.Fatalf("peak parallelism %d", n) } }