All checks were successful
check / check (push) Successful in 2m59s
The empty-TRUSTED_PROXIES warning was gated on IsProd(), but WEBHOOKER_ENVIRONMENT defaults to dev, so an internet-exposed deployment whose operator never set it got no warning at all — the exact operator error the warning exists to catch. It now fires whenever the list is empty, in any environment, and its text is accurate both behind a reverse proxy (shared buckets, remotely deniable admin login) and with nothing in front of the process (harmless). The startup configuration summary also now logs sessionIdleTimeout, the one value where a valid setting silently disables a security control. The README documented a two-stage Docker build on golang:1.24 running "make check" (the tree has three stages: a golangci-lint lint stage running fmt-check and lint, a golang:1.26.1-bookworm builder running test and build, then the Alpine runtime), advertised the public receiver as accepting all methods (it answers 405 to everything but POST), claimed unqualified per-IP login rate limiting, and left the session-expiry prose orphaned inside the trusted-proxy subsection. The rest of the README was swept against the code rather than only the reported lines: every documented route checked method-by-method against internal/server/routes.go (adding the password-change, entrypoint and target routes that were missing), every environment variable checked against internal/config/config.go (MAINTENANCE_MODE serves no maintenance page — it only sets a healthcheck field), the fx wiring, package tree, prerequisites and dev commands brought back in line with the tree. Statements the sweep had waved through, each re-derived from the code this time: - Circuit breakers are not HTTP-only. target_slack.go embeds httpCore and hands its own MaxRetries to the same retry path, so a slack target with max_retries > 0 gets a breaker on the same defaults. - No WAL exists. Both DSNs are file:%s?cache=shared&mode=rwc and no journal_mode pragma is issued anywhere, so every database runs on SQLite's rollback journal. - Slack config is keyed webhookUrl, not webhook_url; the underscored spelling is only the error message. An operator copying the README wrote config that was silently ignored. - Setting carries no BaseModel, so neither the common-field list nor "soft deletes on all entities" held for it. - DeliveryTask names no type; it is delivery.Task. - script/lint runs golangci-lint on the host. Only script/cibuild and script/docker involve Docker; #109 tracks changing that, and until it lands the README says what the tree does. - An entrypoint's path column holds a bare UUID. The /webhook/ prefix is route only and never stored. - max_queue_size is stored and displayed but consulted by nothing; queue depth is the two fixed 10,000-entry channels. - Inline event bodies are cut at "< 16 KiB", not "≤". - retention_days 0 belongs to the retain-forever band — BeforeSave rewrites it to the sentinel — not the finite one, whose floor is 1. A negative value is a 400 in parseRetentionDays. - The receiver's per-client limit keys on the request path (httprate.KeyByEndpoint), not on the entrypoint; the paragraph eight lines below already said so and the two contradicted each other. - Shutdown goes through lifecycle.WaitForShutdown, which is bounded by fx's stop context and can return with goroutines still running, rather than a bare WaitGroup.Wait(). - Nine of the Makefile's fifteen targets shim script/; build, run, dev, deps, clean and css are inline. - Nine entities are documented and nine model files exist, not eight. - The metrics middleware is only registered when METRICS_USERNAME is set, so an over-limit request is not always counted. Two gaps closed while checking: the target-type list omitted slack entirely, and the package tree omitted event_log_view.go and the three testing.go files, which are ordinary compiled sources exporting NewTestDatabase and NewForTest rather than _test.go scaffolding. TestStaticServesEveryMethod settles what /s/* actually answers: chi's Mount registers every method and http.FileServer special-cases only HEAD, so POST, PUT and DELETE to an asset are served the file. The README claimed GET and HEAD. TODO.md drops the unsupported half of its CI claim, keeping the cache-defeated container runs, and splits the landed password change away from the unimplemented reset flow. Rebasing onto current next moved the tree under three of these sections, so they were re-derived against the rebased tree rather than the one the branch was cut from: alpine.min.js is no longer committed but fetched and hash-verified by script/fetch-assets, so the package tree entry for it was wrong and vendor.sha256 was absent; the builder stage runs that script before make test; the Makefile is now ten shims out of sixteen targets, and fmt-check was missing from the development command list. The Quick Start was also wrong in a way that costs a new contributor a red build: it said make deps, which only runs go mod download and tidy, and make check then fails two tests on the absent asset. It now says make bootstrap, which fetches it.
720 lines
15 KiB
Go
720 lines
15 KiB
Go
package config_test
|
|
|
|
import (
|
|
"bytes"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
)
|
|
|
|
// Shared subtest names for the env-parsing tables below, which all
|
|
// exercise the same three cases against different variables.
|
|
const (
|
|
caseUnsetUsesDefault = "unset uses default"
|
|
caseValidValueParsed = "valid value is parsed"
|
|
caseUnparseableFails = "unparseable value fails startup"
|
|
|
|
// cidrPrivateV4 is the sample trusted-proxy block the
|
|
// TRUSTED_PROXIES cases are built from.
|
|
cidrPrivateV4 = "10.0.0.0/8"
|
|
)
|
|
|
|
func TestEnvironmentConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
envVars map[string]string
|
|
expectError bool
|
|
isDev bool
|
|
isProd bool
|
|
}{
|
|
{
|
|
name: "default is dev",
|
|
isDev: true,
|
|
isProd: false,
|
|
},
|
|
{
|
|
name: "explicit dev",
|
|
envValue: "dev",
|
|
isDev: true,
|
|
isProd: false,
|
|
},
|
|
{
|
|
name: "explicit prod",
|
|
envValue: "prod",
|
|
isDev: false,
|
|
isProd: true,
|
|
},
|
|
{
|
|
name: "invalid environment",
|
|
envValue: "staging",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
if tt.envValue != "" {
|
|
t.Setenv(
|
|
"WEBHOOKER_ENVIRONMENT", tt.envValue,
|
|
)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv(
|
|
"WEBHOOKER_ENVIRONMENT",
|
|
))
|
|
}
|
|
|
|
for k, v := range tt.envVars {
|
|
t.Setenv(k, v)
|
|
}
|
|
|
|
if tt.expectError {
|
|
testEnvironmentConfigError(t)
|
|
} else {
|
|
testEnvironmentConfigSuccess(
|
|
t, tt.isDev, tt.isProd,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testEnvironmentConfigError(t *testing.T) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fx.New(
|
|
fx.NopLogger,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
|
|
assert.Error(t, app.Err())
|
|
}
|
|
|
|
func testEnvironmentConfigSuccess(
|
|
t *testing.T,
|
|
isDev, isProd bool,
|
|
) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
assert.Equal(t, isDev, cfg.IsDev())
|
|
assert.Equal(t, isProd, cfg.IsProd())
|
|
}
|
|
|
|
func TestRetentionSweepInterval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
set bool
|
|
value string
|
|
expectError bool
|
|
// sentinel, when set, must be wrapped by the startup
|
|
// error; every error case must additionally name the
|
|
// variable in its message.
|
|
sentinel error
|
|
expected time.Duration
|
|
}{
|
|
{
|
|
name: caseUnsetUsesDefault,
|
|
set: false,
|
|
expected: time.Hour,
|
|
},
|
|
{
|
|
name: caseValidValueParsed,
|
|
set: true,
|
|
value: "15m",
|
|
expected: 15 * time.Minute,
|
|
},
|
|
{
|
|
name: caseUnparseableFails,
|
|
set: true,
|
|
value: "not-a-duration",
|
|
expectError: true,
|
|
},
|
|
{
|
|
// A non-positive period panics the ticker in the
|
|
// reaper and archive-sweeper goroutines, long after
|
|
// startup has reported success, so it has to fail
|
|
// here instead.
|
|
name: "zero fails startup",
|
|
set: true,
|
|
value: "0s",
|
|
expectError: true,
|
|
sentinel: config.ErrNonPositiveValue,
|
|
},
|
|
{
|
|
name: "negative fails startup",
|
|
set: true,
|
|
value: "-1h",
|
|
expectError: true,
|
|
sentinel: config.ErrNonPositiveValue,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
|
|
|
if tt.set {
|
|
t.Setenv("RETENTION_SWEEP_INTERVAL", tt.value)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv(
|
|
"RETENTION_SWEEP_INTERVAL",
|
|
))
|
|
}
|
|
|
|
if tt.expectError {
|
|
expectStartupErrorFor(
|
|
t, "RETENTION_SWEEP_INTERVAL", tt.sentinel,
|
|
)
|
|
} else {
|
|
testRetentionSweepIntervalSuccess(t, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// startupError builds the app config.New belongs to and returns
|
|
// the error fx reports, which is non-nil whenever an environment
|
|
// value is set but invalid.
|
|
func startupError(t *testing.T) error {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fx.New(
|
|
fx.NopLogger,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
|
|
return app.Err()
|
|
}
|
|
|
|
// expectStartupError asserts that fx refuses to build the app,
|
|
// which is what a set-but-invalid environment value must cause.
|
|
func expectStartupError(t *testing.T) {
|
|
t.Helper()
|
|
|
|
assert.Error(t, startupError(t))
|
|
}
|
|
|
|
// expectStartupErrorFor asserts that startup fails, that the error
|
|
// names the offending variable so an operator can find it, and,
|
|
// when sentinel is non-nil, that it wraps that sentinel.
|
|
func expectStartupErrorFor(
|
|
t *testing.T,
|
|
key string,
|
|
sentinel error,
|
|
) {
|
|
t.Helper()
|
|
|
|
err := startupError(t)
|
|
require.ErrorContains(t, err, key)
|
|
|
|
if sentinel != nil {
|
|
require.ErrorIs(t, err, sentinel)
|
|
}
|
|
}
|
|
|
|
func testRetentionSweepIntervalSuccess(
|
|
t *testing.T,
|
|
expected time.Duration,
|
|
) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
assert.Equal(t, expected, cfg.RetentionSweepInterval)
|
|
}
|
|
|
|
func TestSessionIdleTimeout(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
set bool
|
|
value string
|
|
expectError bool
|
|
expected time.Duration
|
|
}{
|
|
{
|
|
name: caseUnsetUsesDefault,
|
|
set: false,
|
|
expected: 24 * time.Hour,
|
|
},
|
|
{
|
|
name: caseValidValueParsed,
|
|
set: true,
|
|
value: "30m",
|
|
expected: 30 * time.Minute,
|
|
},
|
|
{
|
|
name: caseUnparseableFails,
|
|
set: true,
|
|
value: "not-a-duration",
|
|
expectError: true,
|
|
},
|
|
{
|
|
// Non-positive is "idle expiry disabled" for this
|
|
// variable, not a configuration error: unlike
|
|
// RETENTION_SWEEP_INTERVAL it never becomes a ticker
|
|
// period.
|
|
name: "zero disables idle expiry",
|
|
set: true,
|
|
value: "0s",
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "negative disables idle expiry",
|
|
set: true,
|
|
value: "-1h",
|
|
expected: -time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
|
|
|
if tt.set {
|
|
t.Setenv("SESSION_IDLE_TIMEOUT", tt.value)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv(
|
|
"SESSION_IDLE_TIMEOUT",
|
|
))
|
|
}
|
|
|
|
if tt.expectError {
|
|
expectStartupError(t)
|
|
} else {
|
|
testSessionIdleTimeoutSuccess(t, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSessionIdleTimeoutSuccess(
|
|
t *testing.T,
|
|
expected time.Duration,
|
|
) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
assert.Equal(t, expected, cfg.SessionIdleTimeout)
|
|
}
|
|
|
|
func TestDefaultDataDir(t *testing.T) {
|
|
for _, env := range []string{"", "dev", "prod"} {
|
|
name := env
|
|
if name == "" {
|
|
name = "unset"
|
|
}
|
|
|
|
t.Run("env="+name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
if env != "" {
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", env)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv(
|
|
"WEBHOOKER_ENVIRONMENT",
|
|
))
|
|
}
|
|
|
|
require.NoError(t, os.Unsetenv("DATA_DIR"))
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
assert.Equal(
|
|
t, "/var/lib/webhooker", cfg.DataDir,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReceiverRateLimit(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
set bool
|
|
value string
|
|
expectError bool
|
|
// sentinel, when set, must be wrapped by the startup
|
|
// error; every error case must additionally name the
|
|
// variable in its message.
|
|
sentinel error
|
|
expected int
|
|
}{
|
|
{
|
|
name: caseUnsetUsesDefault,
|
|
set: false,
|
|
expected: 120,
|
|
},
|
|
{
|
|
name: caseValidValueParsed,
|
|
set: true,
|
|
value: "30",
|
|
expected: 30,
|
|
},
|
|
{
|
|
name: caseUnparseableFails,
|
|
set: true,
|
|
value: "not-a-number",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "zero fails startup",
|
|
set: true,
|
|
value: "0",
|
|
expectError: true,
|
|
sentinel: config.ErrNonPositiveValue,
|
|
},
|
|
{
|
|
name: "negative fails startup",
|
|
set: true,
|
|
value: "-5",
|
|
expectError: true,
|
|
sentinel: config.ErrNonPositiveValue,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
|
|
|
if tt.set {
|
|
t.Setenv("RECEIVER_RATE_LIMIT", tt.value)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv(
|
|
"RECEIVER_RATE_LIMIT",
|
|
))
|
|
}
|
|
|
|
if tt.expectError {
|
|
expectStartupErrorFor(
|
|
t, "RECEIVER_RATE_LIMIT", tt.sentinel,
|
|
)
|
|
} else {
|
|
testReceiverRateLimitSuccess(t, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testReceiverRateLimitSuccess(
|
|
t *testing.T,
|
|
expected int,
|
|
) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
assert.Equal(t, expected, cfg.ReceiverRateLimit)
|
|
}
|
|
|
|
func TestTrustedProxies(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
set bool
|
|
value string
|
|
expectError bool
|
|
expected []string
|
|
}{
|
|
{
|
|
// The default must be "trust nobody": an empty list
|
|
// means forwarded headers are ignored, never that
|
|
// every peer may speak for the client.
|
|
name: caseUnsetUsesDefault,
|
|
set: false,
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "blank value trusts nothing",
|
|
set: true,
|
|
value: " ",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: caseValidValueParsed,
|
|
set: true,
|
|
value: cidrPrivateV4 + ", 192.168.1.7 ,2001:db8::/32",
|
|
expected: []string{
|
|
cidrPrivateV4, "192.168.1.7/32", "2001:db8::/32",
|
|
},
|
|
},
|
|
{
|
|
name: "host bits are masked off",
|
|
set: true,
|
|
value: "10.1.2.3/8",
|
|
expected: []string{cidrPrivateV4},
|
|
},
|
|
{
|
|
// Peer addresses are unmapped before they are
|
|
// matched, so an IPv4-mapped prefix kept in that
|
|
// form could never match anything.
|
|
name: "IPv4-mapped prefix is unmapped",
|
|
set: true,
|
|
value: "::ffff:10.0.0.0/104",
|
|
expected: []string{cidrPrivateV4},
|
|
},
|
|
{
|
|
name: caseUnparseableFails,
|
|
set: true,
|
|
value: cidrPrivateV4 + ",not-an-address",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "out-of-range prefix length fails startup",
|
|
set: true,
|
|
value: "10.0.0.0/33",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
|
|
|
if tt.set {
|
|
t.Setenv("TRUSTED_PROXIES", tt.value)
|
|
} else {
|
|
require.NoError(t, os.Unsetenv("TRUSTED_PROXIES"))
|
|
}
|
|
|
|
if tt.expectError {
|
|
expectStartupErrorFor(
|
|
t, "TRUSTED_PROXIES", config.ErrInvalidCIDR,
|
|
)
|
|
} else {
|
|
testTrustedProxiesSuccess(t, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testTrustedProxiesSuccess(
|
|
t *testing.T,
|
|
expected []string,
|
|
) {
|
|
t.Helper()
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
),
|
|
fx.Populate(&cfg),
|
|
)
|
|
require.NoError(t, app.Err())
|
|
|
|
app.RequireStart()
|
|
|
|
defer app.RequireStop()
|
|
|
|
got := make([]string, 0, len(cfg.TrustedProxies))
|
|
for _, prefix := range cfg.TrustedProxies {
|
|
got = append(got, prefix.String())
|
|
}
|
|
|
|
assert.Equal(t, expected, got)
|
|
}
|
|
|
|
// TestSharedRateLimitBucketWarning covers the startup warning that
|
|
// tells an operator a deployment behind a reverse proxy shares one
|
|
// rate-limit bucket between every client, which makes the admin login
|
|
// remotely deniable. It must fire whenever TRUSTED_PROXIES is empty,
|
|
// in any environment: WEBHOOKER_ENVIRONMENT defaults to dev, so gating
|
|
// on it would silence the warning for exactly the operator who never
|
|
// configured the deployment. It stays quiet once proxies are named.
|
|
func TestSharedRateLimitBucketWarning(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
environment string
|
|
trustedProxies string
|
|
expectWarning bool
|
|
}{
|
|
{
|
|
name: "prod without trusted proxies warns",
|
|
environment: config.EnvironmentProd,
|
|
expectWarning: true,
|
|
},
|
|
{
|
|
name: "prod with trusted proxies is quiet",
|
|
environment: config.EnvironmentProd,
|
|
trustedProxies: cidrPrivateV4,
|
|
expectWarning: false,
|
|
},
|
|
{
|
|
// The default environment. An internet-exposed
|
|
// deployment whose operator never set
|
|
// WEBHOOKER_ENVIRONMENT lands here and has exactly
|
|
// the exposure the warning announces.
|
|
name: "dev without trusted proxies warns",
|
|
environment: config.EnvironmentDev,
|
|
expectWarning: true,
|
|
},
|
|
{
|
|
name: "dev with trusted proxies is quiet",
|
|
environment: config.EnvironmentDev,
|
|
trustedProxies: cidrPrivateV4,
|
|
expectWarning: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Cannot use t.Parallel() here because t.Setenv
|
|
// is incompatible with parallel subtests.
|
|
t.Setenv("WEBHOOKER_ENVIRONMENT", tt.environment)
|
|
|
|
if tt.trustedProxies == "" {
|
|
require.NoError(
|
|
t, os.Unsetenv("TRUSTED_PROXIES"),
|
|
)
|
|
} else {
|
|
t.Setenv("TRUSTED_PROXIES", tt.trustedProxies)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
log := slog.New(slog.NewJSONHandler(
|
|
&buf, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
},
|
|
))
|
|
|
|
require.NoError(
|
|
t,
|
|
config.WarnSharedRateLimitBucketForTest(log),
|
|
)
|
|
|
|
if !tt.expectWarning {
|
|
assert.Empty(t, buf.String())
|
|
|
|
return
|
|
}
|
|
|
|
logged := buf.String()
|
|
|
|
assert.Contains(t, logged, `"level":"WARN"`)
|
|
assert.Contains(t, logged, "TRUSTED_PROXIES")
|
|
assert.Contains(t, logged, "share one bucket")
|
|
assert.Contains(t, logged, "denying the admin login")
|
|
// The text must stay accurate for a developer with
|
|
// nothing in front of the process, where an empty
|
|
// list costs nothing.
|
|
assert.Contains(
|
|
t, logged, "nothing proxying to this process",
|
|
)
|
|
})
|
|
}
|
|
}
|