oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "net/http"
5 "net/url"
6 "strings"
7 "testing"
8
9 calendar "amadan.net/oficina/calendar/internal/calendar"
10)
11
12// TestEmailedLinkOpensTheConfirmDoor walks the magic-link sign-in end to
13// end: begin, the mail, GET of the emailed link (the confirm door, which
14// spends nothing), the door's POST, and a session that opens a view.
15//
16// GET /auth/verify used to sign in on sight, so a mail-security gateway
17// that fetches every link in an inbound message (Safe Links, Proofpoint)
18// spent it before the person clicked, and they were told it had expired.
19// rastrillo now confirms on GET and redeems on POST; this pins that the
20// confirm step is Calendar's own door with one Sign in button, not
21// rastrillo's plain English fallback, and that the POST is mounted.
22func TestEmailedLinkOpensTheConfirmDoor(t *testing.T) {
23 app := newApp(t)
24 h := app.Handler()
25
26 if w := post(t, h, "/signin", url.Values{"address": {"someone@example.com"}, "force": {"1"}}); w.Code != http.StatusSeeOther || w.Header().Get("Location") != "/signin?sent=1" {
27 t.Fatalf("POST /signin = %d %q, want 303 /signin?sent=1", w.Code, w.Header().Get("Location"))
28 }
29 body, err := app.LastMailForTest()
30 if err != nil {
31 t.Fatal(err)
32 }
33 link := magicLink(t, body)
34 u, err := url.Parse(link)
35 if err != nil {
36 t.Fatal(err)
37 }
38
39 // Opening it, twice, as a scanner and then the person would.
40 for _, who := range []string{"scanner", "person"} {
41 rec := get(t, h, link)
42 if rec.Code != http.StatusOK || len(rec.Result().Cookies()) != 0 {
43 t.Fatalf("%s's GET = %d with %d cookies, want 200 and nothing minted", who, rec.Code, len(rec.Result().Cookies()))
44 }
45 page := rec.Body.String()
46 for _, want := range []string{"estilo-door", "estilo-signin-form", "<span>Calendar</span>", "<title>Sign in · Calendar</title>", `name="token" value="` + u.Query().Get("token") + `"`} {
47 if !strings.Contains(page, want) {
48 t.Errorf("%s: confirm door lacks %q:\n%s", who, want, page)
49 }
50 }
51 if n := strings.Count(page, "<button"); n != 1 || !strings.Contains(page, `type="submit" autofocus>Sign in</button>`) {
52 t.Errorf("%s: confirm door has %d buttons, want exactly one Sign in:\n%s", who, n, page)
53 }
54 if strings.Contains(page, `name="address"`) {
55 t.Errorf("%s: confirm door asks for an address again", who)
56 }
57 if strings.Contains(page, "<title>Sign in to ") {
58 t.Errorf("%s: rastrillo's fallback confirm page, not the door", who)
59 }
60 // auth sets these before RenderConfirm; render must not undo them.
61 for header, want := range map[string]string{
62 "Cache-Control": "no-store",
63 "Referrer-Policy": "strict-origin",
64 "X-Frame-Options": "DENY",
65 } {
66 if got := rec.Header().Get(header); got != want {
67 t.Errorf("%s: %s = %q, want %q", who, header, got, want)
68 }
69 }
70 }
71
72 // A POST with no browser's same-origin evidence is refused and
73 // spends nothing.
74 action, token := openLink(t, h, link)
75 cross := calendar.RequestForTest(nil, http.MethodPost, action, url.Values{"token": {token}})
76 cross.Header.Del("Origin")
77 cross.Header.Set("Sec-Fetch-Site", "cross-site")
78 if rec := serve(h, cross); rec.Code != http.StatusForbidden || len(rec.Result().Cookies()) != 0 {
79 t.Fatalf("cross-site POST = %d with %d cookies, want 403 and nothing minted", rec.Code, len(rec.Result().Cookies()))
80 }
81
82 // Sign in: the POST spends the link and mints the session.
83 signed := postLink(t, h, action, token)
84 if signed.Code != http.StatusSeeOther || signed.Header().Get("Location") != "/" {
85 t.Fatalf("POST %s = %d %q, want 303 /", action, signed.Code, signed.Header().Get("Location"))
86 }
87 var session *http.Cookie
88 for _, c := range signed.Result().Cookies() {
89 if c.Value != "" && c.MaxAge >= 0 {
90 session = c
91 }
92 }
93 if session == nil {
94 t.Fatal("the confirm POST minted no session")
95 }
96 if view := follow(t, h, session, "/"); view.Code != http.StatusOK || !strings.Contains(view.Body.String(), `aria-label="Account: someone@example.com"`) {
97 t.Fatalf("GET / with the new session = %d, or it does not name the person", view.Code)
98 }
99
100 // Spent: the door still opens (it looks nothing up), but its button
101 // now says the link expired.
102 action, token = openLink(t, h, link)
103 again := postLink(t, h, action, token)
104 if len(again.Result().Cookies()) != 0 || again.Header().Get("Location") != "/signin?err=expired" {
105 t.Fatalf("second POST = %d %q, want /signin?err=expired and no session", again.Code, again.Header().Get("Location"))
106 }
107}
108