oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "fmt"
5 "html"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "os"
10 "regexp"
11 "strings"
12 "testing"
13 "time"
14
15 "amadan.net/oficina/sheets/internal/sheets/keymailtest"
16)
17
18// TestMain keeps Keymail classification inside the process: every DNS
19// query is answered NXDOMAIN and only the Keymail fixtures a test
20// registers answer HTTPS (keymailtest). Before it, every POST /signin
21// in this suite classified its address against the real network.
22func TestMain(m *testing.M) {
23 restore, err := keymailtest.Install()
24 if err != nil {
25 fmt.Fprintln(os.Stderr, "keymailtest.Install:", err)
26 os.Exit(1)
27 }
28 code := m.Run()
29 // No test may leave a sign-in mail worker running: every plugin a
30 // test builds is closed in its cleanup (CloseSigninMail).
31 if code == 0 {
32 deadline := time.Now().Add(5 * time.Second)
33 for mailWorkers() > 0 && time.Now().Before(deadline) {
34 time.Sleep(10 * time.Millisecond)
35 }
36 if n := mailWorkers(); n > 0 {
37 fmt.Fprintf(os.Stderr, "FAIL: %d sign-in mail queue goroutines still running after the tests\n", n)
38 code = 1
39 }
40 }
41 restore()
42 os.Exit(code)
43}
44
45const (
46 keymailHost = "keymail.sheets.test"
47 keymailMember = "paul@" + keymailHost
48 keymailStranger = "stranger@" + keymailHost
49)
50
51// keymailApp is a standalone app whose owners include keymailMember,
52// beside a Keymail deployment holding keymailMember and keymailStranger.
53func keymailApp(t *testing.T) (*App, *recorder, *keymailtest.Server) {
54 t.Helper()
55 km := keymailtest.Serve(t, keymailHost, keymailMember, keymailStranger)
56 app, rec := appWithOwners(t, testOrigin, signedInAddress, keymailMember)
57 return app, rec, km
58}
59
60func keymailPost(t *testing.T, h http.Handler, form url.Values, cookies ...*http.Cookie) *httptest.ResponseRecorder {
61 t.Helper()
62 r := httptest.NewRequest(http.MethodPost, testOrigin+"/signin", strings.NewReader(form.Encode()))
63 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
64 r.Header.Set("Origin", testOrigin)
65 r.Header.Set("Sec-Fetch-Site", "same-origin")
66 for _, c := range cookies {
67 r.AddCookie(c)
68 }
69 w := httptest.NewRecorder()
70 h.ServeHTTP(w, r)
71 return w
72}
73
74func keymailGet(t *testing.T, h http.Handler, path string, cookies ...*http.Cookie) *httptest.ResponseRecorder {
75 t.Helper()
76 r := httptest.NewRequest(http.MethodGet, testOrigin+path, nil)
77 for _, c := range cookies {
78 r.AddCookie(c)
79 }
80 w := httptest.NewRecorder()
81 h.ServeHTTP(w, r)
82 return w
83}
84
85var authorizeHref = regexp.MustCompile(`href="(https://` + regexp.QuoteMeta(keymailHost) + `/oauth/authorize\?[^"]+)"`)
86
87// continuation asserts rec is the page between the form and Keymail —
88// a 200 document with a meta refresh, never a redirect from the form
89// POST (Safari applies form-action to that redirect) — and returns the
90// authorize URL it carries.
91func continuation(t *testing.T, label string, rec *httptest.ResponseRecorder) *url.URL {
92 t.Helper()
93 if rec.Code != http.StatusOK || rec.Header().Get("Location") != "" {
94 t.Fatalf("%s: %d %q, want 200 and the continuation page — the hop to Keymail must not be a redirect from the form POST", label, rec.Code, rec.Header().Get("Location"))
95 }
96 body := rec.Body.String()
97 m := authorizeHref.FindStringSubmatch(body)
98 if m == nil || !strings.Contains(body, `http-equiv="refresh"`) {
99 t.Fatalf("%s: no Keymail continuation on the page:\n%s", label, body)
100 }
101 u, err := url.Parse(html.UnescapeString(m[1]))
102 if err != nil {
103 t.Fatal(err)
104 }
105 return u
106}
107
108// TestKeymailAddressGoesToKeymailWhetherAdmittedOrNot: whether an
109// address goes to Keymail depends on its domain alone. An admitted and
110// a non-admitted Keymail address get the same first answer — the
111// continuation page — so the answer says nothing about who is on the
112// team. Before, the admission gate answered the stranger ?sent=1 and
113// sent the member to Keymail with a 303 out of the form POST, which
114// Safari refused under form-action 'self'.
115func TestKeymailAddressGoesToKeymailWhetherAdmittedOrNot(t *testing.T) {
116 app, mail, km := keymailApp(t)
117 h := app.Router()
118 member := keymailPost(t, h, url.Values{"address": {keymailMember}})
119 stranger := keymailPost(t, h, url.Values{"address": {keymailStranger}})
120 mu := continuation(t, "admitted Keymail address", member)
121 su := continuation(t, "non-admitted Keymail address", stranger)
122 mask := func(rec *httptest.ResponseRecorder, u *url.URL) string {
123 return strings.ReplaceAll(rec.Body.String(), html.EscapeString(u.String()), "AUTHORIZE")
124 }
125 if a, b := mask(member, mu), mask(stranger, su); a != b {
126 t.Errorf("the two Keymail answers differ beyond the authorize URL:\nadmitted:\n%s\nnot admitted:\n%s", a, b)
127 }
128 if got := mail.addresses(); len(got) != 0 {
129 t.Errorf("a Keymail address was mailed: %v", got)
130 }
131 if n := km.Lookups(); n != 2 {
132 t.Errorf("Keymail saw %d lookups, want 2: both addresses classified", n)
133 }
134}
135
136// TestHomeLocalKeymailIsTheSameAnswer is the same in Home mode, where the
137// local form posts to /signin under LocalGuard.
138func TestHomeLocalKeymailIsTheSameAnswer(t *testing.T) {
139 km := keymailtest.Serve(t, keymailHost, keymailMember, keymailStranger)
140 f := newHomeFixture(t)
141 member := f.request(t, http.MethodPost, "/signin", url.Values{"address": {signedInAddress}}, false)
142 if member.Code != http.StatusSeeOther || member.Header().Get("Location") != "/signin?sent=1" {
143 t.Fatalf("Home-local plain member: %d %q", member.Code, member.Header().Get("Location"))
144 }
145 stranger := f.request(t, http.MethodPost, "/signin", url.Values{"address": {keymailStranger}}, false)
146 continuation(t, "Home-local non-admitted Keymail address", stranger)
147 if n := km.Lookups(); n != 1 {
148 t.Errorf("Keymail saw %d lookups, want 1", n)
149 }
150}
151
152// TestNonAdmittedPlainAddressIsAnsweredSentAndNotMailed: an address
153// with no Keymail still gets ?sent=1, admitted or not, and only the
154// admitted one is mailed.
155func TestNonAdmittedPlainAddressIsAnsweredSentAndNotMailed(t *testing.T) {
156 app, mail, _ := keymailApp(t)
157 h := app.Router()
158 for _, address := range []string{"stranger@example.com", signedInAddress} {
159 rec := keymailPost(t, h, url.Values{"address": {address}})
160 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
161 t.Fatalf("POST /signin %q: %d %q, want 303 /signin?sent=1", address, rec.Code, rec.Header().Get("Location"))
162 }
163 }
164 if got := mail.addresses(); len(got) != 1 || got[0] != signedInAddress {
165 t.Fatalf("mail = %v, want one, to %s", got, signedInAddress)
166 }
167}
168
169// TestKeymailFailureFallsBackToAnEmailLink: the form rastrillo lands a
170// failed Keymail exchange on carries force, and a forced POST mails a
171// link without classifying.
172func TestKeymailFailureFallsBackToAnEmailLink(t *testing.T) {
173 app, mail, km := keymailApp(t)
174 h := app.Router()
175 if body := keymailGet(t, h, "/signin?force=1&err=keymail").Body.String(); !strings.Contains(body, `name="force"`) {
176 t.Fatal("the err=keymail page does not carry force")
177 }
178 rec := keymailPost(t, h, url.Values{"address": {keymailMember}, "force": {"1"}})
179 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
180 t.Fatalf("forced POST: %d %q", rec.Code, rec.Header().Get("Location"))
181 }
182 if got := mail.addresses(); len(got) != 1 || got[0] != keymailMember {
183 t.Fatalf("mail = %v, want one link to %s", got, keymailMember)
184 }
185 if km.Lookups() != 0 {
186 t.Error("a forced sign-in classified the address")
187 }
188}
189
190// TestKeymailRefusalIsTheDoor: a non-admitted address Keymail verified
191// is refused after verification with the sign-in door, rendered in the
192// refusal's own 403 by auth's Refused hook (App.refused) — not
193// rastrillo's bare text, and not a redirect to a query anybody could
194// type. No session is minted. An admitted one is signed in, through the
195// continuation page, and its session opens the list.
196func TestKeymailRefusalIsTheDoor(t *testing.T) {
197 app, _, km := keymailApp(t)
198 h := app.Router()
199 roundTrip := func(address string) *httptest.ResponseRecorder {
200 begin := keymailPost(t, h, url.Values{"address": {address}})
201 u := continuation(t, address, begin)
202 return keymailGet(t, h, "/auth/callback?"+url.Values{"state": {u.Query().Get("state")}, "code": {km.Approve(address)}}.Encode(), begin.Result().Cookies()...)
203 }
204 cb := roundTrip(keymailMember)
205 if cb.Code != http.StatusSeeOther || cb.Header().Get("Location") != "/sheets" {
206 t.Fatalf("admitted callback: %d %q, want 303 /sheets", cb.Code, cb.Header().Get("Location"))
207 }
208 if list := page(t, app, cb.Result().Cookies(), "/sheets"); list.Code != http.StatusOK || !strings.Contains(list.Body.String(), keymailMember) {
209 t.Fatalf("GET /sheets with the Keymail session = %d, or it does not name %s", list.Code, keymailMember)
210 }
211 cb = roundTrip(keymailStranger)
212 if cb.Code != http.StatusForbidden || cb.Header().Get("Location") != "" {
213 t.Fatalf("non-admitted callback: %d %q, want 403 and the door itself (bare text is the bug)\n%s", cb.Code, cb.Header().Get("Location"), cb.Body.String())
214 }
215 for _, c := range cb.Result().Cookies() {
216 if strings.Contains(c.Name, "session") && c.MaxAge >= 0 && c.Value != "" {
217 t.Fatalf("a refused sign-in set a session cookie %q", c.Name)
218 }
219 }
220 door := cb.Body.String()
221 for _, want := range []string{"estilo-door", `name="address"`, "can’t sign in to Sheets"} {
222 if !strings.Contains(door, want) {
223 t.Errorf("refusal door lacks %q:\n%s", want, door)
224 }
225 }
226 // The sentence means "you proved this address and it is not
227 // admitted"; nobody may summon it by typing a URL (Astra, round 3).
228 if spoof := keymailGet(t, h, "/signin?err=refused").Body.String(); strings.Contains(spoof, "can’t sign in to Sheets") {
229 t.Fatal("anybody can show the refusal by visiting /signin?err=refused")
230 }
231}
232