All checks were successful
check / check (push) Successful in 3m26s
max_retries was read through parseNonNegativeInt, which returned 0 for any parse failure. On the create form `abc`, `2.7`, `-5` and a twenty-digit number were all accepted with HTTP 200 and stored as 0, and `999999999` was stored verbatim with no ceiling. On the edit form the same input destroyed a working retry configuration: a target delivering with max_retries=2, re-saved with a typo in the field, was silently left at 0 — fire-and-forget on a store-and-forward proxy, with nothing said. A value that is set but unparseable must be rejected loudly. A default belongs only to an absent value. parseMaxRetries makes that distinction explicit: an empty or omitted field yields the caller's fallback (0 at creation, the stored count on edit), and anything else that is not a whole number in range is a 400. Both forms go through one validator, so they cannot come to disagree. The ceiling is 20, which both target templates have always declared as max="20" on the input; only the server never enforced it. Backoff is 2^(n-1) seconds, so attempt 20 is already about six days out, and each attempt writes a delivery_results row the event log then loads and renders. The rejection wording matches the timeout control on the same submission, which already got this right, and names the ceiling when the value is out of range. Existing rows above the ceiling are untouched: they still render on the source and edit pages and still deliver. This is input validation, not a migration. parseNonNegativeInt is removed. Its other two callers read the log page number for a post-action redirect, where falling back to page 1 is correct — it is navigation, not stored configuration, and the action has already completed. They now use pageOrFirst, named for what it does and shared with parsePage on the GET side, so no general silently-coercing int parser is left for a configuration field to reach for.
127 lines
3.7 KiB
Go
127 lines
3.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// SetLogForTest replaces the handler's logger, so the handlers_test
|
|
// package can assert on what a log line actually contains rather than
|
|
// on what it is meant to contain.
|
|
func (s *Handlers) SetLogForTest(log *slog.Logger) {
|
|
s.log = log
|
|
}
|
|
|
|
// MaxRenderedBodyBytesForTest exposes the event log's body cap
|
|
// to the handlers_test package.
|
|
const MaxRenderedBodyBytesForTest = maxRenderedBodyBytes
|
|
|
|
// MaxRenderedResponseBytesForTest exposes the event log's
|
|
// delivery response cap to the handlers_test package.
|
|
const MaxRenderedResponseBytesForTest = maxRenderedResponseBytes
|
|
|
|
// MaxRenderedAttemptsForTest exposes the event log's
|
|
// per-delivery attempt ceiling to the handlers_test package.
|
|
const MaxRenderedAttemptsForTest = maxRenderedAttempts
|
|
|
|
// MaxTargetRetriesForTest exposes the target max_retries ceiling to
|
|
// the handlers_test package, so the tests assert against the constant
|
|
// the handlers enforce rather than a number copied beside it.
|
|
const MaxTargetRetriesForTest = maxTargetRetries
|
|
|
|
// PageOrFirstForTest exposes pageOrFirst for use in the handlers_test
|
|
// package.
|
|
func PageOrFirstForTest(s string) int {
|
|
return pageOrFirst(s)
|
|
}
|
|
|
|
// DummyVerificationsForTest reports how many equivalent-cost
|
|
// verifications were charged for usernames that do not exist. It
|
|
// lets a test prove the anti-enumeration path ran without timing
|
|
// anything.
|
|
func (s *Handlers) DummyVerificationsForTest() uint64 {
|
|
return s.dummyVerifications.Load()
|
|
}
|
|
|
|
// TrimPartialRuneForTest exposes trimPartialRune for use in the
|
|
// handlers_test package.
|
|
func TrimPartialRuneForTest(b []byte) []byte {
|
|
return trimPartialRune(b)
|
|
}
|
|
|
|
// LoadEventLogViewsForTest exposes loadEventsWithDeliveries for
|
|
// use in the handlers_test package. Assertions on the projected
|
|
// body need the bytes as loaded: html/template rewrites invalid
|
|
// UTF-8 on the way out, so the rendered page cannot show whether
|
|
// a binary body survived the projection intact.
|
|
func (s *Handlers) LoadEventLogViewsForTest(
|
|
w http.ResponseWriter,
|
|
webhook database.Webhook,
|
|
page int,
|
|
) []EventLogView {
|
|
views, _, _ := s.loadEventsWithDeliveries(
|
|
w, webhook, nil, page,
|
|
)
|
|
|
|
return views
|
|
}
|
|
|
|
// AddTemplateForTest registers a template under a page name so that
|
|
// the handlers_test package can drive the render path with a
|
|
// template of its own.
|
|
func (s *Handlers) AddTemplateForTest(
|
|
pageTemplate string,
|
|
tmpl *template.Template,
|
|
) {
|
|
s.templates[pageTemplate] = tmpl
|
|
}
|
|
|
|
// RenderTemplateForTest exposes renderTemplate for use in the
|
|
// handlers_test package.
|
|
func (s *Handlers) RenderTemplateForTest(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
pageTemplate string,
|
|
data any,
|
|
) {
|
|
s.renderTemplate(w, r, pageTemplate, data)
|
|
}
|
|
|
|
// BuildSlackTargetConfigForTest exposes
|
|
// buildSlackTargetConfig for use in the handlers_test package.
|
|
func (s *Handlers) BuildSlackTargetConfigForTest(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
targetURL string,
|
|
) (string, error) {
|
|
return s.buildSlackTargetConfig(w, r, targetURL)
|
|
}
|
|
|
|
// BuildHTTPTargetConfigForTest exposes buildHTTPTargetConfig
|
|
// for use in the handlers_test package, taking the form fields
|
|
// an HTTP target's configuration is built from.
|
|
func (s *Handlers) BuildHTTPTargetConfigForTest(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
targetURL, headers, timeout string,
|
|
) (string, error) {
|
|
return s.buildHTTPTargetConfig(w, r, targetFormInput{
|
|
URL: targetURL,
|
|
Headers: headers,
|
|
Timeout: timeout,
|
|
})
|
|
}
|
|
|
|
// BuildDatabaseTargetConfigForTest exposes
|
|
// buildDatabaseTargetConfig for use in the handlers_test
|
|
// package.
|
|
func (s *Handlers) BuildDatabaseTargetConfigForTest(
|
|
w http.ResponseWriter,
|
|
expiry string,
|
|
) (string, error) {
|
|
return s.buildDatabaseTargetConfig(w, expiry)
|
|
}
|