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.
91 lines
3.1 KiB
Go
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
|
|
}
|