check / check (push) Waiting to run
Every 400, 403, 404 and 500 on an admin page now answers with an error page in the normal layout: the status, one fixed line explaining it, and a link back to the webhook list, or to sign-in when nobody is signed in. Unknown paths reach it through the router's not-found handler, a refused form token through the CSRF middleware, and a panic through a recoverer each admin page route group installs first. Status codes are unchanged, and the page always sends Cache-Control: no-store. If the error page fails to render, the answer is the same status in plain text; if it panics, the answer is a 500. The receiver, the healthcheck and /metrics keep their plain answers. Model: opus-5-5
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package server
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-chi/chi"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"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)
|
|
}
|
|
|
|
// NewRouterForTest builds the real route tree via SetupRoutes with
|
|
// the supplied middleware and handlers, bypassing the fx lifecycle
|
|
// and the HTTP listener. 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(
|
|
log *slog.Logger,
|
|
cfg *config.Config,
|
|
mw *middleware.Middleware,
|
|
h *handlers.Handlers,
|
|
) http.Handler {
|
|
s := &Server{
|
|
log: log,
|
|
mw: mw,
|
|
h: h,
|
|
params: ServerParams{Config: cfg},
|
|
}
|
|
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(
|
|
log *slog.Logger,
|
|
cfg *config.Config,
|
|
mw *middleware.Middleware,
|
|
h *handlers.Handlers,
|
|
sentryEnabled bool,
|
|
probe http.HandlerFunc,
|
|
) http.Handler {
|
|
s := &Server{
|
|
log: log,
|
|
mw: mw,
|
|
h: h,
|
|
params: ServerParams{Config: cfg},
|
|
}
|
|
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(
|
|
log *slog.Logger,
|
|
cfg *config.Config,
|
|
mw *middleware.Middleware,
|
|
h *handlers.Handlers,
|
|
sentryEnabled bool,
|
|
probe http.HandlerFunc,
|
|
) http.Handler {
|
|
s := &Server{
|
|
log: log,
|
|
mw: mw,
|
|
h: h,
|
|
params: ServerParams{Config: cfg},
|
|
}
|
|
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
|
|
}
|