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 "runtime"
8 "strings"
9 "testing"
10 "time"
11
12 "amadan.net/rastrillo/rastrillo/sessions"
13)
14
15func TestTeamSessionReaderMatchesActualLegacySession(t *testing.T) {
16 f := newBridgeFixture(t)
17 reader, e := newTeamSessionReader(f.app.db.Writer(), f.b.origin, f.b.sessionCookie, f.app.logger)
18 if e != nil {
19 t.Fatal(e)
20 }
21 if _, e = newTeamSessionReader(f.app.db.Writer(), f.b.origin, "wrong-cookie", f.app.logger); e == nil {
22 t.Fatal("different cookie configuration accepted")
23 }
24 for _, method := range []string{"magiclink", "keymail", homeAssertionMethod} {
25 t.Run(method, func(t *testing.T) {
26 r := bridgeRequest("GET", "/", nil)
27 w := httptest.NewRecorder()
28 at := time.Now().Add(-time.Hour).Truncate(time.Second)
29 if e = f.b.sessions.SignIn(w, r, sessions.Session{Subject: "opaque-credential", Method: method, AuthTime: at}); e != nil {
30 t.Fatal(e)
31 }
32 cookie := bridgeCookie(t, w, f.b.sessionCookie)
33 r.AddCookie(cookie)
34 old, ok := f.b.sessions.From(r)
35 if !ok {
36 t.Fatal("actual legacy session missing")
37 }
38 got, ok := reader(t.Context(), r)
39 if !ok || got != old || !got.AuthTime.Equal(at) || got.Subject != "opaque-credential" || got.Method != method {
40 t.Fatal("reader conversion changed session authority")
41 }
42 ctx, cancel := context.WithCancel(t.Context())
43 cancel()
44 if _, ok = reader(ctx, r); ok {
45 t.Fatal("canceled session lookup accepted")
46 }
47 if got, ok = reader(t.Context(), r); !ok || got != old {
48 t.Fatal("cancellation rotated or refreshed actual session")
49 }
50 duplicate := bridgeRequest("GET", "/", []*http.Cookie{cookie, cookie})
51 if _, ok = reader(t.Context(), duplicate); ok {
52 t.Fatal("duplicate actual cookies accepted")
53 }
54 if _, e = f.app.db.Writer().Exec("UPDATE sessions SET expires_at=? WHERE token_hash=?", time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), sessions.HashToken(cookie.Value)); e != nil {
55 t.Fatal(e)
56 }
57 if _, ok = reader(t.Context(), r); ok {
58 t.Fatal("expired session accepted")
59 }
60 if _, ok = f.b.sessions.From(r); ok {
61 t.Fatal("legacy reader differed on expiry")
62 }
63 })
64 }
65 r := bridgeRequest("GET", "/", nil)
66 if _, ok := reader(t.Context(), r); ok {
67 t.Fatal("missing session accepted")
68 }
69 r.AddCookie(&http.Cookie{Name: f.b.sessionCookie, Value: "malformed"})
70 if _, ok := reader(t.Context(), r); ok {
71 t.Fatal("malformed session accepted")
72 }
73}
74
75func TestTeamSessionReaderRevocationAndDatabaseErrorParity(t *testing.T) {
76 f := newBridgeFixture(t)
77 reader, e := newTeamSessionReader(f.app.db.Writer(), f.b.origin, f.b.sessionCookie, f.app.logger)
78 if e != nil {
79 t.Fatal(e)
80 }
81 r := bridgeRequest("GET", "/", nil)
82 w := httptest.NewRecorder()
83 if e := f.b.sessions.SignIn(w, r, sessions.Session{Subject: "alice@example.com", Method: "magiclink", AuthTime: time.Now()}); e != nil {
84 t.Fatal(e)
85 }
86 r.AddCookie(bridgeCookie(t, w, f.b.sessionCookie))
87 if _, ok := reader(t.Context(), r); !ok {
88 t.Fatal("live session refused")
89 }
90 f.b.sessions.SignOut(httptest.NewRecorder(), r)
91 if _, ok := reader(t.Context(), r); ok {
92 t.Fatal("revoked session accepted")
93 }
94 if _, ok := f.b.sessions.From(r); ok {
95 t.Fatal("legacy revocation differs")
96 }
97 if e := f.app.db.Writer().Close(); e != nil {
98 t.Fatal(e)
99 }
100 if _, ok := reader(t.Context(), r); ok {
101 t.Fatal("database failure accepted")
102 }
103 if _, ok := f.b.sessions.From(r); ok {
104 t.Fatal("legacy database failure differs")
105 }
106}
107
108// A lookup cancelled while it waits for the writer must end its database
109// work, not merely stop waiting for it. A reader that only abandons the
110// answer (a871701 ran From in a goroutine and returned on ctx.Done) leaves
111// one goroutine per cancelled lookup queued for the connection, and they
112// all run when the writer comes back. Review #77, Astra: 64 left behind.
113func TestTeamSessionReaderCancelledLookupsLeaveNoWorkQueued(t *testing.T) {
114 f := newBridgeFixture(t)
115 reader, e := newTeamSessionReader(f.app.db.Writer(), f.b.origin, f.b.sessionCookie, f.app.logger)
116 if e != nil {
117 t.Fatal(e)
118 }
119 r := bridgeRequest("GET", "/", nil)
120 w := httptest.NewRecorder()
121 if e = f.b.sessions.SignIn(w, r, sessions.Session{Subject: "alice@example.com", Method: "magiclink", AuthTime: time.Now()}); e != nil {
122 t.Fatal(e)
123 }
124 r.AddCookie(bridgeCookie(t, w, f.b.sessionCookie))
125
126 db := f.app.db.Writer()
127 tx, e := db.BeginTx(t.Context(), nil)
128 if e != nil {
129 t.Fatal(e)
130 }
131 defer tx.Rollback()
132
133 const lookups = 64
134 for i := 0; i < lookups; i++ {
135 ctx, cancel := context.WithCancel(t.Context())
136 before := db.Stats().WaitCount
137 done := make(chan bool, 1)
138 go func() { _, ok := reader(ctx, r); done <- ok }()
139 for deadline := time.Now().Add(2 * time.Second); db.Stats().WaitCount == before; {
140 if time.Now().After(deadline) {
141 cancel()
142 t.Fatal("the lookup never queued for the held writer")
143 }
144 time.Sleep(time.Millisecond)
145 }
146 cancel()
147 select {
148 case ok := <-done:
149 if ok {
150 t.Fatal("a cancelled lookup admitted the session")
151 }
152 case <-time.After(2 * time.Second):
153 t.Fatal("cancelling a queued lookup did not return")
154 }
155 }
156
157 // Every caller has returned and the writer is still held: nothing may
158 // still be waiting inside rastrillo's session lookup.
159 var queued int
160 for deadline := time.Now().Add(time.Second); ; {
161 queued = goroutinesIn("rastrillo/sessions.")
162 if queued == 0 || time.Now().After(deadline) {
163 break
164 }
165 time.Sleep(10 * time.Millisecond)
166 }
167 if queued != 0 {
168 t.Fatalf("%d of %d cancelled session lookups are still queued for the writer", queued, lookups)
169 }
170 if e = tx.Rollback(); e != nil {
171 t.Fatal(e)
172 }
173 if _, ok := reader(t.Context(), r); !ok {
174 t.Fatal("cancelled lookups disturbed the live session")
175 }
176}
177
178// goroutinesIn counts goroutines with a frame in a function whose name
179// contains marker.
180func goroutinesIn(marker string) int {
181 buf := make([]byte, 1<<20)
182 for {
183 n := runtime.Stack(buf, true)
184 if n < len(buf) {
185 buf = buf[:n]
186 break
187 }
188 buf = make([]byte, 2*len(buf))
189 }
190 count := 0
191 for _, g := range strings.Split(string(buf), "\n\n") {
192 if strings.Contains(g, marker) {
193 count++
194 }
195 }
196 return count
197}
198