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, and that nothing else is // sent to Sentry. 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) }, ) // An ordinary request first: with client reports on, Sentry would // add a count of its dropped transaction to the panic report. serve(srv, httptest.NewRequestWithContext( t.Context(), http.MethodGet, "/.well-known/healthcheck", nil, )) 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") } if standIn.received("client_report") { t.Error("Sentry was sent a client report, not only the panic") } } 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) } }