oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slidestest
2
3import (
4 "fmt"
5 "html"
6 "io"
7 "net/http"
8 "net/url"
9 "os"
10 "regexp"
11 "runtime"
12 "strings"
13 "testing"
14 "time"
15
16 "amadan.net/oficina/slides/internal/slides/keymailtest"
17)
18
19// TestMain keeps every sign-in in this package inside the process: no
20// DNS query or HTTPS probe made by Keymail classification leaves it
21// (keymailtest's package comment says how). An address at a domain no
22// test registered classifies as plain email.
23func TestMain(m *testing.M) {
24 restore, err := keymailtest.Install()
25 if err != nil {
26 fmt.Fprintln(os.Stderr, "keymailtest.Install:", err)
27 os.Exit(1)
28 }
29 code := m.Run()
30 // No test may leave a sign-in mail worker running: every app and
31 // every queue a test builds is closed in its cleanup.
32 if code == 0 {
33 deadline := time.Now().Add(5 * time.Second)
34 for mailWorkers() > 0 && time.Now().Before(deadline) {
35 time.Sleep(10 * time.Millisecond)
36 }
37 if n := mailWorkers(); n > 0 {
38 fmt.Fprintf(os.Stderr, "FAIL: %d sign-in mail queue goroutines still running after the tests\n", n)
39 code = 1
40 }
41 }
42 restore()
43 os.Exit(code)
44}
45
46// The Keymail deployment these tests sign in through: keymail.dev's
47// shape, a domain that is its own Keymail (no `_keymail` record).
48const (
49 keymailHost = "keymail.slides.test"
50 keymailMember = "paul@" + keymailHost
51 keymailStranger = "stranger@" + keymailHost
52)
53
54// newKeymailApp is newApp with keymailMember on the allowlist beside
55// allowedAddress, and a Keymail deployment holding both keymail
56// addresses — one permitted here, one not.
57func newKeymailApp(t *testing.T) (*testApp, *keymailtest.Server) {
58 t.Helper()
59 km := keymailtest.Serve(t, keymailHost, keymailMember, keymailStranger)
60 cfg, mailer := newConfig(t)
61 cfg.AllowedAddresses = append(cfg.AllowedAddresses, keymailMember)
62 return newAppWith(t, cfg, mailer), km
63}
64
65var authorizeHref = regexp.MustCompile(`href="(https://[^"]+/oauth/authorize\?[^"]+)"`)
66
67// continuation asserts rec is the page between the form and Keymail —
68// a 200 document, never a redirect — and returns the authorize URL it
69// carries.
70func continuation(t *testing.T, label string, rec interface {
71 Result() *http.Response
72}, body string) *url.URL {
73 t.Helper()
74 res := rec.Result()
75 if res.StatusCode != http.StatusOK {
76 t.Fatalf("%s: status %d, want 200 — the hop to Keymail must be a page, not a redirect from the form POST (Safari applies form-action to it)", label, res.StatusCode)
77 }
78 if loc := res.Header.Get("Location"); loc != "" {
79 t.Fatalf("%s: Location %q, want none", label, loc)
80 }
81 m := authorizeHref.FindStringSubmatch(body)
82 if m == nil {
83 t.Fatalf("%s: no Keymail authorize link on the page:\n%s", label, body)
84 }
85 u, err := url.Parse(html.UnescapeString(m[1]))
86 if err != nil {
87 t.Fatalf("%s: parse authorize link: %v", label, err)
88 }
89 if u.Host != keymailHost {
90 t.Errorf("%s: authorize link goes to %q, want %q", label, u.Host, keymailHost)
91 }
92 if !strings.Contains(body, `http-equiv="refresh"`) {
93 t.Errorf("%s: the continuation does not navigate on by itself (no meta refresh)", label)
94 }
95 return u
96}
97
98// TestKeymailAddressGoesToKeymailWhetherPermittedOrNot is the oracle
99// test. Whether an address goes to Keymail depends on its domain alone:
100// a permitted and a non-permitted Keymail address get the same first
101// answer, a continuation page to Keymail, so the answer says nothing
102// about the allowlist. Neither is mailed anything.
103func TestKeymailAddressGoesToKeymailWhetherPermittedOrNot(t *testing.T) {
104 app, km := newKeymailApp(t)
105
106 member := post(t, app, "/signin", url.Values{"address": {keymailMember}})
107 stranger := post(t, app, "/signin", url.Values{"address": {keymailStranger}})
108
109 mu := continuation(t, "permitted Keymail address", member, member.Body.String())
110 su := continuation(t, "non-permitted Keymail address", stranger, stranger.Body.String())
111
112 // The same page but for the per-attempt state and PKCE challenge.
113 mask := func(body string, u *url.URL) string {
114 return strings.ReplaceAll(body, html.EscapeString(u.String()), "AUTHORIZE")
115 }
116 if a, b := mask(member.Body.String(), mu), mask(stranger.Body.String(), su); a != b {
117 t.Errorf("the two Keymail answers differ beyond the authorize URL:\npermitted:\n%s\nnot permitted:\n%s", a, b)
118 }
119 cookieNames := func(res *http.Response) string {
120 var n []string
121 for _, c := range res.Cookies() {
122 n = append(n, c.Name)
123 }
124 return strings.Join(n, ",")
125 }
126 if a, b := cookieNames(member.Result()), cookieNames(stranger.Result()); a != b {
127 t.Errorf("cookies differ: permitted %q, not permitted %q", a, b)
128 }
129 if n := len(app.mailer.messages()); n != 0 {
130 t.Errorf("mailer captured %d messages, want 0: a Keymail address is never mailed", n)
131 }
132 if n := km.Lookups(); n != 2 {
133 t.Errorf("Keymail saw %d federation lookups, want 2 (both addresses classified)", n)
134 }
135}
136
137// TestNonPermittedPlainAddressIsAnsweredSentAndNotMailed: an address at
138// a domain with no Keymail still gets the ordinary ?sent=1, permitted
139// or not, and only the permitted one is mailed.
140func TestNonPermittedPlainAddressIsAnsweredSentAndNotMailed(t *testing.T) {
141 app, _ := newKeymailApp(t)
142 for _, address := range []string{deniedAddress, allowedAddress} {
143 rec := post(t, app, "/signin", url.Values{"address": {address}})
144 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
145 t.Fatalf("POST /signin %q: %d %q, want 303 /signin?sent=1", address, rec.Code, rec.Header().Get("Location"))
146 }
147 }
148 msgs := app.mailer.messages()
149 if len(msgs) != 1 || msgs[0].To != allowedAddress {
150 t.Fatalf("mail sent = %+v, want exactly one, to %q", msgs, allowedAddress)
151 }
152}
153
154// TestKeymailFailureFallsBackToAnEmailLink: rastrillo answers a failed
155// Keymail exchange with ?force=1&err=keymail. That page must carry
156// force back so the next POST mails a link instead of returning to the
157// Keymail that just failed — Paul is never stuck. The ordinary form
158// carries no force at all.
159func TestKeymailFailureFallsBackToAnEmailLink(t *testing.T) {
160 app, km := newKeymailApp(t)
161 forced := `name="force" value="1"`
162
163 if body := get(t, app, "/signin").Body.String(); strings.Contains(body, forced) {
164 t.Fatalf("the ordinary sign-in form forces an email link; Keymail addresses would never reach Keymail")
165 }
166 page := get(t, app, "/signin?force=1&err=keymail")
167 if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), forced) {
168 t.Fatalf("GET /signin?force=1&err=keymail: %d, carries force=%v", page.Code, strings.Contains(page.Body.String(), forced))
169 }
170
171 rec := post(t, app, "/signin", url.Values{"address": {keymailMember}, "force": {"1"}})
172 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
173 t.Fatalf("forced POST: %d %q, want 303 /signin?sent=1", rec.Code, rec.Header().Get("Location"))
174 }
175 msgs := app.mailer.messages()
176 if len(msgs) != 1 || msgs[0].To != keymailMember || magicLinkPattern.FindString(msgs[0].Body) == "" {
177 t.Fatalf("mail sent = %+v, want one magic link to %q", msgs, keymailMember)
178 }
179 if n := km.Lookups(); n != 0 {
180 t.Errorf("a forced sign-in classified the address (%d lookups)", n)
181 }
182}
183
184// keymailRoundTrip runs POST /signin → continuation → Keymail approves
185// → GET /auth/callback for address and returns the callback's answer
186// and the cookies collected along the way.
187func keymailRoundTrip(t *testing.T, app *testApp, km *keymailtest.Server, address string) (*http.Response, []*http.Cookie) {
188 t.Helper()
189 begin := post(t, app, "/signin", url.Values{"address": {address}})
190 u := continuation(t, address, begin, begin.Body.String())
191 cookies := begin.Result().Cookies()
192 cb := get(t, app, "/auth/callback?"+url.Values{"state": {u.Query().Get("state")}, "code": {km.Approve(address)}}.Encode(), cookies...)
193 return cb.Result(), append(cookies, cb.Result().Cookies()...)
194}
195
196// TestKeymailMemberIsSignedIn: the whole Keymail round trip for a
197// permitted address ends signed in.
198func TestKeymailMemberIsSignedIn(t *testing.T) {
199 app, km := newKeymailApp(t)
200 res, cookies := keymailRoundTrip(t, app, km, keymailMember)
201 if res.StatusCode != http.StatusSeeOther {
202 t.Fatalf("callback: %d, want 303 into the app", res.StatusCode)
203 }
204 if rec := get(t, app, "/", cookies...); rec.Code != http.StatusOK {
205 t.Fatalf("GET / after Keymail sign-in: %d, want 200", rec.Code)
206 }
207}
208
209// TestKeymailRefusalIsTheDoor: a non-permitted address that Keymail
210// has verified is refused AFTER verification, and the refusal is the
211// sign-in door, rendered right there with the error and a form for a
212// different address by rauth's Refused hook (refused) — not rastrillo's
213// bare-text 403. No session is minted. And the sentence cannot be summoned by anybody else: visiting
214// /signin?err=refused shows no such thing (Astra, round 3), because the
215// refusal is never a query parameter.
216func TestKeymailRefusalIsTheDoor(t *testing.T) {
217 app, km := newKeymailApp(t)
218 res, cookies := keymailRoundTrip(t, app, km, keymailStranger)
219 body, _ := io.ReadAll(res.Body)
220 if res.StatusCode != http.StatusForbidden || res.Header.Get("Location") != "" {
221 t.Fatalf("callback for a non-permitted address: %d %q, want 403 and the door itself", res.StatusCode, res.Header.Get("Location"))
222 }
223 for _, want := range []string{"estilo-door", `name="address"`, "can’t sign in to Slides"} {
224 if !strings.Contains(string(body), want) {
225 t.Errorf("refusal page lacks %q:\n%s", want, body)
226 }
227 }
228 if strings.Contains(string(body), "not admitted here") {
229 t.Error("the refusal is rastrillo's bare text")
230 }
231 if got := res.Header.Get("Cache-Control"); got != "no-store" {
232 t.Errorf("refusal door Cache-Control = %q, want no-store", got)
233 }
234 if rec := get(t, app, "/", cookies...); rec.Code == http.StatusOK {
235 t.Fatal("a refused Keymail sign-in left a working session")
236 }
237 if spoof := get(t, app, "/signin?err=refused").Body.String(); strings.Contains(spoof, "can’t sign in to Slides") {
238 t.Fatal("anybody can show the refusal by visiting /signin?err=refused")
239 }
240}
241
242// mailWorkers counts goroutines running a sign-in mail queue's worker
243// or log pump (internal/slides/mailqueue.go) — the targeted leak check
244// TestMain runs after the package's tests.
245func mailWorkers() int {
246 buf := make([]byte, 1<<22)
247 stacks := string(buf[:runtime.Stack(buf, true)])
248 return strings.Count(stacks, "(*mailQueue).run(") + strings.Count(stacks, "(*mailQueue).pump(")
249}
250