check / check (push) Waiting to run
A route test now posts an oversized body with no session or CSRF token to each page route group, /settings included, and requires 413 with no CSRF cookie. Before, only the login form pinned the cap ahead of CSRF; reordering the /settings, /hooks or /hook groups failed nothing. The MaxBodySize doc comment says other methods pass uncapped on purpose, and the middleware test comment names the helper it describes. The three router helpers in the server tests build the Server through New, on a lifecycle that is never started, instead of setting its fields by hand. The README already described the cap's position correctly. Model: opus-5-5
162 lines
4.7 KiB
Go
162 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"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)
|
|
}
|
|
|
|
// 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
|
|
}
|