Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06083b19a2 | ||
|
|
bf3df0312b | ||
|
|
503c57efd9 | ||
|
|
d084f4f912 | ||
|
|
e67fffb05d |
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/datadir"
|
"sneak.berlin/go/webhooker/internal/datadir"
|
||||||
"sneak.berlin/go/webhooker/internal/resetpw"
|
"sneak.berlin/go/webhooker/internal/resetpw"
|
||||||
"sneak.berlin/go/webhooker/internal/server"
|
"sneak.berlin/go/webhooker/internal/server"
|
||||||
@@ -36,6 +37,7 @@ const dockerStopGrace = 10 * time.Second
|
|||||||
// fx.New applies options before it executes invokes, so the timeout
|
// fx.New applies options before it executes invokes, so the timeout
|
||||||
// is set whether or not the graph itself can be constructed here.
|
// is set whether or not the graph itself can be constructed here.
|
||||||
func TestNewApp_StopTimeout(t *testing.T) {
|
func TestNewApp_StopTimeout(t *testing.T) {
|
||||||
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("DATA_DIR", t.TempDir())
|
t.Setenv("DATA_DIR", t.TempDir())
|
||||||
|
|
||||||
got := newApp().StopTimeout()
|
got := newApp().StopTimeout()
|
||||||
@@ -73,6 +75,7 @@ func freePort(t *testing.T) int {
|
|||||||
// anything is built, and the run of logger.New, which happens before
|
// anything is built, and the run of logger.New, which happens before
|
||||||
// the configuration sets the level.
|
// the configuration sets the level.
|
||||||
func TestNewApp_SendsFxEventsToTheLogger(t *testing.T) {
|
func TestNewApp_SendsFxEventsToTheLogger(t *testing.T) {
|
||||||
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("DATA_DIR", t.TempDir())
|
t.Setenv("DATA_DIR", t.TempDir())
|
||||||
t.Setenv("PORT", strconv.Itoa(freePort(t)))
|
t.Setenv("PORT", strconv.Itoa(freePort(t)))
|
||||||
t.Setenv("DEBUG", "true")
|
t.Setenv("DEBUG", "true")
|
||||||
|
|||||||
@@ -355,9 +355,14 @@ func TestProcessRetryTask_SuccessfulRetry(t *testing.T) {
|
|||||||
|
|
||||||
s := newISetup(t)
|
s := newISetup(t)
|
||||||
|
|
||||||
|
var receivedBody string
|
||||||
|
|
||||||
ts := httptest.NewServer(
|
ts := httptest.NewServer(
|
||||||
http.HandlerFunc(
|
http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
receivedBody = string(body)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -397,6 +402,8 @@ func TestProcessRetryTask_SuccessfulRetry(t *testing.T) {
|
|||||||
context.TODO(), &task,
|
context.TODO(), &task,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, event.Body, receivedBody)
|
||||||
|
|
||||||
iAssertStatus(t, s.WebhookDB, d.ID,
|
iAssertStatus(t, s.WebhookDB, d.ID,
|
||||||
database.DeliveryStatusDelivered,
|
database.DeliveryStatusDelivered,
|
||||||
)
|
)
|
||||||
@@ -443,9 +450,14 @@ func TestProcessRetryTask_LargeBody_FetchFromDB(
|
|||||||
|
|
||||||
s := newISetup(t)
|
s := newISetup(t)
|
||||||
|
|
||||||
|
var receivedBody string
|
||||||
|
|
||||||
ts := httptest.NewServer(
|
ts := httptest.NewServer(
|
||||||
http.HandlerFunc(
|
http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
receivedBody = string(body)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -482,6 +494,8 @@ func TestProcessRetryTask_LargeBody_FetchFromDB(
|
|||||||
context.TODO(), &task,
|
context.TODO(), &task,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, largeBody, receivedBody)
|
||||||
|
|
||||||
iAssertStatus(t, s.WebhookDB, d.ID,
|
iAssertStatus(t, s.WebhookDB, d.ID,
|
||||||
database.DeliveryStatusDelivered,
|
database.DeliveryStatusDelivered,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -18,15 +19,11 @@ import (
|
|||||||
// width.
|
// width.
|
||||||
const budget = 64
|
const budget = 64
|
||||||
|
|
||||||
// sampleRunes is how many runes wide the values in the charge test
|
// batchRunes is how many consecutive code points the charge test logs
|
||||||
// are. The handlers add a constant per field — a pair of quotes when
|
// in one value from U+1000 up. Logging each of those on its own line
|
||||||
// the value needs quoting — so the per-rune charge is only visible
|
// is too slow for the suite under the race detector; 4,096 at a time
|
||||||
// once it is amortised over a run of them.
|
// is 271 batches, each logged on two lines, so 542 lines per handler.
|
||||||
const sampleRunes = 64
|
const batchRunes = 4096
|
||||||
|
|
||||||
// quotingSlack is that constant: the pair of quotes a handler adds to
|
|
||||||
// a value that needs them and omits from one that does not.
|
|
||||||
const quotingSlack = 2
|
|
||||||
|
|
||||||
// newHandlers are the two handlers internal/logger can install. Time
|
// newHandlers are the two handlers internal/logger can install. Time
|
||||||
// is dropped so a line's width is a function of its value alone —
|
// is dropped so a line's width is a function of its value alone —
|
||||||
@@ -66,46 +63,48 @@ func renderedWidth(
|
|||||||
return buf.Len()
|
return buf.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// chargeTestRunes is the set of code points the charge test measures:
|
// emittedBytes is what a handler writes for the runes of s alone, in a
|
||||||
// every rune in the first two planes' worth of the BMP that the
|
// value that starts with prefix: the width of a line carrying prefix
|
||||||
// handlers are most likely to treat specially, the separators that
|
// and then s twice, less that of a line carrying prefix and s once.
|
||||||
// only slog's JSON handler escapes, and a stratified sample across
|
// Both values start the same way and hold the same runes, so the text
|
||||||
// the rest of Unicode so the astral charge is exercised on more than
|
// handler quotes both or neither, and the quotes cancel along with the
|
||||||
// one hand-picked rune.
|
// prefix and everything else on the line.
|
||||||
func chargeTestRunes() []rune {
|
func emittedBytes(
|
||||||
const (
|
newHandler func(io.Writer) slog.Handler,
|
||||||
denseCeiling = 0x800
|
prefix, s string,
|
||||||
stride = 1021
|
) int {
|
||||||
surrogateLo = 0xD800
|
return renderedWidth(newHandler, prefix+s+s) -
|
||||||
surrogateHi = 0xDFFF
|
renderedWidth(newHandler, prefix+s)
|
||||||
)
|
}
|
||||||
|
|
||||||
var runes []rune
|
// firstUndercharged returns the first rune in s that the handler
|
||||||
|
// writes in more bytes than EncodedBytes charges for it, and how many
|
||||||
|
// runes in s are undercharged that way. It measures one rune per line,
|
||||||
|
// in a value of that rune alone and again after a space, which makes
|
||||||
|
// the text handler quote the value. The charge test calls it on the
|
||||||
|
// code points below U+1000, and from there up only on a batch that has
|
||||||
|
// already failed, to name the code points rather than just their range.
|
||||||
|
func firstUndercharged(
|
||||||
|
newHandler func(io.Writer) slog.Handler,
|
||||||
|
s string,
|
||||||
|
) (rune, int) {
|
||||||
|
first, count := rune(-1), 0
|
||||||
|
|
||||||
keep := func(r rune) {
|
for _, r := range s {
|
||||||
if r >= surrogateLo && r <= surrogateHi {
|
charge := logfield.EncodedBytes(r)
|
||||||
return
|
if emittedBytes(newHandler, "", string(r)) <= charge &&
|
||||||
|
emittedBytes(newHandler, " ", string(r)) <= charge {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
runes = append(runes, r)
|
if count == 0 {
|
||||||
|
first = r
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
for r := range rune(denseCeiling) {
|
return first, count
|
||||||
keep(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range []rune{
|
|
||||||
0x2028, 0x2029, 0x200B, 0x4E00, 0xE000, 0xFFFD,
|
|
||||||
0x1000C, 0x1F600, 0xE0001, 0x10FFFF,
|
|
||||||
} {
|
|
||||||
keep(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
for r := rune(denseCeiling); r <= utf8.MaxRune; r += stride {
|
|
||||||
keep(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return runes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEncodedBytes_ChargesAtLeastWhatTheHandlersEmit is the property
|
// TestEncodedBytes_ChargesAtLeastWhatTheHandlersEmit is the property
|
||||||
@@ -114,33 +113,93 @@ func chargeTestRunes() []rune {
|
|||||||
// how a stated ceiling becomes false without any test noticing, so
|
// how a stated ceiling becomes false without any test noticing, so
|
||||||
// the charge is measured against what the handlers actually write
|
// the charge is measured against what the handlers actually write
|
||||||
// rather than against the escaping rules as read.
|
// rather than against the escaping rules as read.
|
||||||
|
//
|
||||||
|
// Every code point below U+1000 is checked on its own, for both
|
||||||
|
// handlers. That range holds the quote, the backslash and the control
|
||||||
|
// characters the handlers escape, next to code points each handler
|
||||||
|
// writes in fewer bytes than their charge, which in a sum would cover
|
||||||
|
// a neighbour charged too little. Each is measured in a value of it
|
||||||
|
// alone and again in one the text handler quotes, because that handler
|
||||||
|
// writes U+007F as one raw byte in a value it leaves bare but as \x7f,
|
||||||
|
// four bytes, in one it quotes.
|
||||||
|
//
|
||||||
|
// From U+1000 up the text handler writes every code point in exactly
|
||||||
|
// its charge, so the rest of Unicode is checked batchRunes at a time:
|
||||||
|
// each batch's summed charge must cover what the handler writes for
|
||||||
|
// the whole batch. The sums there can miss the JSON handler alone
|
||||||
|
// writing one code point in more bytes than its charge, when it writes
|
||||||
|
// others in the same batch in fewer.
|
||||||
func TestEncodedBytes_ChargesAtLeastWhatTheHandlersEmit(t *testing.T) {
|
func TestEncodedBytes_ChargesAtLeastWhatTheHandlersEmit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
var below strings.Builder
|
||||||
|
for r := range rune(0x1000) {
|
||||||
|
below.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var batches []string
|
||||||
|
|
||||||
|
for lo := rune(0x1000); lo <= utf8.MaxRune; lo += batchRunes {
|
||||||
|
var batch strings.Builder
|
||||||
|
|
||||||
|
for r := lo; r < lo+batchRunes; r++ {
|
||||||
|
// Surrogate halves are not runes a string can carry.
|
||||||
|
if utf8.ValidRune(r) {
|
||||||
|
batch.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batches = append(batches, batch.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// What EncodedBytes charges for each batch. Under -race -cover this
|
||||||
|
// takes longer than logging the batches, so it is worked out once,
|
||||||
|
// by whichever handler finishes logging first, while the other is
|
||||||
|
// still logging.
|
||||||
|
charged := sync.OnceValue(func() []int {
|
||||||
|
costs := make([]int, len(batches))
|
||||||
|
|
||||||
|
for i, batch := range batches {
|
||||||
|
for _, r := range batch {
|
||||||
|
costs[i] += logfield.EncodedBytes(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return costs
|
||||||
|
})
|
||||||
|
|
||||||
for name, newHandler := range newHandlers() {
|
for name, newHandler := range newHandlers() {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// 'a' is a printable ASCII rune, charged exactly one
|
if first, count := firstUndercharged(newHandler, below.String()); count > 0 {
|
||||||
// byte, so it is the zero point the other runes are
|
t.Errorf(
|
||||||
// measured against.
|
"%d code points below U+1000 cost more than "+
|
||||||
base := renderedWidth(
|
"EncodedBytes charges, the first U+%04X",
|
||||||
newHandler, strings.Repeat("a", sampleRunes),
|
count, first,
|
||||||
)
|
|
||||||
|
|
||||||
for _, r := range chargeTestRunes() {
|
|
||||||
got := renderedWidth(
|
|
||||||
newHandler,
|
|
||||||
strings.Repeat(string(r), sampleRunes),
|
|
||||||
)
|
)
|
||||||
charged := sampleRunes *
|
}
|
||||||
(logfield.EncodedBytes(r) - 1)
|
|
||||||
|
|
||||||
require.LessOrEqual(
|
emitted := make([]int, len(batches))
|
||||||
t, got-base, charged+quotingSlack,
|
for i, batch := range batches {
|
||||||
"U+%04X costs more on the line than "+
|
emitted[i] = emittedBytes(newHandler, "", batch)
|
||||||
"EncodedBytes charges for it",
|
}
|
||||||
r,
|
|
||||||
|
for i, cost := range charged() {
|
||||||
|
if emitted[i] <= cost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lo := rune(0x1000 + i*batchRunes)
|
||||||
|
first, count := firstUndercharged(
|
||||||
|
newHandler, batches[i],
|
||||||
|
)
|
||||||
|
t.Errorf(
|
||||||
|
"U+%04X to U+%04X emit %d bytes but are "+
|
||||||
|
"charged %d; %d of them cost more than "+
|
||||||
|
"EncodedBytes charges, the first U+%04X",
|
||||||
|
lo, lo+batchRunes-1, emitted[i], cost,
|
||||||
|
count, first,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -600,7 +600,10 @@ func bodyLimitedMethod(method string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MaxBodySize returns middleware that limits the size of
|
// MaxBodySize returns middleware that limits the size of
|
||||||
// POST/PUT/PATCH request bodies to maxBytes. It must be registered
|
// POST/PUT/PATCH request bodies to maxBytes. A request with any other
|
||||||
|
// method passes through uncapped, deliberately: no route behind it
|
||||||
|
// reads a body on GET, HEAD or DELETE. A handler that starts to needs
|
||||||
|
// its method added to bodyLimitedMethod first. It must be registered
|
||||||
// before any middleware that parses the body — notably CSRF, which
|
// before any middleware that parses the body — notably CSRF, which
|
||||||
// calls r.PostFormValue — so that form parsing happens under this
|
// calls r.PostFormValue — so that form parsing happens under this
|
||||||
// cap rather than net/http's 10 MB default.
|
// cap rather than net/http's 10 MB default.
|
||||||
|
|||||||
@@ -730,10 +730,8 @@ func TestNoCache_SetsHeaders(t *testing.T) {
|
|||||||
|
|
||||||
const testBodyLimit int64 = 64
|
const testBodyLimit int64 = 64
|
||||||
|
|
||||||
// maxBodySizeHandler wraps a sentinel handler in MaxBodySize with
|
// maxBodySizeResult is what runMaxBodySize's sentinel handler saw,
|
||||||
// testBodyLimit. The sentinel records whether it ran and how much of
|
// together with the response.
|
||||||
// the body it managed to read, so tests can distinguish "never
|
|
||||||
// reached" from "reached but truncated".
|
|
||||||
type maxBodySizeResult struct {
|
type maxBodySizeResult struct {
|
||||||
called bool
|
called bool
|
||||||
read int
|
read int
|
||||||
@@ -741,6 +739,10 @@ type maxBodySizeResult struct {
|
|||||||
response *httptest.ResponseRecorder
|
response *httptest.ResponseRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runMaxBodySize wraps a sentinel handler in MaxBodySize with
|
||||||
|
// testBodyLimit and serves req through it. The sentinel records
|
||||||
|
// whether it ran and how much of the body it managed to read, so
|
||||||
|
// tests can distinguish "never reached" from "reached but truncated".
|
||||||
func runMaxBodySize(
|
func runMaxBodySize(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func TestErrorPage_PanicOnAdminPage(t *testing.T) {
|
|||||||
|
|
||||||
w := serve(
|
w := serve(
|
||||||
server.NewRouterWithPageProbeForTest(
|
server.NewRouterWithPageProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
true, panicProbeHandler,
|
true, panicProbeHandler,
|
||||||
),
|
),
|
||||||
server.PageProbePattern,
|
server.PageProbePattern,
|
||||||
@@ -200,7 +200,7 @@ func TestErrorPage_PanicOnAdminPage(t *testing.T) {
|
|||||||
|
|
||||||
w = serve(
|
w = serve(
|
||||||
server.NewRouterWithProbeForTest(
|
server.NewRouterWithProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
true, panicProbeHandler,
|
true, panicProbeHandler,
|
||||||
),
|
),
|
||||||
server.ProbePattern,
|
server.ProbePattern,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi"
|
"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/config"
|
||||||
"sneak.berlin/go/webhooker/internal/handlers"
|
"sneak.berlin/go/webhooker/internal/handlers"
|
||||||
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,23 +37,45 @@ func SentryClientOptionsForTest(
|
|||||||
return sentryClientOptions(dsn, release)
|
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
|
// NewRouterForTest builds the real route tree via SetupRoutes with
|
||||||
// the supplied middleware and handlers, bypassing the fx lifecycle
|
// the supplied middleware and handlers, on a Server from New whose
|
||||||
// and the HTTP listener. Tests use it so that route-group middleware
|
// lifecycle is never started, so no HTTP listener runs. Tests use it
|
||||||
// registration order is exercised exactly as it ships, rather than
|
// so that route-group middleware registration order is exercised
|
||||||
// against a hand-rebuilt chain that could drift from routes.go.
|
// exactly as it ships, rather than against a hand-rebuilt chain that
|
||||||
|
// could drift from routes.go.
|
||||||
func NewRouterForTest(
|
func NewRouterForTest(
|
||||||
log *slog.Logger,
|
t *testing.T,
|
||||||
|
log *logger.Logger,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mw *middleware.Middleware,
|
mw *middleware.Middleware,
|
||||||
h *handlers.Handlers,
|
h *handlers.Handlers,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
s := &Server{
|
t.Helper()
|
||||||
log: log,
|
|
||||||
mw: mw,
|
s := newServerForTest(t, log, cfg, mw, h)
|
||||||
h: h,
|
|
||||||
params: ServerParams{Config: cfg},
|
|
||||||
}
|
|
||||||
s.SetupRoutes()
|
s.SetupRoutes()
|
||||||
|
|
||||||
return s.router
|
return s.router
|
||||||
@@ -83,19 +108,17 @@ const ProbePattern = "/probe"
|
|||||||
// option and the recoverer registered outside it is the thing a test
|
// option and the recoverer registered outside it is the thing a test
|
||||||
// has to be able to pin.
|
// has to be able to pin.
|
||||||
func NewRouterWithProbeForTest(
|
func NewRouterWithProbeForTest(
|
||||||
log *slog.Logger,
|
t *testing.T,
|
||||||
|
log *logger.Logger,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mw *middleware.Middleware,
|
mw *middleware.Middleware,
|
||||||
h *handlers.Handlers,
|
h *handlers.Handlers,
|
||||||
sentryEnabled bool,
|
sentryEnabled bool,
|
||||||
probe http.HandlerFunc,
|
probe http.HandlerFunc,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
s := &Server{
|
t.Helper()
|
||||||
log: log,
|
|
||||||
mw: mw,
|
s := newServerForTest(t, log, cfg, mw, h)
|
||||||
h: h,
|
|
||||||
params: ServerParams{Config: cfg},
|
|
||||||
}
|
|
||||||
s.sentryEnabled.Store(sentryEnabled)
|
s.sentryEnabled.Store(sentryEnabled)
|
||||||
s.SetupRoutes()
|
s.SetupRoutes()
|
||||||
s.router.Handle(ProbePattern, probe)
|
s.router.Handle(ProbePattern, probe)
|
||||||
@@ -113,19 +136,17 @@ const PageProbePattern = "/pages/probe"
|
|||||||
// it, so the probe runs behind that group's own middleware exactly as
|
// it, so the probe runs behind that group's own middleware exactly as
|
||||||
// the group's real routes do.
|
// the group's real routes do.
|
||||||
func NewRouterWithPageProbeForTest(
|
func NewRouterWithPageProbeForTest(
|
||||||
log *slog.Logger,
|
t *testing.T,
|
||||||
|
log *logger.Logger,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mw *middleware.Middleware,
|
mw *middleware.Middleware,
|
||||||
h *handlers.Handlers,
|
h *handlers.Handlers,
|
||||||
sentryEnabled bool,
|
sentryEnabled bool,
|
||||||
probe http.HandlerFunc,
|
probe http.HandlerFunc,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
s := &Server{
|
t.Helper()
|
||||||
log: log,
|
|
||||||
mw: mw,
|
s := newServerForTest(t, log, cfg, mw, h)
|
||||||
h: h,
|
|
||||||
params: ServerParams{Config: cfg},
|
|
||||||
}
|
|
||||||
s.sentryEnabled.Store(sentryEnabled)
|
s.sentryEnabled.Store(sentryEnabled)
|
||||||
s.SetupRoutes()
|
s.SetupRoutes()
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func TestPanicProbeChild(t *testing.T) {
|
|||||||
env := newTestEnv(t)
|
env := newTestEnv(t)
|
||||||
|
|
||||||
router := server.NewRouterWithProbeForTest(
|
router := server.NewRouterWithProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
false, panicProbeHandler,
|
false, panicProbeHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ func TestSentryStillSeesAPanic(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
router := server.NewRouterWithProbeForTest(
|
router := server.NewRouterWithProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
true, panicProbeHandler,
|
true, panicProbeHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ func TestResponseControllerThroughProductionRouter(t *testing.T) {
|
|||||||
|
|
||||||
routers := map[string]http.Handler{
|
routers := map[string]http.Handler{
|
||||||
server.ProbePattern: server.NewRouterWithProbeForTest(
|
server.ProbePattern: server.NewRouterWithProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
tc.sentryEnabled, probe,
|
tc.sentryEnabled, probe,
|
||||||
),
|
),
|
||||||
server.PageProbePattern: server.NewRouterWithPageProbeForTest(
|
server.PageProbePattern: server.NewRouterWithPageProbeForTest(
|
||||||
env.log.Get(), env.cfg, env.mw, env.hnd,
|
t, env.log, env.cfg, env.mw, env.hnd,
|
||||||
tc.sentryEnabled, probe,
|
tc.sentryEnabled, probe,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func newTestEnvWithConfig(
|
|||||||
t.Cleanup(app.RequireStop)
|
t.Cleanup(app.RequireStop)
|
||||||
|
|
||||||
return &testEnv{
|
return &testEnv{
|
||||||
router: server.NewRouterForTest(log.Get(), cfg, mw, hnd),
|
router: server.NewRouterForTest(t, log, cfg, mw, hnd),
|
||||||
sess: sess,
|
sess: sess,
|
||||||
db: db,
|
db: db,
|
||||||
dbMgr: dbMgr,
|
dbMgr: dbMgr,
|
||||||
@@ -531,6 +531,48 @@ func TestStaticServesOnlyGetAndHead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- every page route group ---
|
||||||
|
|
||||||
|
// TestPageRouteGroups_OversizeBody_RejectedBeforeCSRF pins the body
|
||||||
|
// cap ahead of CSRF and RequireAuth in every page route group. The
|
||||||
|
// requests carry no session and no CSRF token, so if either ran first
|
||||||
|
// the answer would be a 403 or a redirect to the login page rather
|
||||||
|
// than 413, and CSRF would issue its cookie (see
|
||||||
|
// TestPagesLogin_UnderLimit_NoToken_CSRFRejects). /settings has no
|
||||||
|
// POST route, but its group's middleware runs before the method is
|
||||||
|
// matched, so a POST there still reaches CSRF's form parsing if the
|
||||||
|
// cap moves after it. The user and webhook in the paths need not
|
||||||
|
// exist: nothing after the cap runs.
|
||||||
|
func TestPageRouteGroups_OversizeBody_RejectedBeforeCSRF(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("name", oversizeValue())
|
||||||
|
|
||||||
|
for _, path := range []string{
|
||||||
|
"/pages/login",
|
||||||
|
"/user/nobody/password",
|
||||||
|
"/settings/",
|
||||||
|
"/hooks/new",
|
||||||
|
"/hook/nonexistent/edit",
|
||||||
|
} {
|
||||||
|
w := env.post(path, form, nil)
|
||||||
|
|
||||||
|
assert.Equal(
|
||||||
|
t, http.StatusRequestEntityTooLarge, w.Code, path,
|
||||||
|
)
|
||||||
|
assert.False(
|
||||||
|
t, csrfCookieSet(w),
|
||||||
|
"CSRF middleware must not run for an oversized body to %s",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- /pages group ---
|
// --- /pages group ---
|
||||||
|
|
||||||
// TestPagesLogin_OversizeBody_RejectedBeforeCSRF proves the cap runs
|
// TestPagesLogin_OversizeBody_RejectedBeforeCSRF proves the cap runs
|
||||||
@@ -1610,7 +1652,7 @@ func TestTwoMetricsRoutersInOneProcess(t *testing.T) {
|
|||||||
)
|
)
|
||||||
third := &testEnv{
|
third := &testEnv{
|
||||||
router: server.NewRouterForTest(
|
router: server.NewRouterForTest(
|
||||||
first.log.Get(), first.cfg, first.mw, first.hnd,
|
t, first.log, first.cfg, first.mw, first.hnd,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user