check / check (push) Successful in 1m36s
DNSWATCHER_SENTRY_DSN was read but never used. This ports the Sentry integration from gohttpserver with sentry-go v0.49.0. The server's start hook calls sentry.Init when the DSN is set; a DSN Sentry cannot parse fails the hook, so startup stops with the parse error. sentryhttp, with Repanic, reports handler panics and passes them on to chi's Recoverer. Shutdown sends queued reports once the HTTP server has stopped. The client uses the older transport (DisableTelemetryBuffer): with the default one, Flush can return before sending a report made just before it. The DSN is checked at server start, not in config, so the config test is unchanged. sentry-go raises several golang.org/x modules. Model: opus-5-5
180 lines
4.2 KiB
Go
180 lines
4.2 KiB
Go
package server_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/server"
|
|
)
|
|
|
|
// The tests below set env vars and touch the global state of viper and
|
|
// of Sentry, so they cannot use t.Parallel.
|
|
|
|
// standInDelay is how long the Sentry stand-in takes to answer. It
|
|
// records a report only then, so a report that Shutdown did not wait
|
|
// for has not been recorded yet when Shutdown returns.
|
|
const standInDelay = 100 * time.Millisecond
|
|
|
|
// sentryStandIn is a local HTTP server in place of Sentry's, so that
|
|
// nothing a test reports leaves the host. It keeps the body of every
|
|
// request it receives.
|
|
type sentryStandIn struct {
|
|
server *httptest.Server
|
|
mu sync.Mutex
|
|
bodies []string
|
|
}
|
|
|
|
func newSentryStandIn(t *testing.T) *sentryStandIn {
|
|
t.Helper()
|
|
|
|
standIn := &sentryStandIn{}
|
|
standIn.server = httptest.NewServer(http.HandlerFunc(
|
|
func(_ http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("reading request to the Sentry stand-in: %v", err)
|
|
}
|
|
|
|
time.Sleep(standInDelay)
|
|
|
|
standIn.mu.Lock()
|
|
defer standIn.mu.Unlock()
|
|
|
|
standIn.bodies = append(standIn.bodies, string(body))
|
|
},
|
|
))
|
|
t.Cleanup(standIn.server.Close)
|
|
|
|
return standIn
|
|
}
|
|
|
|
// dsn returns a DSN that points Sentry at the stand-in.
|
|
func (s *sentryStandIn) dsn(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
dsn, err := url.Parse(s.server.URL)
|
|
if err != nil {
|
|
t.Fatalf("parsing the stand-in URL: %v", err)
|
|
}
|
|
|
|
dsn.User = url.User("public-key")
|
|
dsn.Path = "/1"
|
|
|
|
return dsn.String()
|
|
}
|
|
|
|
// received reports whether a request to the stand-in contained text.
|
|
func (s *sentryStandIn) received(text string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
for _, body := range s.bodies {
|
|
if strings.Contains(body, text) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func TestSentryUnsetDoesNothing(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_SENTRY_DSN", "")
|
|
|
|
srv := buildServer(t)
|
|
|
|
err := server.EnableSentry(srv)
|
|
if err != nil {
|
|
t.Fatalf("Sentry setup with no DSN: %v", err)
|
|
}
|
|
|
|
if sentry.CurrentHub().Client() != nil {
|
|
t.Error("Sentry was set up with no DSN configured")
|
|
}
|
|
}
|
|
|
|
// TestSentryReportsHandlerPanic checks that with a valid DSN a panic in
|
|
// a handler is reported to Sentry, still reaches chimw.Recoverer, and
|
|
// has been sent by the time Shutdown returns.
|
|
func TestSentryReportsHandlerPanic(t *testing.T) {
|
|
standIn := newSentryStandIn(t)
|
|
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_SENTRY_DSN", standIn.dsn(t))
|
|
|
|
srv := buildServer(t)
|
|
|
|
err := server.EnableSentry(srv)
|
|
if err != nil {
|
|
t.Fatalf("Sentry setup with a valid DSN: %v", err)
|
|
}
|
|
|
|
// Sentry's client is global: close it so later tests find none.
|
|
t.Cleanup(func() {
|
|
sentry.CurrentHub().Client().Close()
|
|
sentry.CurrentHub().BindClient(nil)
|
|
})
|
|
|
|
const panicMessage = "handler panic in the Sentry test"
|
|
|
|
srv.SetupRoutes()
|
|
server.RouterOf(srv).Get(
|
|
"/panic",
|
|
func(http.ResponseWriter, *http.Request) {
|
|
panic(panicMessage)
|
|
},
|
|
)
|
|
|
|
rec := serve(srv, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, "/panic", nil,
|
|
))
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Errorf(
|
|
"status %d, want %d from the recoverer",
|
|
rec.Code, http.StatusInternalServerError,
|
|
)
|
|
}
|
|
|
|
err = srv.Shutdown(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Shutdown: %v", err)
|
|
}
|
|
|
|
if !standIn.received(panicMessage) {
|
|
t.Error("the panic had not been sent to Sentry when Shutdown returned")
|
|
}
|
|
}
|
|
|
|
func TestSentryInvalidDSNStopsStartup(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_DATA_DIR", t.TempDir())
|
|
// Sentry cannot parse this: it has no public key before the host.
|
|
t.Setenv("DNSWATCHER_SENTRY_DSN", "https://sentry.test/1")
|
|
|
|
app := newServerApp(fx.Invoke(func(*server.Server) {}))
|
|
|
|
err := app.Start(t.Context())
|
|
if err == nil {
|
|
_ = app.Stop(t.Context())
|
|
|
|
t.Fatal("startup succeeded with an invalid DSN")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "invalid DNSWATCHER_SENTRY_DSN") {
|
|
t.Errorf("startup error does not name the setting: %v", err)
|
|
}
|
|
}
|