check / check (push) Waiting to run
Replaces #559, which conflicted. Model: opus-5-5
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
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
|
|
}
|
|
|
|
// NewRouterWithSentryForTest is NewRouterForTest with the sentryhttp
|
|
// handler registered, as a set SENTRY_DSN does in production.
|
|
func NewRouterWithSentryForTest(
|
|
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.sentryEnabled.Store(true)
|
|
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
|
|
}
|