Seed from go-template-repo, renamed to simplexcalc

The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 21:38:57 +00:00
parent e336f46e34
commit f8ce8cef83
79 changed files with 7131 additions and 0 deletions
+560
View File
@@ -0,0 +1,560 @@
package server_test
import (
"encoding/json"
"io"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/simplexcalc/internal/config"
"sneak.berlin/go/simplexcalc/internal/database"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/internal/handlers"
"sneak.berlin/go/simplexcalc/internal/logger"
"sneak.berlin/go/simplexcalc/internal/middleware"
"sneak.berlin/go/simplexcalc/internal/render"
"sneak.berlin/go/simplexcalc/internal/server"
"sneak.berlin/go/simplexcalc/internal/telemetry"
)
// formNameField is the widget form's name input; refererHeader is the
// header gorilla/csrf checks on every state-changing request.
const (
formNameField = "name"
refererHeader = "Referer"
)
// response is a finished exchange: status, headers and the body, read
// and closed before the helper returns. Handing the tests a value
// rather than an *http.Response means no test can leak a connection by
// forgetting to close one.
type response struct {
status int
header http.Header
body string
}
// instance is a running service: a real listener on an ephemeral port,
// a real sqlite database in a temp dir, and the whole middleware chain.
// Nothing is stubbed, because the things most likely to be wrong —
// middleware order, route registration, embedded assets — are exactly
// what a stub would paper over.
type instance struct {
base string
client *http.Client
}
// testConfig is the configuration every instance starts from.
func testConfig(t *testing.T) *config.Config {
t.Helper()
dir := t.TempDir()
return &config.Config{
// Port 0: the OS picks a free one, so parallel tests do not
// fight over a fixed port.
Port: 0,
DataDir: dir,
DBPath: filepath.Join(dir, "test.db"),
Debug: true,
HSTS: false,
MaxRequestBody: 1 << 20,
RequestTimeout: 5 * time.Second,
ShutdownGrace: 2 * time.Second,
CSRFKeyEphemeral: true,
}
}
// build constructs the graph by hand. fx would do the same wiring; done
// explicitly, a constructor that starts needing a new dependency shows
// up here as a compile error rather than as a runtime resolution
// failure inside a container.
func build(t *testing.T, lc *fxtest.Lifecycle, cfg *config.Config) *server.Server {
t.Helper()
g := &globals.Globals{Appname: "simplexcalc", Version: "test", Buildarch: "amd64"}
log, err := logger.New(lc, logger.Params{Globals: g, Output: io.Discard})
if err != nil {
t.Fatalf("logger: %v", err)
}
db, err := database.New(lc, database.Params{Config: cfg, Logger: log})
if err != nil {
t.Fatalf("database: %v", err)
}
sentry, err := telemetry.NewSentry(lc, telemetry.SentryParams{
Config: cfg, Globals: g, Logger: log,
})
if err != nil {
t.Fatalf("sentry: %v", err)
}
metrics, err := telemetry.NewMetrics(telemetry.MetricsParams{Config: cfg})
if err != nil {
t.Fatalf("metrics: %v", err)
}
renderer, err := render.New(lc, render.Params{Globals: g})
if err != nil {
t.Fatalf("render: %v", err)
}
mw, err := middleware.New(lc, middleware.Params{
Config: cfg, Logger: log, Sentry: sentry, Metrics: metrics,
})
if err != nil {
t.Fatalf("middleware: %v", err)
}
h, err := handlers.New(lc, handlers.Params{
Config: cfg, Globals: g, Logger: log,
Database: db, Renderer: renderer, Sentry: sentry,
})
if err != nil {
t.Fatalf("handlers: %v", err)
}
srv, err := server.New(lc, server.Params{
Logger: log, Globals: g, Config: cfg,
Middleware: mw, Handlers: h, Metrics: metrics,
})
if err != nil {
t.Fatalf("server: %v", err)
}
return srv
}
// newInstance starts the service and registers its shutdown.
func newInstance(t *testing.T, mutate func(*config.Config)) *instance {
t.Helper()
cfg := testConfig(t)
if mutate != nil {
mutate(cfg)
}
lc := fxtest.NewLifecycle(t)
srv := build(t, lc, cfg)
lc.RequireStart()
t.Cleanup(lc.RequireStop)
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("cookie jar: %v", err)
}
// The listener binds the unspecified address, so Addr() reads
// "[::]:PORT". Dialling that works, but a cookie jar keyed on "::"
// does not, and the CSRF flow depends on cookies coming back — so
// the tests talk to the loopback address explicitly.
_, port, err := net.SplitHostPort(srv.Addr())
if err != nil {
t.Fatalf("parsing listen address %q: %v", srv.Addr(), err)
}
return &instance{
base: "http://127.0.0.1:" + port,
client: &http.Client{
Jar: jar,
Timeout: 10 * time.Second,
// The redirect after a successful POST is under test, so
// it must not be followed silently.
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
},
}
}
// host is the Host header value the service is reached by.
func (i *instance) host() string {
return strings.TrimPrefix(i.base, "http://")
}
func (i *instance) get(t *testing.T, path string) response {
t.Helper()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, i.base+path, nil)
if err != nil {
t.Fatalf("building GET %s: %v", path, err)
}
return i.do(t, req)
}
// post submits a form the way a browser does — including the Referer,
// which gorilla/csrf checks against the request's Host on every
// state-changing request. Omitting it is itself a CSRF rejection, so a
// test that leaves it out proves nothing about the token.
func (i *instance) post(t *testing.T, path string, form url.Values) response {
t.Helper()
return i.postWithHeaders(t, path, form, map[string]string{
refererHeader: i.base + "/",
})
}
// postWithHeaders is post with the headers stated explicitly, for the
// reverse-proxy cases.
func (i *instance) postWithHeaders(
t *testing.T, path string, form url.Values, headers map[string]string,
) response {
t.Helper()
req, err := http.NewRequestWithContext(
t.Context(), http.MethodPost, i.base+path, strings.NewReader(form.Encode()),
)
if err != nil {
t.Fatalf("building POST %s: %v", path, err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for k, v := range headers {
req.Header.Set(k, v)
}
return i.do(t, req)
}
func (i *instance) do(t *testing.T, req *http.Request) response {
t.Helper()
resp, err := i.client.Do(req)
if err != nil {
t.Fatalf("%s %s: %v", req.Method, req.URL.Path, err)
}
body, readErr := io.ReadAll(resp.Body)
closeErr := resp.Body.Close()
if closeErr != nil {
t.Errorf("closing response body: %v", closeErr)
}
if readErr != nil {
t.Fatalf("reading %s %s: %v", req.Method, req.URL.Path, readErr)
}
return response{status: resp.StatusCode, header: resp.Header, body: string(body)}
}
// TestServesPageFromEmbeddedTemplate is one of the definition-of-done
// claims: a running process answers / from the template compiled into
// it.
func TestServesPageFromEmbeddedTemplate(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
resp := i.get(t, "/")
if resp.status != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.status)
}
for _, want := range []string{"<!doctype html>", "<nav>", "<footer>", "widgets ("} {
if !strings.Contains(resp.body, want) {
t.Errorf("page is missing %q", want)
}
}
if resp.header.Get("X-Content-Type-Options") != "nosniff" {
t.Error("security headers are missing from a normal page response")
}
if resp.header.Get(middleware.RequestIDHeader) == "" {
t.Error("no request id on the response")
}
}
// TestServesEmbeddedStaticAsset, with the cache policy the route sets.
func TestServesEmbeddedStaticAsset(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
resp := i.get(t, "/static/css/style.css")
if resp.status != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.status)
}
if !strings.Contains(resp.body, "--fg:") {
t.Error("the served asset is not the embedded stylesheet")
}
if !strings.Contains(resp.header.Get("Cache-Control"), "immutable") {
t.Errorf("Cache-Control = %q", resp.header.Get("Cache-Control"))
}
}
// TestStaticDirectoriesAreNotListed: enumerating what the binary ships
// is a capability no caller needs.
func TestStaticDirectoriesAreNotListed(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
for _, path := range []string{"/static/", "/static/css/"} {
resp := i.get(t, path)
if resp.status != http.StatusNotFound {
t.Errorf("GET %s: status = %d, want 404", path, resp.status)
}
if strings.Contains(resp.body, "style.css") {
t.Errorf("GET %s listed the directory contents", path)
}
}
}
// TestHealthcheckReportsMigratedSchema proves the embedded migrations
// ran during startup, from a directory that was empty a moment ago.
func TestHealthcheckReportsMigratedSchema(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
resp := i.get(t, "/.well-known/healthcheck.json")
if resp.status != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.status)
}
var health handlers.HealthResponse
err := json.Unmarshal([]byte(resp.body), &health)
if err != nil {
t.Fatalf("decoding healthcheck: %v (body %q)", err, resp.body)
}
if !health.OK || !health.DatabaseOK {
t.Errorf("healthcheck reports unhealthy: %+v", health)
}
if health.SchemaVersion != 1 {
t.Errorf("schema_version = %d, want 1", health.SchemaVersion)
}
}
// TestMetricsEndpointIsServedAndGated covers both halves of the metrics
// requirement in one running service.
func TestMetricsEndpointIsServedAndGated(t *testing.T) {
t.Parallel()
t.Run("open when unconfigured", func(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
// One request first: the middleware records a request after
// the response is written, so a scrape that is the very first
// request to the process legitimately has no HTTP series in it
// yet.
i.get(t, "/")
resp := i.get(t, "/metrics")
if resp.status != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.status)
}
want := `http_requests_total{code="200",method="GET",route="/"}`
if !strings.Contains(resp.body, want) {
t.Errorf("the scrape does not report the request that preceded it:\n%s", resp.body)
}
})
t.Run("gated when configured", func(t *testing.T) {
t.Parallel()
i := newInstance(t, func(c *config.Config) {
c.MetricsUser = "scraper"
c.MetricsPassword = "hunter2"
})
resp := i.get(t, "/metrics")
if resp.status != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", resp.status)
}
})
}
// TestUnknownRouteRendersErrorPage: a 404 still carries the site's
// headers and chrome rather than net/http's bare text.
func TestUnknownRouteRendersErrorPage(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
resp := i.get(t, "/no-such-page")
if resp.status != http.StatusNotFound {
t.Fatalf("status = %d, want 404", resp.status)
}
if !strings.Contains(resp.body, "No such page") ||
!strings.Contains(resp.body, "<nav>") {
t.Errorf("404 is not the rendered error page:\n%s", resp.body)
}
}
// TestPanicIsAnsweredWith500 exercises the recovery middleware through
// the whole stack, on the debug-only route.
func TestPanicIsAnsweredWith500(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
resp := i.get(t, "/debug/panic")
if resp.status != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.status)
}
if strings.Contains(resp.body, "deliberate panic") {
t.Error("the panic value reached the client")
}
}
// TestDebugRouteAbsentWithoutDebug: the panic route is a denial-of-
// service primitive, so it must not exist in a normal deployment.
func TestDebugRouteAbsentWithoutDebug(t *testing.T) {
t.Parallel()
i := newInstance(t, func(c *config.Config) { c.Debug = false })
resp := i.get(t, "/debug/panic")
if resp.status != http.StatusNotFound {
t.Errorf("status = %d, want 404 with DEBUG off", resp.status)
}
}
// csrfField finds the hidden token in a rendered form.
var csrfField = regexp.MustCompile(
`<input type="hidden" name="gorilla\.csrf\.Token" value="([^"]+)"`,
)
// token fetches the index page and returns the CSRF token its form
// carries.
func (i *instance) token(t *testing.T) string {
t.Helper()
page := i.get(t, "/")
match := csrfField.FindStringSubmatch(page.body)
if match == nil {
t.Fatalf("no CSRF field in the rendered form:\n%s", page.body)
}
return match[1]
}
// TestCSRFProtectsStateChange is the whole point of the CSRF
// middleware: the same POST must be refused without a token and
// accepted with one.
func TestCSRFProtectsStateChange(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
// A well-formed cross-site request: everything a browser would
// send except the token.
resp := i.post(t, "/widgets", url.Values{formNameField: {"unauthorised"}})
if resp.status != http.StatusForbidden {
t.Fatalf("POST without a CSRF token: status = %d, want 403", resp.status)
}
resp = i.post(t, "/widgets", url.Values{
formNameField: {"authorised"},
"size": {"4KiB"},
"gorilla.csrf.Token": {i.token(t)},
})
if resp.status != http.StatusSeeOther {
t.Fatalf("POST with a CSRF token: status = %d, want 303", resp.status)
}
// And the write actually happened.
after := i.get(t, "/")
if !strings.Contains(after.body, "authorised") {
t.Error("the created widget is not on the page")
}
if !strings.Contains(after.body, "4.0 KiB") {
t.Error("the size was not parsed and rendered")
}
if strings.Contains(after.body, "unauthorised") {
t.Error("the rejected POST created a widget anyway")
}
}
// TestCSRFBehindTLSTerminatingProxy is the deployment shape this
// service actually runs in: the browser speaks HTTPS to a proxy and the
// proxy speaks plaintext HTTP here, saying so with X-Forwarded-Proto.
// The token must still be accepted, with the stricter TLS-side Referer
// rules applied to the browser's real scheme rather than to the
// connection's.
func TestCSRFBehindTLSTerminatingProxy(t *testing.T) {
t.Parallel()
i := newInstance(t, nil)
form := url.Values{
formNameField: {"through the proxy"},
"gorilla.csrf.Token": {i.token(t)},
}
// A cleartext Referer on a connection the proxy says was HTTPS is
// exactly the machine-in-the-middle case the strict check exists
// for, and must be refused.
resp := i.postWithHeaders(t, "/widgets", form, map[string]string{
"X-Forwarded-Proto": "https",
refererHeader: "http://" + i.host() + "/",
})
if resp.status != http.StatusForbidden {
t.Errorf("cleartext referer under X-Forwarded-Proto=https: status = %d, want 403",
resp.status)
}
// The genuine article: a same-origin HTTPS referer.
resp = i.postWithHeaders(t, "/widgets", form, map[string]string{
"X-Forwarded-Proto": "https",
refererHeader: "https://" + i.host() + "/",
})
if resp.status != http.StatusSeeOther {
t.Errorf("valid submission through a TLS-terminating proxy: status = %d, want 303",
resp.status)
}
}
// TestOversizedRequestIsRefused: the body cap is wired into the running
// chain, not just unit-tested in isolation.
func TestOversizedRequestIsRefused(t *testing.T) {
t.Parallel()
i := newInstance(t, func(c *config.Config) { c.MaxRequestBody = 1024 })
// The cap is enforced ahead of CSRF in the chain, so an oversized
// body is refused as oversized rather than as untokened.
resp := i.post(t, "/widgets",
url.Values{formNameField: {strings.Repeat("x", 4096)}})
if resp.status != http.StatusRequestEntityTooLarge {
t.Errorf("status = %d, want 413", resp.status)
}
}