package server import ( "context" "log/slog" "net/http" "testing" "github.com/getsentry/sentry-go" "github.com/go-chi/chi" "github.com/stretchr/testify/require" "go.uber.org/fx/fxtest" "sneak.berlin/go/webhooker/internal/config" "sneak.berlin/go/webhooker/internal/handlers" "sneak.berlin/go/webhooker/internal/logger" "sneak.berlin/go/webhooker/internal/middleware" ) // MaxFormBodySizeForTest exposes the form body cap so tests can // build requests that sit exactly at, below, and above it. const MaxFormBodySizeForTest = maxFormBodySize // ScrubSentryRequestForTest exposes the BeforeSend hook that // enableSentry installs, so a test can assert on what it leaves in an // event without standing up a Sentry client. func ScrubSentryRequestForTest( event *sentry.Event, hint *sentry.EventHint, ) *sentry.Event { return scrubSentryRequest(event, hint) } // SentryClientOptionsForTest exposes the exact options enableSentry // initialises the SDK with, so a test can capture events through the // production hook wiring rather than a hand-built equivalent. func SentryClientOptionsForTest( dsn, release string, ) sentry.ClientOptions { return sentryClientOptions(dsn, release) } // CleanShutdownForTest runs the server's stop hook, cleanShutdown, // against hs: a server the test started itself, so it can hold a // request open across the drain. Sentry is off. func CleanShutdownForTest(ctx context.Context, hs *http.Server) { s := &Server{log: slog.New(slog.DiscardHandler), httpServer: hs} s.cleanShutdown(ctx) } // newServerForTest builds a Server through New, as the application // does, on a lifecycle that is never started: the hooks New adds to // it never run, so nothing listens. func newServerForTest( t *testing.T, log *logger.Logger, cfg *config.Config, mw *middleware.Middleware, h *handlers.Handlers, ) *Server { t.Helper() s, err := New(fxtest.NewLifecycle(t), ServerParams{ Logger: log, Config: cfg, Middleware: mw, Handlers: h, }) require.NoError(t, err) return s } // NewRouterForTest builds the real route tree via SetupRoutes with // the supplied middleware and handlers, on a Server from New whose // lifecycle is never started, so no HTTP listener runs. Tests use it // so that route-group middleware registration order is exercised // exactly as it ships, rather than against a hand-rebuilt chain that // could drift from routes.go. func NewRouterForTest( t *testing.T, log *logger.Logger, cfg *config.Config, mw *middleware.Middleware, h *handlers.Handlers, ) http.Handler { t.Helper() s := newServerForTest(t, log, cfg, mw, h) s.SetupRoutes() return s.router } // ListenAddrForTest exposes the address the HTTP listener binds for // a given Config, so the rendering of host and port — IPv6 // bracketing above all — can be pinned without standing up a // listener. func ListenAddrForTest(cfg *config.Config) string { s := &Server{params: ServerParams{Config: cfg}} return s.listenAddr() } // ProbePattern is the route NewRouterWithProbeForTest adds to the // production route tree. const ProbePattern = "/probe" // NewRouterWithProbeForTest builds the production route tree exactly // as NewRouterForTest does and then registers probe at ProbePattern, // so a test can drive a handler that panics through the shipped // global middleware chain rather than a hand-assembled one. Nothing // about the chain is rebuilt here: the probe is an extra leaf under // the same Use() registrations every other route gets. // // sentryEnabled selects whether the sentryhttp handler is registered, // which in production a configured SENTRY_DSN decides. It is a // parameter because the relationship between that handler's Repanic // option and the recoverer registered outside it is the thing a test // has to be able to pin. func NewRouterWithProbeForTest( t *testing.T, log *logger.Logger, cfg *config.Config, mw *middleware.Middleware, h *handlers.Handlers, sentryEnabled bool, probe http.HandlerFunc, ) http.Handler { t.Helper() s := newServerForTest(t, log, cfg, mw, h) s.sentryEnabled.Store(sentryEnabled) s.SetupRoutes() s.router.Handle(ProbePattern, probe) return s.router } // PageProbePattern is where NewRouterWithPageProbeForTest serves its // probe: inside the /pages route group, the admin page group a // request reaches without signing in. const PageProbePattern = "/pages/probe" // NewRouterWithPageProbeForTest is NewRouterWithProbeForTest with the // probe added to the /pages route group once SetupRoutes has built // it, so the probe runs behind that group's own middleware exactly as // the group's real routes do. func NewRouterWithPageProbeForTest( t *testing.T, log *logger.Logger, cfg *config.Config, mw *middleware.Middleware, h *handlers.Handlers, sentryEnabled bool, probe http.HandlerFunc, ) http.Handler { t.Helper() s := newServerForTest(t, log, cfg, mw, h) s.sentryEnabled.Store(sentryEnabled) s.SetupRoutes() for _, route := range s.router.Routes() { pages, ok := route.SubRoutes.(chi.Router) if ok && route.Pattern == "/pages/*" { pages.Get("/probe", probe) } } return s.router }