package observability import ( "bytes" "context" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/getsentry/sentry-go" "git.nakama.town/fmartingr/butterrobot/internal/config" ) // samplingContextFor builds the minimal sampling context the sampler inspects. func samplingContextFor(name string) sentry.SamplingContext { return sentry.SamplingContext{Span: &sentry.Span{Name: name}} } func TestInitSentryWithoutDSNIsNoop(t *testing.T) { if err := InitSentry(config.SentryConfig{}, false, nil); err != nil { t.Fatalf("expected no error, got %v", err) } if Enabled() { t.Fatal("expected Sentry to stay disabled without a DSN") } } func TestWrapLoggerReturnsOriginalWhenDisabled(t *testing.T) { logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil)) if got := WrapLogger(logger, config.SentryConfig{}); got != logger { t.Fatal("expected the original logger when Sentry is disabled") } } func TestWithTagsReturnsOriginalContextWhenDisabled(t *testing.T) { ctx := context.Background() if got := WithTags(ctx, map[string]string{"platform": "telegram"}); got != ctx { t.Fatal("expected the original context when Sentry is disabled") } } func TestHTTPMiddlewarePassesThroughWhenDisabled(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusTeapot) }) recorder := httptest.NewRecorder() HTTPMiddleware(next).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/healthz", nil)) if recorder.Code != http.StatusTeapot { t.Fatalf("expected status %d, got %d", http.StatusTeapot, recorder.Code) } } func TestRecoverAndCaptureKeepsCallerAlive(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, nil)) func() { defer RecoverAndCapture(logger, "test worker") panic("boom") }() if !strings.Contains(buf.String(), "boom") { t.Fatalf("expected the panic value to be logged, got %q", buf.String()) } } func TestFanoutHandlerWritesToEveryHandler(t *testing.T) { var first, second bytes.Buffer logger := slog.New(&fanoutHandler{handlers: []slog.Handler{ slog.NewTextHandler(&first, nil), slog.NewTextHandler(&second, nil), }}) logger.With("platform", "telegram").Error("failure") for name, buf := range map[string]*bytes.Buffer{"first": &first, "second": &second} { if !strings.Contains(buf.String(), "failure") || !strings.Contains(buf.String(), "telegram") { t.Fatalf("%s handler missing record: %q", name, buf.String()) } } } func TestTracesSamplerSkipsHealthCheck(t *testing.T) { sampler := tracesSampler(1.0) if rate := sampler(samplingContextFor("GET /healthz")); rate != 0 { t.Fatalf("expected health checks to be dropped, got %v", rate) } if rate := sampler(samplingContextFor("POST /telegram/incoming/")); rate != 1.0 { t.Fatalf("expected configured rate, got %v", rate) } } func TestTracesSamplerClampsOutOfRangeValues(t *testing.T) { if rate := tracesSampler(50)(samplingContextFor("POST /telegram/incoming/")); rate != 1 { t.Errorf("expected an out-of-range rate to clamp to 1, got %v", rate) } if rate := tracesSampler(-1)(samplingContextFor("POST /telegram/incoming/")); rate != 0 { t.Errorf("expected a negative rate to clamp to 0, got %v", rate) } }