oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "gorm.io/gorm"
13)
14
15// TestRememberedReturnRejectsTamperAndExpiry is the boundary before a return
16// path can become a redirect. A request cookie is attacker input: neither a
17// replacement path nor an expired sealed intent is allowed to select where a
18// completed sign-in goes.
19func TestRememberedReturnRejectsTamperAndExpiry(t *testing.T) {
20 a, _, err := newApp(testDB(t), Config{Env: EnvDevelopment, Origin: "https://slides.example.test", Logger: quietLogger()})
21 if err != nil {
22 t.Fatal(err)
23 }
24 t.Cleanup(func() { a.Close() })
25 now := time.Unix(1900000000, 0)
26 a.returnNow = func() time.Time { return now }
27 w := httptest.NewRecorder()
28 a.rememberReturn(w, "/d/private?view=present")
29 cookies := w.Result().Cookies()
30 if len(cookies) != 1 {
31 t.Fatalf("remembered %d cookies, want one", len(cookies))
32 }
33 cookie := cookies[0]
34 sealed, err := base64.RawURLEncoding.DecodeString(cookie.Value)
35 if err != nil || bytes.Contains(sealed, []byte("private")) {
36 t.Fatal("return path was not sealed")
37 }
38 valid := httptest.NewRequest(http.MethodGet, "https://slides.example.test/auth/landed", nil)
39 valid.AddCookie(cookie)
40 if got, ok := a.rememberedReturn(valid); !ok || got != "/d/private?view=present" {
41 t.Fatalf("sealed return = %q, %v", got, ok)
42 }
43
44 sealed[len(sealed)-1] ^= 1
45 tampered := *cookie
46 tampered.Value = base64.RawURLEncoding.EncodeToString(sealed)
47 r := httptest.NewRequest(http.MethodGet, "https://slides.example.test/auth/landed", nil)
48 r.AddCookie(&tampered)
49 if got, ok := a.rememberedReturn(r); ok || got != "" {
50 t.Fatalf("tampered return cookie was accepted as %q", got)
51 }
52
53 now = now.Add(time.Duration(returnCookieAge) * time.Second)
54 if got, ok := a.rememberedReturn(valid); ok || got != "" {
55 t.Fatalf("expired return cookie was accepted as %q", got)
56 }
57}
58
59// countCreates registers a GORM callback that counts INSERT statements
60// on the handle, returning a function that reads the count. It is the
61// only honest way to answer "did that request write?": a row's id and
62// CreatedAt look identical whether the second call inserted nothing or
63// ran an upsert that conflicted, and SQLite's AUTOINCREMENT sequence
64// does not move for a DO NOTHING either. Counting the statement itself
65// is the observation that distinguishes them.
66func countCreates(t *testing.T, g *gorm.DB) func() int {
67 t.Helper()
68 var n int
69 if err := g.Callback().Create().Before("gorm:create").Register("slides_test:count_creates", func(*gorm.DB) {
70 n++
71 }); err != nil {
72 t.Fatalf("register create callback: %v", err)
73 }
74 return func() int { return n }
75}
76
77// TestEnsureUserWritesOnceOnly is the regression test for a real
78// performance bug: ensureUser used to run INSERT ... ON CONFLICT DO
79// NOTHING followed by a SELECT on EVERY authenticated request, because
80// currentUser calls it before any handler does its own work. SQLite has
81// one writer, so that was a write transaction per page view,
82// serialising every reader in the app behind a statement whose answer
83// was almost always "this row already exists".
84//
85// It now reads first and writes only on a miss. Three calls, one
86// insert.
87func TestEnsureUserWritesOnceOnly(t *testing.T) {
88 d := testDB(t)
89 if a, _, err := newApp(d, Config{Env: EnvDevelopment, Logger: quietLogger()}); err != nil {
90 t.Fatalf("newApp: %v", err)
91 } else {
92 t.Cleanup(func() { a.Close() })
93 }
94 creates := countCreates(t, d.G)
95
96 ctx := context.Background()
97 first, created, err := ensureUser(ctx, d.G, "paul@example.test")
98 if err != nil {
99 t.Fatalf("ensureUser (first): %v", err)
100 }
101 if first.ID == 0 {
102 t.Fatal("ensureUser (first) returned a row with no id")
103 }
104 // The created flag is what tells a first sign-in from every later
105 // request, and so what decides whether the sample deck is seeded
106 // (sample.go). It has to be true exactly once.
107 if !created {
108 t.Error("ensureUser (first) reported the row as already existing")
109 }
110 if got := creates(); got != 1 {
111 t.Fatalf("first sign-in ran %d inserts, want 1", got)
112 }
113
114 for i := 2; i <= 3; i++ {
115 again, createdAgain, err := ensureUser(ctx, d.G, "paul@example.test")
116 if err != nil {
117 t.Fatalf("ensureUser (call %d): %v", i, err)
118 }
119 if again.ID != first.ID {
120 t.Errorf("call %d returned id %d, want the same row %d", i, again.ID, first.ID)
121 }
122 if createdAgain {
123 t.Errorf("call %d reported the row as newly created", i)
124 }
125 if !again.CreatedAt.Equal(first.CreatedAt) {
126 t.Errorf("call %d rewrote CreatedAt (%v, was %v)", i, again.CreatedAt, first.CreatedAt)
127 }
128 if got := creates(); got != 1 {
129 t.Fatalf("after call %d the app had run %d inserts, want the 1 from first sign-in", i, got)
130 }
131 }
132}
133
134// TestEnsureUserNormalizesTheAddress: the address is the identity, so
135// the same person typing capitals or a stray space is the same row —
136// and only one row, since a second would mean one person owning two
137// disjoint sets of decks depending on which one a request found.
138func TestEnsureUserNormalizesTheAddress(t *testing.T) {
139 d := testDB(t)
140 if a, _, err := newApp(d, Config{Env: EnvDevelopment, Logger: quietLogger()}); err != nil {
141 t.Fatalf("newApp: %v", err)
142 } else {
143 t.Cleanup(func() { a.Close() })
144 }
145 ctx := context.Background()
146
147 first, _, err := ensureUser(ctx, d.G, "Paul@Example.Test")
148 if err != nil {
149 t.Fatalf("ensureUser: %v", err)
150 }
151 if first.Email != "paul@example.test" {
152 t.Errorf("stored Email = %q, want it normalized", first.Email)
153 }
154 again, _, err := ensureUser(ctx, d.G, " paul@example.test ")
155 if err != nil {
156 t.Fatalf("ensureUser (again): %v", err)
157 }
158 if again.ID != first.ID {
159 t.Errorf("a differently-spelled address got id %d, want the same row %d", again.ID, first.ID)
160 }
161
162 var n int64
163 if err := d.G.Model(&User{}).Count(&n).Error; err != nil {
164 t.Fatalf("count users: %v", err)
165 }
166 if n != 1 {
167 t.Errorf("users table has %d rows, want 1", n)
168 }
169}
170
171// TestEnsureUserRefusesAnEmptyAddress: an empty Subject would otherwise
172// create a row owning decks that nobody could ever sign in as.
173func TestEnsureUserRefusesAnEmptyAddress(t *testing.T) {
174 d := testDB(t)
175 if a, _, err := newApp(d, Config{Env: EnvDevelopment, Logger: quietLogger()}); err != nil {
176 t.Fatalf("newApp: %v", err)
177 } else {
178 t.Cleanup(func() { a.Close() })
179 }
180 if _, _, err := ensureUser(context.Background(), d.G, " "); err == nil {
181 t.Fatal("ensureUser accepted a blank address; want an error")
182 }
183}
184
185// TestColourForIsStable: a presence colour is only worth recognising if
186// it is the same colour tomorrow, and on every instance.
187func TestColourForIsStable(t *testing.T) {
188 first := colourFor("paul@example.test")
189 if first != colourFor("paul@example.test") {
190 t.Error("colourFor is not deterministic")
191 }
192 var found bool
193 for _, c := range presenceColours {
194 if c == first {
195 found = true
196 }
197 }
198 if !found {
199 t.Errorf("colourFor returned %q, which is not in the palette", first)
200 }
201}
202