Correct release-blocking README and startup-warning inaccuracies (closes #151)
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.
This commit is contained in:
2026-08-17 20:42:42 +00:00
parent c378690977
commit 92f3a016e1
5 changed files with 365 additions and 142 deletions

View File

@@ -628,10 +628,12 @@ func testTrustedProxiesSuccess(
}
// TestSharedRateLimitBucketWarning covers the startup warning that
// tells an operator their production deployment shares one rate-limit
// bucket between every client, which makes the admin login remotely
// deniable. It must fire when TRUSTED_PROXIES is empty in production
// and stay quiet otherwise.
// 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
@@ -651,12 +653,19 @@ func TestSharedRateLimitBucketWarning(t *testing.T) {
expectWarning: false,
},
{
// Development is not required to run behind a
// reverse proxy, so the shared bucket the warning
// describes is not the expected shape there.
name: "dev without trusted proxies is quiet",
// 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: false,
expectWarning: true,
},
{
name: "dev with trusted proxies is quiet",
environment: config.EnvironmentDev,
trustedProxies: cidrPrivateV4,
expectWarning: false,
},
}
@@ -697,8 +706,14 @@ func TestSharedRateLimitBucketWarning(t *testing.T) {
assert.Contains(t, logged, `"level":"WARN"`)
assert.Contains(t, logged, "TRUSTED_PROXIES")
assert.Contains(t, logged, "shares one bucket")
assert.Contains(t, logged, "deny the admin login")
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",
)
})
}
}