oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12)
13
14// blockingMailer holds every Send until release is closed — an SMTP
15// relay that takes as long as the test likes.
16type blockingMailer struct {
17 release chan struct{}
18 mu sync.Mutex
19 to []string
20}
21
22func (m *blockingMailer) Send(_ context.Context, to, _, _ string) error {
23 <-m.release
24 m.mu.Lock()
25 m.to = append(m.to, to)
26 m.mu.Unlock()
27 return nil
28}
29
30func (m *blockingMailer) sent() []string {
31 m.mu.Lock()
32 defer m.mu.Unlock()
33 return append([]string(nil), m.to...)
34}
35
36// TestSigninNeverWaitsForTheMailer: when the send ran inside the
37// request (admissionMailer), a permitted address waited for SMTP before
38// its ?sent=1 and a refused one did not — Astra measured 252 ms against
39// 0.35 ms on Slides, which had the same shape — so the allowlist was
40// readable off a stopwatch. With the mailer blocked, every POST —
41// permitted, refused, and both with force=1 — must answer, and the
42// permitted links must then go out.
43func TestSigninNeverWaitsForTheMailer(t *testing.T) {
44 bm := &blockingMailer{release: make(chan struct{})}
45 app, err := NewForTestWith(t, Config{AllowedAddresses: "member@example.test", Mailer: bm})
46 if err != nil {
47 t.Fatal(err)
48 }
49 var once sync.Once
50 free := func() { once.Do(func() { close(bm.release) }) }
51 t.Cleanup(free) // before the app's Close, which drains the queue
52
53 for _, form := range []url.Values{
54 {"address": {"member@example.test"}},
55 {"address": {"outsider@example.test"}},
56 {"address": {"member@example.test"}, "force": {"1"}},
57 {"address": {"outsider@example.test"}, "force": {"1"}},
58 } {
59 done := make(chan *httptest.ResponseRecorder, 1)
60 go func() {
61 req := httptest.NewRequest(http.MethodPost, "/signin", strings.NewReader(form.Encode()))
62 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
63 req.Header.Set("Sec-Fetch-Site", "same-origin")
64 rec := httptest.NewRecorder()
65 app.Handler().ServeHTTP(rec, req)
66 done <- rec
67 }()
68 select {
69 case rec := <-done:
70 if rec.Code != http.StatusSeeOther || !strings.Contains(rec.Header().Get("Location"), "sent=1") {
71 t.Errorf("POST %v: %d %q, want 303 ?sent=1", form, rec.Code, rec.Header().Get("Location"))
72 }
73 case <-time.After(2 * time.Second):
74 t.Errorf("POST %v waited on the mailer: the response time says the address is permitted", form)
75 free()
76 <-done
77 }
78 }
79
80 free()
81 deadline := time.Now().Add(5 * time.Second)
82 for len(bm.sent()) < 2 && time.Now().Before(deadline) {
83 time.Sleep(10 * time.Millisecond)
84 }
85 time.Sleep(50 * time.Millisecond)
86 if got := bm.sent(); len(got) != 2 || got[0] != "member@example.test" || got[1] != "member@example.test" {
87 t.Fatalf("mail sent = %v, want two links, both to member@example.test", got)
88 }
89}
90
91// TestAppCloseStopsTheMailWorker: the app owns its queue, and Close
92// stops its worker.
93func TestAppCloseStopsTheMailWorker(t *testing.T) {
94 app, err := NewForTest(t)
95 if err != nil {
96 t.Fatal(err)
97 }
98 q := app.signinMail
99 _ = app.Close()
100 select {
101 case <-q.done:
102 case <-time.After(2 * time.Second):
103 t.Fatal("the sign-in mail worker was still running after App.Close")
104 }
105}
106