oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package memoriatest
2
3import (
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "testing"
9)
10
11// B5 (polish audit, 2026-09-23): the sign-in page said nothing after any
12// sign-in attempt. It switched on ?outcome=, a word nothing writes;
13// rastrillo's auth handlers and memoria's own beginGuard redirect with
14// ?sent=1 and ?err=rate|address|expired|keymail|1. So "Send me a link"
15// came back to the same blank form, and a rate limit, an unparseable
16// address and a dead link were all silent.
17//
18// Every case here drives the REAL flow and follows the Location it was
19// given — never a hand-built query. A hand-built query is written from
20// the same model as the switch that reads it, which is exactly how the
21// old ?outcome= test-free code agreed with itself for months. Each case
22// also asserts the Location's query, so if rastrillo renames a code the
23// failure names the new spelling rather than a missing sentence.
24//
25// Verified red 2026-09-23 against the old localSignin (switch on
26// "outcome"): every subtest failed with the blank form.
27
28// followToSignin performs the redirect a browser would and returns the
29// page body. It refuses anything that is not a redirect to /signin, so a
30// changed status cannot pass as "the page rendered without the flash".
31func followToSignin(t *testing.T, h http.Handler, rr *httptest.ResponseRecorder, wantQuery string) string {
32 t.Helper()
33 if rr.Code != http.StatusSeeOther && rr.Code != http.StatusFound {
34 t.Fatalf("status = %d, want a redirect to the sign-in page; body %q", rr.Code, rr.Body.String())
35 }
36 loc := rr.Header().Get("Location")
37 if !strings.HasPrefix(loc, "/signin?") {
38 t.Fatalf("Location = %q, want /signin?…", loc)
39 }
40 if got := strings.TrimPrefix(loc, "/signin?"); got != wantQuery {
41 t.Fatalf("Location query = %q, want %q — rastrillo's vocabulary moved; "+
42 "signinOutcome must read the new word", got, wantQuery)
43 }
44 req := httptest.NewRequest(http.MethodGet, loc, nil)
45 page := httptest.NewRecorder()
46 h.ServeHTTP(page, req)
47 if page.Code != http.StatusOK {
48 t.Fatalf("GET %s = %d", loc, page.Code)
49 }
50 b, _ := io.ReadAll(page.Body)
51 return string(b)
52}
53
54func assertFlash(t *testing.T, body, tone, sentence string) {
55 t.Helper()
56 if !strings.Contains(body, `rst-tone="`+tone+`"`) {
57 t.Errorf("no %s-toned callout on the page the redirect landed on", tone)
58 }
59 if !strings.Contains(body, sentence) {
60 t.Errorf("page does not say %q", sentence)
61 }
62}
63
64func TestSigninPageSaysWhatHappened(t *testing.T) {
65 t.Run("a sent link says check your email", func(t *testing.T) {
66 // Both writers of ?sent=1: rastrillo after mailing an owner's
67 // link, and beginGuard answering a stranger with the owner's
68 // answer by design.
69 //
70 // Since the sign-in door (2026-09-29) a sent link is the door's
71 // own check-your-email notice (estilo-signin-sent), which REPLACES
72 // the form rather than toning a flash above it — so the state
73 // asserted is the notice, with no form under it.
74 var bodies []string
75 for _, address := range []string{dialTestOwner, "stranger@attacker.example"} {
76 h, _ := signinApp(t)
77 rr := beginSignin(t, h, address)
78 body := followToSignin(t, h, rr, "sent=1")
79 if !strings.Contains(body, `<p estilo-signin-body role="status">If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.</p>`) {
80 t.Errorf("the sent page is not the door's check-your-email notice:\n%s", body)
81 }
82 if !strings.Contains(body, "<h1>Check your email</h1>") {
83 t.Error("the sent notice does not say check your email")
84 }
85 if !strings.Contains(body, "<title>Check your email · Memoria</title>") {
86 t.Error("the sent page's tab title is not \"Check your email · Memoria\"")
87 }
88 if !strings.Contains(body, `<a href="/signin">Use a different address</a>`) {
89 t.Errorf("the sent notice does not link back to the form as \"Use a different address\":\n%s", body)
90 }
91 if strings.Contains(body, `action="/auth/begin"`) {
92 t.Error("the sign-in form is still on the page after a link was sent")
93 }
94 bodies = append(bodies, body)
95 }
96 // Anti-oracle: an owner and a stranger land on the same page,
97 // byte for byte.
98 if bodies[0] != bodies[1] {
99 t.Error("the sent page differs between an owner and a stranger; the page is an oracle for who the owners are")
100 }
101 })
102
103 t.Run("an address rastrillo cannot parse says so", func(t *testing.T) {
104 // Admitted by the owners list, refused by signin's parser: a
105 // dotless domain. Only an owner reaches rastrillo at all.
106 const owner = "paul@localhost"
107 h, _ := signinAppWithOwners(t, []string{owner})
108 rr := beginSignin(t, h, owner)
109 body := followToSignin(t, h, rr, "err=address")
110 // In the field's own error slot, not a banner (spec §9.2).
111 if !strings.Contains(body, "That doesn’t look like an email address.") || strings.Contains(body, `rst-tone="negative"`) {
112 t.Errorf("the address error is not in the field's slot:\n%s", body)
113 }
114 })
115
116 t.Run("a rate-limited address says wait", func(t *testing.T) {
117 // signin's per-address budget, spent on an owner whose links
118 // are being sent (the stub mailer takes them).
119 h, _ := signinApp(t)
120 var rr *httptest.ResponseRecorder
121 for i := 0; i < 20; i++ {
122 rr = beginSignin(t, h, dialTestOwner)
123 if strings.Contains(rr.Header().Get("Location"), "err=rate") {
124 break
125 }
126 }
127 body := followToSignin(t, h, rr, "err=rate")
128 assertFlash(t, body, "negative", "Too many attempts. Wait a few minutes, then try again.")
129 })
130
131 t.Run("a dead magic link says it expired", func(t *testing.T) {
132 // The GET is the confirm door and spends nothing, so a dead
133 // token is only found dead at the POST its button makes.
134 h, _ := signinApp(t)
135 rr := newClient(t, h).openLink("/auth/verify?token=not-a-real-token")
136 body := followToSignin(t, h, rr, "err=expired")
137 assertFlash(t, body, "negative", "expired or was already used")
138 })
139
140 t.Run("a keymail return with no pending cookie says it expired", func(t *testing.T) {
141 h, _ := signinApp(t)
142 req := httptest.NewRequest(http.MethodGet, "/auth/callback?code=x&state=y", nil)
143 rr := httptest.NewRecorder()
144 h.ServeHTTP(rr, req)
145 body := followToSignin(t, h, rr, "err=expired")
146 assertFlash(t, body, "negative", "expired or was already used")
147 })
148
149 t.Run("a plain visit says nothing", func(t *testing.T) {
150 h, _ := signinApp(t)
151 rr := httptest.NewRecorder()
152 h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/signin", nil))
153 if strings.Contains(rr.Body.String(), "rst-callout") {
154 t.Error("a sign-in page nobody submitted carries a callout")
155 }
156 })
157}
158