Files
go-otel/internal/alerts/alerts_test.go
T
mats 645f923175
Build and Publish / Metadata (push) Successful in 10s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 2m16s
include db for gitea build
2026-09-20 11:01:56 +02:00

131 lines
4.7 KiB
Go

package alerts
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"git.tornberg.me/go-otel/internal/features"
)
func burst(sig string, n int, at time.Time, status int, route string) []features.Event {
var out []features.Event
for i := 0; i < n; i++ {
out = append(out, features.Event{Timestamp: at.Add(time.Duration(i) * time.Second), Service: "api", Kind: "span", IsError: true,
PatternSignature: sig, StatusCode: status, Route: route, ClientIP: "203.0.113.7"})
}
return out
}
type fake struct {
mu sync.Mutex
sent []Alert
err error
}
func (f *fake) Name() string { return "fake" }
func (f *fake) Send(_ context.Context, a Alert) error {
f.mu.Lock()
defer f.mu.Unlock()
f.sent = append(f.sent, a)
return f.err
}
func newRunner(events []features.Event, n Notifier) *Runner {
cfg := Config{Interval: time.Minute, Window: 5 * time.Minute, BaselineWindows: 12, Cooldown: 30 * time.Minute, MinExcess: 5, Multiple: 3, Lag: 0}
return &Runner{
cfg: cfg, notifiers: []Notifier{n}, sup: features.NewSuppressor(cfg.Cooldown),
load: func(context.Context, time.Time, time.Time) ([]features.Event, error) { return events, nil },
dataSince: func(context.Context) time.Time { return time.Time{} },
}
}
func TestRunnerPrimesThenAlertsOnceAndFiltersProbes(t *testing.T) {
now := time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC)
cur := now.Add(-2 * time.Minute)
f := &fake{}
// a real defect burst and a probe burst in the same window
events := append(burst("defect", 20, cur, 500, "/api/orders"), burst("probe", 50, cur, 404, "/.env")...)
r := newRunner(events, f)
r.Tick(context.Background(), now) // first tick primes: nothing is sent
if len(f.sent) != 0 {
t.Fatalf("primed tick sent %d alerts", len(f.sent))
}
// the same incident keeps going: suppressed by the primed cooldown
r.Tick(context.Background(), now.Add(time.Minute))
if len(f.sent) != 0 {
t.Fatalf("ongoing incident re-alerted: %d", len(f.sent))
}
// a new, different failure appears
r.load = func(context.Context, time.Time, time.Time) ([]features.Event, error) {
return append(events, burst("newdefect", 15, now.Add(time.Minute), 503, "/api/pay")...), nil
}
r.Tick(context.Background(), now.Add(4*time.Minute))
if len(f.sent) != 1 {
t.Fatalf("want 1 alert, got %d", len(f.sent))
}
txt := f.sent[0].Text("https://otel.example")
if strings.Contains(txt, "/.env") {
t.Fatalf("probe leaked into alert: %s", txt)
}
if !strings.Contains(txt, "/api/pay") || !strings.Contains(txt, "1 client") || !strings.HasSuffix(txt, "https://otel.example/issues") {
t.Fatalf("unexpected text: %s", txt)
}
if got := r.Status(); len(got.Alerts) != 1 || got.Channels[0] != "fake" {
t.Fatalf("status = %+v", got)
}
}
func TestTelegramSendsAndNeverLeaksToken(t *testing.T) {
var gotPath string
var body map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
_ = json.NewDecoder(r.Body).Decode(&body)
w.WriteHeader(200)
}))
tg := &Telegram{Token: "SECRETTOKEN", ChatID: "42", ThreadID: "7", API: srv.URL}
a := NewAlert(features.Spike{Service: "s", ErrorCount: 9}, time.Now())
if err := tg.Send(context.Background(), a); err != nil {
t.Fatal(err)
}
if gotPath != "/botSECRETTOKEN/sendMessage" || body["chat_id"] != "42" || body["message_thread_id"] != float64(7) {
t.Fatalf("request wrong: %s %+v", gotPath, body)
}
srv.Close() // connection refused: the transport error contains the URL
err := tg.Send(context.Background(), a)
if err == nil || strings.Contains(err.Error(), "SECRETTOKEN") {
t.Fatalf("token leaked or no error: %v", err)
}
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"description":"bad token SECRETTOKEN"}`, 401)
}))
defer bad.Close()
err = (&Telegram{Token: "SECRETTOKEN", ChatID: "1", API: bad.URL}).Send(context.Background(), a)
if err == nil || strings.Contains(err.Error(), "SECRETTOKEN") || !strings.Contains(err.Error(), "401") {
t.Fatalf("error not scrubbed: %v", err)
}
}
func TestTelegramTruncates(t *testing.T) {
var body map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&body) }))
defer srv.Close()
spike := features.Spike{Service: "s"}
for i := 0; i < 200; i++ {
spike.Patterns = append(spike.Patterns, features.PatternDelta{Count: 1, Example: features.Event{Message: strings.Repeat("x", 100)}})
}
if err := (&Telegram{Token: "t", ChatID: "1", API: srv.URL}).Send(context.Background(), NewAlert(spike, time.Now())); err != nil {
t.Fatal(err)
}
if n := len([]rune(body["text"].(string))); n > 4096 {
t.Fatalf("message too long: %d", n)
}
}