Files
member-console/internal/testkit/authtest/authtest.go
T
cgalo5758 eb4a7552bd Pin every route's pattern and answer through the router
No production file changes. authtest gains Cookie, which commits a session
and returns its cookie, for a handler wrapped in the session middleware,
such as the whole router, where Get and Post cannot put the session on the
request's context. docs/testing.md names it and the kit table's authtest
row says "signed-in sessions, their cookies, and in-process requests".

internal/server/new_handler_test.go (no database) builds the router from a
configured profile and a bare one (no Stripe key), with a fake route mount
and two UI mounts, and adds five tests. TestNewHandlerRouteTable writes one
block per registered pattern (the 97 literal HandleFunc patterns the lint
scan finds in internal/server and internal/auth, plus the two static-file
Handle registrations and the fake mount's three routes) to
testdata/routes.golden: the pattern the mux resolves a probe path to, and
the answer to an anonymous request, a cross-site request for a method that
changes state, and a signed-in member where the handler answers before any
read; then eight extra probes and the bare profile's checkout row. The
golden was written once with -update and read against the code: every
operator row answers 302 /login anonymously and 403 Forbidden to the
member, the cross-site writes answer the CSRF refusal except the fake
webhook (204), the two wrong methods and the bare checkout resolve to / and
the styled 404, and the body one byte over 1 MiB answers 413.
TestNewHandlerTrustsTheBaseURLOrigin, TestNewHandlerRefusedSignInIsStyled,
TestNewHandlerRefusesAMountTemplateOutsideItsKey and
TestNewHandlerPassesDepsToMounts cover the base URL's origin, the styled
refused sign-in, the key-prefix refusal and the Deps a mount is handed.

Shown to change no output: no production file is touched, and the names and
results of internal/server and internal/testkit equal the parent's apart
from the five new tests, which pass three runs in a row. The 47 breaks of
the plan, each applied to the new side after a green control run, are all
caught.

Left unprobed:
- The answers the golden records that code outside the router produces
  (each handler's own answer, the auth handlers, the operator role guard,
  each middleware's behaviour): each has its own tests, and no edit to the
  router changes them; the golden holds them so a change shows.
- middleware.Timeout(32s): a short value races fast handlers, and a probe of
  the real value waits 32 seconds.
- middleware.Logging and the order of RequestID before it: they write a log
  line no test reads.
- The http.Server fields: Start keeps them, and nothing builds the server in
  a test.
- The dashboard's root-only check: a later commit probes it.
- The real integrations' routes (FedWiki, the Stripe webhook, Discourse):
  they mount from cmd/start.go, and the fake mount stands in.
- The Config cmd/start.go builds (including StripeKeyMode and BaseURL) and
  the Start call with its exit path: no test builds the command's Config;
  these tests build their own.
2026-10-09 21:23:00 -05:00

91 lines
3.1 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package authtest builds the signed-in sessions tests hand to handlers. A
// session has the keys and values a completed sign-in writes, because
// SignIn writes them through auth.PutSignIn, the function the callback
// handler calls. Get and Post run a handler in process with that session on
// the request, and send no htmx headers, so a handler answers as it does to a
// native request.
package authtest
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/alexedwards/scs/v2"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
)
// NewSessionManager returns an in-memory session manager, the one every test
// that signs in builds.
func NewSessionManager() *scs.SessionManager {
return scs.New()
}
// SignIn loads a session on sm, writes s into it as a sign-in does, and
// returns the context that carries it.
func SignIn(tb testing.TB, sm *scs.SessionManager, s auth.SignIn) context.Context {
tb.Helper()
ctx, err := sm.Load(context.Background(), "")
if err != nil {
tb.Fatalf("load session: %v", err)
}
auth.PutSignIn(ctx, sm, s, time.Now())
return ctx
}
// Cookie commits the session ctx carries, as the session middleware does at
// the end of a request, and returns the cookie a browser sends back with it.
// A test hands it to a handler wrapped in sm.LoadAndSave, such as the
// server's whole router, where Get and Post cannot put the session on the
// request's context themselves.
func Cookie(tb testing.TB, sm *scs.SessionManager, ctx context.Context) *http.Cookie {
tb.Helper()
token, _, err := sm.Commit(ctx)
if err != nil {
tb.Fatalf("commit session: %v", err)
}
return &http.Cookie{Name: sm.Cookie.Name, Value: token}
}
// RequestOpt adjusts a request before the handler runs.
type RequestOpt func(*http.Request)
// PathValue sets a path parameter, as http.ServeMux does for a registered
// route.
func PathValue(name, value string) RequestOpt {
return func(r *http.Request) { r.SetPathValue(name, value) }
}
// Get runs h on a GET of path that carries ctx, the session context SignIn
// returned, and returns the recorder.
func Get(tb testing.TB, ctx context.Context, h http.HandlerFunc, path string, opts ...RequestOpt) *httptest.ResponseRecorder {
tb.Helper()
return serve(h, httptest.NewRequestWithContext(ctx, http.MethodGet, path, nil), opts)
}
// Post runs h on a POST of path that carries ctx and the URL-encoded form,
// and returns the recorder.
func Post(tb testing.TB, ctx context.Context, h http.HandlerFunc, path string, form url.Values, opts ...RequestOpt) *httptest.ResponseRecorder {
tb.Helper()
req := httptest.NewRequestWithContext(ctx, http.MethodPost, path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return serve(h, req, opts)
}
func serve(h http.HandlerFunc, req *http.Request, opts []RequestOpt) *httptest.ResponseRecorder {
for _, opt := range opts {
opt(req)
}
rec := httptest.NewRecorder()
h(rec, req)
return rec
}