oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package calls
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "fmt"
8 "log/slog"
9 "net/http"
10 "net/http/httptest"
11 "net/url"
12 "path/filepath"
13 "sort"
14 "strconv"
15 "strings"
16 "sync/atomic"
17 "testing"
18 "time"
19
20 "amadan.net/rastrillo/rastrillo/db"
21 "gorm.io/gorm"
22)
23
24// measuredKey marks the context of a request whose statements count.
25type measuredKey struct{}
26
27// countingHandler counts log records, and nothing else.
28type countingHandler struct{ n *atomic.Int64 }
29
30func (h countingHandler) Enabled(context.Context, slog.Level) bool { return true }
31func (h countingHandler) Handle(context.Context, slog.Record) error { h.n.Add(1); return nil }
32func (h countingHandler) WithAttrs([]slog.Attr) slog.Handler { return h }
33func (h countingHandler) WithGroup(string) slog.Handler { return h }
34
35// TestMemberAndOutsiderDoTheSameWorkOverBudget is Astra's round-3 probe
36// on #25 (P1). With a live invitation in the browser and the outsider
37// budgets spent, membership used to decide what POST /signin did on the
38// response path: a member wrote a pending row, an outsider wrote a log
39// line and returned — 0.38–0.65 ms against 76.9–77.9 ms. Every POST now
40// does the same synchronous work whoever it names: the same database
41// statements, and no log line at all. The budgets still hold: the
42// outsider is mailed nothing.
43func TestMemberAndOutsiderDoTheSameWorkOverBudget(t *testing.T) {
44 var logs atomic.Int64
45 logger := slog.New(countingHandler{&logs})
46 d, err := db.Open(filepath.Join(t.TempDir(), "calls.db"), slog.New(slog.DiscardHandler))
47 if err != nil {
48 t.Fatal(err)
49 }
50 t.Cleanup(func() { d.Close() })
51 mail := &countingMailer{}
52 mux, a, err := buildForTest(t, d, Config{Origin: keymailOrigin, InstanceKey: "test-only-instance",
53 DEK: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{3}, 32)), AllowedAddresses: []string{plainMember}, Mailer: mail, Logger: logger})
54 if err != nil {
55 t.Fatal(err)
56 }
57 _, tok, err := a.store.createMeeting(t.Context(), "x", nil, "person:1")
58 if err != nil {
59 t.Fatal(err)
60 }
61 join := httptest.NewRecorder()
62 mux.ServeHTTP(join, httptest.NewRequest(http.MethodGet, keymailOrigin+"/j/"+tok, nil))
63 cookies := join.Result().Cookies()
64 if len(cookies) == 0 {
65 t.Fatal("following the invitation set no join cookie")
66 }
67
68 var statements atomic.Int64
69 // Only the request's own statements: the mail worker runs beside it
70 // (authorize, after the response) on a context of its own.
71 count := func(tx *gorm.DB) {
72 if tx.Statement.Context != nil && tx.Statement.Context.Value(measuredKey{}) != nil {
73 statements.Add(1)
74 }
75 }
76 cb := a.db.G.Callback()
77 for _, err := range []error{
78 cb.Query().After("gorm:query").Register("equalwork:query", count),
79 cb.Create().After("gorm:create").Register("equalwork:create", count),
80 cb.Update().After("gorm:update").Register("equalwork:update", count),
81 cb.Delete().After("gorm:delete").Register("equalwork:delete", count),
82 cb.Raw().After("gorm:raw").Register("equalwork:raw", count),
83 cb.Row().After("gorm:row").Register("equalwork:row", count),
84 } {
85 if err != nil {
86 t.Fatal(err)
87 }
88 }
89 ip := 0
90 post := func(form url.Values) *httptest.ResponseRecorder {
91 ip++ // a fresh client address each time, so rastrillo's per-IP budget stays out of it
92 r := httptest.NewRequest(http.MethodPost, keymailOrigin+"/signin", strings.NewReader(form.Encode()))
93 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
94 r.Header.Set("Origin", keymailOrigin)
95 r.Header.Set("Sec-Fetch-Site", "same-origin")
96 r.RemoteAddr = "198.51.100." + strconv.Itoa(ip) + ":1234"
97 r = r.WithContext(context.WithValue(r.Context(), measuredKey{}, true))
98 for _, c := range cookies {
99 r.AddCookie(c)
100 }
101 w := httptest.NewRecorder()
102 mux.ServeHTTP(w, r)
103 return w
104 }
105 // Spend the per-link budget (five) with outsiders, each its own
106 // address so rastrillo's per-address budget stays out of it.
107 for i := 0; i < 6; i++ {
108 post(url.Values{"address": {"spender" + string(rune('a'+i)) + "@else.test"}})
109 }
110
111 measure := func(address string) (stmts, lines int64, took time.Duration) {
112 statements.Store(0)
113 logs.Store(0)
114 start := time.Now()
115 rec := post(url.Values{"address": {address}})
116 took = time.Since(start)
117 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
118 t.Fatalf("POST %s: %d %q", address, rec.Code, rec.Header().Get("Location"))
119 }
120 return statements.Load(), logs.Load(), took
121 }
122 ms, ml, _ := measure(plainMember)
123 os, ol, _ := measure("outsider@else.test")
124 if ms != os || ml != ol || ml != 0 {
125 t.Fatalf("member did %d statements and %d log lines, outsider %d and %d: want the same work and no log line on the response path", ms, ml, os, ol)
126 }
127
128 // Astra's measurement, re-run for the record (not asserted: a
129 // stopwatch in a test is a flake, the statement count is the proof).
130 // Four more of each: rastrillo's per-address budget is five.
131 var mt, ot []time.Duration
132 for i := 0; i < 4; i++ {
133 _, _, d1 := measure(plainMember)
134 _, _, d2 := measure(fmt.Sprintf("outsider%d@else.test", i))
135 mt, ot = append(mt, d1), append(ot, d2)
136 }
137 median := func(xs []time.Duration) time.Duration {
138 sort.Slice(xs, func(i, j int) bool { return xs[i] < xs[j] })
139 return xs[len(xs)/2]
140 }
141 t.Logf("median POST /signin over budget: member %v, outsider %v", median(mt), median(ot))
142
143 a.signinMail.wait()
144 for _, to := range mail.sent() {
145 if strings.HasPrefix(to, "outsider") {
146 t.Fatalf("an over-budget outsider was mailed (%s): the budgets must still hold", to)
147 }
148 }
149}
150