oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

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