package memoriatest
import (
"bytes"
"html"
"io"
"log/slog"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"amadan.net/rastrillo/rastrillo"
"amadan.net/rastrillo/rastrillo/db"
memoria "amadan.net/oficina/memoria/internal/memoria"
)
// servedApp is the production app behind rastrillo's production stack:
// memoria.App with real sign-in, mounted as main.go mounts it, completed
// by memoria.ServeOptions and wrapped by rastrillo.Handler — so the CSP,
// the framing headers and the framework's own same-origin checks are the
// ones a deployed instance sends. owners is the admission list; mail is
// captured.
func servedApp(t *testing.T, dbPath string, owners []string) (http.Handler, *capturingMailer) {
t.Helper()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
d, err := db.Open(dbPath, logger)
if err != nil {
t.Fatalf("db.Open: %v", err)
}
t.Cleanup(func() { d.Close() })
mux, a, err := memoria.App(d, testOrigin, testDEK(t), logger)
if err != nil {
t.Fatalf("App: %v", err)
}
mailer := &capturingMailer{}
a.SetOwners(owners)
au, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
Origin: testOrigin, InstanceKey: "admission-gate-instance-key",
Owners: owners, Mailer: mailer,
}, logger, a)
if err != nil {
t.Fatalf("NewAuth: %v", err)
}
a.UseAuth(au)
a.MountAuth(mux, testOrigin)
h := production(t, a.ServeOptions(rastrillo.Options{Logger: logger}, mux))
offlineResolver(t)
return h, mailer
}
// production wraps opts in rastrillo.Handler, the stack Serve runs.
func production(t *testing.T, opts rastrillo.Options) http.Handler {
t.Helper()
h, done, err := rastrillo.Handler(opts)
if err != nil {
t.Fatalf("rastrillo.Handler: %v", err)
}
if done != nil {
t.Cleanup(func() { _ = done() })
}
return h
}
// defaultCSP is rastrillo's own policy at the pinned version, read off a
// bare handler rather than copied, so a tightening upstream is what the
// tests below expect next time without anybody editing them.
func defaultCSP(t *testing.T) string {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
rec := httptest.NewRecorder()
production(t, rastrillo.Options{Mux: mux}).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, testOrigin+"/", nil))
csp := rec.Header().Get("Content-Security-Policy")
if csp == "" {
t.Fatal("a bare rastrillo handler sends no CSP; this measures nothing")
}
return csp
}
// sessionSet reports whether rec set a live session cookie.
func sessionSet(rec *httptest.ResponseRecorder) bool {
for _, c := range rec.Result().Cookies() {
if strings.Contains(strings.ToLower(c.Name), "session") && c.Value != "" && c.MaxAge >= 0 {
return true
}
}
return false
}
// rastrillo #64, end to end through the production stack: opening an
// emailed link spends nothing; the confirm door's Sign in button does.
//
// begin → mail → the link opened twice (a mail scanner's fetch, then the
// person's: both the door, neither a session) → a cross-site POST and a
// POST with no browser evidence, both refused WITHOUT spending the link →
// the door's own POST signs in and the session opens the index → the same
// token again is ?err=expired.
func TestEmailedLinkOpensTheConfirmDoor(t *testing.T) {
h, mailer := servedApp(t, tempDB(t), []string{chromeOwner})
c := newClient(t, h)
if rec := c.do(http.MethodPost, testOrigin+"/auth/begin", url.Values{"address": {chromeOwner}}); rec.Code != http.StatusSeeOther {
t.Fatalf("POST /auth/begin = %d", rec.Code)
}
link := mailer.link()
if link == "" {
t.Fatalf("no magic link was sent: %q", mailer.text())
}
u, err := url.Parse(link)
if err != nil {
t.Fatal(err)
}
// Twice: a scanner, then the person. A GET mints nothing.
var action, token string
for _, who := range []*client{newClient(t, h), c} {
rec := who.get(testOrigin + u.RequestURI())
if rec.Code != http.StatusOK {
t.Fatalf("GET the link = %d, want the confirm door", rec.Code)
}
if sessionSet(rec) {
t.Fatal("opening the link minted a session; the GET must spend nothing")
}
action, token = confirmForm(t, rec.Body.String())
}
if action != "/auth/verify" || token != u.Query().Get("token") {
t.Fatalf("the door posts %q with token %q, want /auth/verify and the link's own token", action, token)
}
post := func(origin, site string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, testOrigin+action, strings.NewReader(url.Values{"token": {token}}.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if origin != "" {
req.Header.Set("Origin", origin)
}
if site != "" {
req.Header.Set("Sec-Fetch-Site", site)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
for name, rec := range map[string]*httptest.ResponseRecorder{
"cross-site": post("https://evil.example", "cross-site"),
"no browser proof": post("", ""),
} {
if rec.Code != http.StatusForbidden || sessionSet(rec) {
t.Errorf("%s POST = %d (session %v), want 403 and no session", name, rec.Code, sessionSet(rec))
}
}
// The link survived both: the door's own POST signs in.
rec := c.postSameOrigin(testOrigin+action, url.Values{"token": {token}})
if rec.Code != http.StatusSeeOther || !sessionSet(rec) {
t.Fatalf("the door's POST = %d (session %v), want 303 and a session — the refused POSTs above spent the link", rec.Code, sessionSet(rec))
}
dest := rec.Header().Get("Location")
for i := 0; i < 3 && dest != "/"; i++ {
dest = location(t, c.get(testOrigin+dest))
}
if dest != "/" {
t.Fatalf("sign-in landed on %q, want /", dest)
}
if got := c.get(testOrigin + "/"); got.Code != http.StatusOK || !strings.Contains(got.Body.String(), "estilo-account") {
t.Fatalf("GET / with the new session = %d, or it is not signed in", got.Code)
}
// Spent: the same token again is ?err=expired, and no session.
replay := newClient(t, h).postSameOrigin(testOrigin+action, url.Values{"token": {token}})
if replay.Code != http.StatusSeeOther || replay.Header().Get("Location") != "/signin?err=expired" || sessionSet(replay) {
t.Errorf("a replayed token = %d %q, want 303 /signin?err=expired and no session", replay.Code, replay.Header().Get("Location"))
}
}
// The confirm door is drawn by Memoria (renderConfirm), inside its own
// layout — and auth's headers survive the render: no-store, the
// strict-origin Referrer-Policy (the URL is a live credential, and the
// layout's stylesheet and script requests would otherwise carry it as
// their Referer), the framing refusals, and BOTH CSP values: rastrillo's
// policy, then auth's added frame-ancestors 'none'.
//
// MUTATIONS (author-chosen, 2026-10-06; an independent pass is owed per
// AGENTS.md): renderConfirm setting Referrer-Policy to
// strict-origin-when-cross-origin, or Set-ing a CSP of its own, each fail
// here. Reverted.
func TestTheConfirmDoorKeepsEveryPolicy(t *testing.T) {
h, _ := servedApp(t, tempDB(t), []string{chromeOwner})
const evil = `">`
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, testOrigin+"/auth/verify?token="+url.QueryEscape(evil), nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET /auth/verify = %d", rec.Code)
}
hd := rec.Header()
for name, want := range map[string]string{
"Cache-Control": "no-store",
"Referrer-Policy": "strict-origin",
"X-Frame-Options": "DENY",
"X-Robots-Tag": "noindex, nofollow",
} {
if got := hd.Get(name); got != want {
t.Errorf("%s = %q, want %q", name, got, want)
}
}
csp := hd.Values("Content-Security-Policy")
if want := []string{defaultCSP(t), "frame-ancestors 'none'"}; strings.Join(csp, "\n") != strings.Join(want, "\n") {
t.Errorf("CSP values = %q, want %q", csp, want)
}
body := rec.Body.String()
_, token := confirmForm(t, body)
if token != evil {
t.Errorf("the door carries token %q, want the link's own", token)
}
if strings.Contains(body, "