oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package callstest
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "io"
9 "net"
10 "net/http"
11 "strings"
12 "sync"
13 "syscall"
14 "testing"
15 "time"
16)
17
18// sseReader is a browser's EventSource, near enough: one open GET
19// against the harness's real server, its lines parsed into events on a
20// channel while the test goroutine reads them one at a time.
21type sseReader struct {
22 t *testing.T
23 cancel context.CancelFunc
24 body io.ReadCloser
25 events chan sseEvent
26 hb chan struct{}
27 done chan struct{}
28 closing chan struct{}
29 once sync.Once
30}
31
32type sseEvent struct {
33 name string
34 data []byte
35}
36
37// stream opens an SSE stream and fails the test unless the server
38// accepted it. A refusal is read with c.get instead, which needs no
39// socket: the status and the JSON body are the whole answer (§4.5).
40func (c *client) stream(target string) *sseReader {
41 c.t.Helper()
42 srv := c.x.server()
43 ctx, cancel := context.WithCancel(context.Background())
44 req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+target, nil)
45 if err != nil {
46 cancel()
47 c.t.Fatal(err)
48 }
49 for _, ck := range c.cookies {
50 req.AddCookie(ck)
51 }
52 req.Header.Set("Accept", "text/event-stream")
53 resp, err := srv.Client().Do(req)
54 if err != nil {
55 cancel()
56 c.t.Fatalf("GET %s: %v", target, err)
57 }
58 if resp.StatusCode != http.StatusOK {
59 b, _ := io.ReadAll(resp.Body)
60 resp.Body.Close()
61 cancel()
62 c.t.Fatalf("GET %s: %d\n%s", target, resp.StatusCode, b)
63 }
64 if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") {
65 resp.Body.Close()
66 cancel()
67 c.t.Fatalf("GET %s: Content-Type %q", target, ct)
68 }
69 s := &sseReader{
70 t: c.t, cancel: cancel, body: resp.Body,
71 events: make(chan sseEvent, 64), hb: make(chan struct{}, 64),
72 done: make(chan struct{}), closing: make(chan struct{}),
73 }
74 c.t.Cleanup(s.close)
75 go s.read()
76 return s
77}
78
79// read parses the wire format the way an EventSource does: a field is
80// "name:" with one optional space after the colon, several data lines
81// join with a newline, a line starting with a colon is a comment, and a
82// blank line dispatches what has been collected.
83func (s *sseReader) read() {
84 defer close(s.done)
85 sc := bufio.NewScanner(s.body)
86 sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
87 var ev sseEvent
88 var hasData bool
89 for sc.Scan() {
90 line := sc.Text()
91 switch {
92 case line == "":
93 if ev.name != "" || hasData {
94 s.dispatch(ev)
95 }
96 ev, hasData = sseEvent{}, false
97 case strings.HasPrefix(line, ":"):
98 select {
99 case s.hb <- struct{}{}:
100 default:
101 }
102 case strings.HasPrefix(line, "event:"):
103 ev.name = field(line, "event:")
104 case strings.HasPrefix(line, "data:"):
105 if hasData {
106 ev.data = append(ev.data, '\n')
107 }
108 ev.data = append(ev.data, field(line, "data:")...)
109 hasData = true
110 }
111 }
112}
113
114func field(line, name string) string {
115 return strings.TrimPrefix(strings.TrimPrefix(line, name), " ")
116}
117
118// dispatch hands one event to the test goroutine. It blocks rather than
119// dropping: an overflowing buffer means a test is not reading what it
120// asked for, and that should fail out loud rather than quietly lose the
121// event that would have failed the assertion.
122func (s *sseReader) dispatch(ev sseEvent) {
123 select {
124 case s.events <- ev:
125 case <-s.closing:
126 case <-time.After(5 * time.Second):
127 s.t.Errorf("sse: nothing read %q for five seconds; the event buffer overflowed", ev.name)
128 }
129}
130
131// next waits for one event.
132func (s *sseReader) next(timeout time.Duration) (string, []byte, bool) {
133 select {
134 case ev := <-s.events:
135 return ev.name, ev.data, true
136 case <-time.After(timeout):
137 return "", nil, false
138 }
139}
140
141// hello reads the first event, which is always hello (spec §4.1), and
142// returns the peer id and the boot id it carries.
143func (s *sseReader) hello() (peer, boot string) {
144 s.t.Helper()
145 name, data, ok := s.next(2 * time.Second)
146 if !ok || name != "hello" {
147 s.t.Fatalf("first event is %q (ok=%v), want hello", name, ok)
148 }
149 var body struct{ Peer, Boot string }
150 if err := json.Unmarshal(data, &body); err != nil {
151 s.t.Fatalf("hello %s: %v", data, err)
152 }
153 return body.Peer, body.Boot
154}
155
156// nextOf skips events until the named one arrives, or the timeout
157// passes — for a test that expects some event (a roster, a peer) ahead
158// of the one it actually wants to assert on.
159func (s *sseReader) nextOf(event string, timeout time.Duration) (string, []byte, bool) {
160 s.t.Helper()
161 deadline := time.After(timeout)
162 for {
163 select {
164 case ev := <-s.events:
165 if ev.name == event {
166 return ev.name, ev.data, true
167 }
168 case <-deadline:
169 return "", nil, false
170 }
171 }
172}
173
174// waitHeartbeat waits for one SSE comment line, which is the only thing
175// the server sends on an idle stream.
176func (s *sseReader) waitHeartbeat(timeout time.Duration) bool {
177 select {
178 case <-s.hb:
179 return true
180 case <-time.After(timeout):
181 return false
182 }
183}
184
185// waitClosed waits for the server to end the stream.
186func (s *sseReader) waitClosed(timeout time.Duration) bool {
187 select {
188 case <-s.done:
189 return true
190 case <-time.After(timeout):
191 return false
192 }
193}
194
195// close is what a browser navigating away does. It waits for the read
196// goroutine to finish so the test never races the server's own exit.
197func (s *sseReader) close() {
198 s.once.Do(func() {
199 close(s.closing)
200 s.cancel()
201 s.body.Close()
202 select {
203 case <-s.done:
204 case <-time.After(2 * time.Second):
205 s.t.Error("the stream reader did not stop")
206 }
207 })
208}
209
210// rawStream is the stream with nobody draining it in the background —
211// the one thing sseReader cannot be, because its read goroutine is
212// always reading. A test that wants the server's own 64-event
213// subscriber buffer to overflow (spec §4.4) has to stop reading the
214// socket: that is what backs the handler's write up, and only a handler
215// that is not draining lets the buffer behind it fill.
216//
217// Nothing here parses more of the wire format than the tests need: an
218// event name, its data, and heartbeat comments skipped.
219type rawStream struct {
220 t *testing.T
221 cancel context.CancelFunc
222 body io.ReadCloser
223 r *bufio.Reader
224 once sync.Once
225}
226
227// tinyWindowClient dials with a deliberately small receive buffer. It is
228// what makes "the client stopped reading" mean something: with the
229// kernel's own defaults, loopback will happily absorb megabytes on the
230// reader's behalf, and the server's write never blocks, so the hub's
231// buffer never fills and the test proves nothing. SO_RCVBUF also turns
232// receive-window autotuning off, which is the point — the window must
233// stay small rather than growing to meet the flood.
234func tinyWindowClient() *http.Client {
235 d := &net.Dialer{
236 Control: func(network, address string, c syscall.RawConn) error {
237 var setErr error
238 if err := c.Control(func(fd uintptr) {
239 setErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, 4096)
240 }); err != nil {
241 return err
242 }
243 return setErr
244 },
245 }
246 return &http.Client{Transport: &http.Transport{DialContext: d.DialContext}}
247}
248
249func (c *client) rawStream(target string) *rawStream {
250 c.t.Helper()
251 srv := c.x.server()
252 ctx, cancel := context.WithCancel(context.Background())
253 req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+target, nil)
254 if err != nil {
255 cancel()
256 c.t.Fatal(err)
257 }
258 for _, ck := range c.cookies {
259 req.AddCookie(ck)
260 }
261 req.Header.Set("Accept", "text/event-stream")
262 resp, err := tinyWindowClient().Do(req)
263 if err != nil {
264 cancel()
265 c.t.Fatalf("GET %s: %v", target, err)
266 }
267 if resp.StatusCode != http.StatusOK {
268 b, _ := io.ReadAll(resp.Body)
269 resp.Body.Close()
270 cancel()
271 c.t.Fatalf("GET %s: %d\n%s", target, resp.StatusCode, b)
272 }
273 // A small reader on purpose: the socket must stop being drained the
274 // moment the test stops calling next, not a buffer's worth later.
275 s := &rawStream{t: c.t, cancel: cancel, body: resp.Body, r: bufio.NewReaderSize(resp.Body, 512)}
276 c.t.Cleanup(s.close)
277 return s
278}
279
280// next reads one event, blocking until the server sends one or the
281// timeout cancels the request out from under the read.
282func (s *rawStream) next(timeout time.Duration) (string, []byte, bool) {
283 s.t.Helper()
284 stop := time.AfterFunc(timeout, s.cancel)
285 defer stop.Stop()
286 var name string
287 var data []byte
288 for {
289 raw, err := s.r.ReadString('\n')
290 if err != nil {
291 return "", nil, false
292 }
293 line := strings.TrimRight(raw, "\r\n")
294 switch {
295 case line == "":
296 if name != "" || data != nil {
297 return name, data, true
298 }
299 case strings.HasPrefix(line, ":"): // heartbeat
300 case strings.HasPrefix(line, "event:"):
301 name = field(line, "event:")
302 case strings.HasPrefix(line, "data:"):
303 data = append(data, field(line, "data:")...)
304 }
305 }
306}
307
308// hello is next's first event, which is always hello (§4.1).
309func (s *rawStream) hello() string {
310 s.t.Helper()
311 name, data, ok := s.next(2 * time.Second)
312 if !ok || name != "hello" {
313 s.t.Fatalf("first event is %q (ok=%v), want hello", name, ok)
314 }
315 var body struct{ Peer string }
316 if err := json.Unmarshal(data, &body); err != nil {
317 s.t.Fatalf("hello %s: %v", data, err)
318 }
319 return body.Peer
320}
321
322func (s *rawStream) close() {
323 s.once.Do(func() {
324 s.cancel()
325 s.body.Close()
326 })
327}
328
329// signal is POST /m/{sqid}/signal, the upstream half of §4.1: a JSON
330// body carrying the peer and the message, under CSRF. body is decoded
331// into a map and given "peer", rather than spliced in as a string — a
332// caller's body of "{}" or "" is not a special case this way, and every
333// call sends one well-formed JSON document. The endpoint itself arrives
334// with the signal handler; this is the client for it.
335func (c *client) signal(sqid, peer, body string) (*http.Response, []byte) {
336 c.t.Helper()
337 msg := map[string]any{}
338 if strings.TrimSpace(body) != "" {
339 if err := json.Unmarshal([]byte(body), &msg); err != nil {
340 c.t.Fatalf("signal body %q: %v", body, err)
341 }
342 }
343 msg["peer"] = peer
344 payload, err := json.Marshal(msg)
345 if err != nil {
346 c.t.Fatal(err)
347 }
348 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
349 defer cancel()
350 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.x.server().URL+"/m/"+sqid+"/signal", bytes.NewReader(payload))
351 if err != nil {
352 c.t.Fatal(err)
353 }
354 for _, ck := range c.cookies {
355 req.AddCookie(ck)
356 }
357 req.Header.Set("Content-Type", "application/json")
358 req.Header.Set("Origin", testOrigin)
359 resp, err := c.x.server().Client().Do(req)
360 if err != nil {
361 c.t.Fatalf("POST signal: %v", err)
362 }
363 b, err := io.ReadAll(resp.Body)
364 resp.Body.Close()
365 if err != nil {
366 c.t.Fatalf("POST signal: reading body: %v", err)
367 }
368 return resp, b
369}
370
371// signalRaw posts body verbatim, unlike signal, which always injects a
372// well-formed "peer" — for the shapes signal cannot express at all: a
373// malformed document, or one with no peer field.
374func (c *client) signalRaw(sqid, body string) (*http.Response, []byte) {
375 c.t.Helper()
376 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
377 defer cancel()
378 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.x.server().URL+"/m/"+sqid+"/signal", strings.NewReader(body))
379 if err != nil {
380 c.t.Fatal(err)
381 }
382 for _, ck := range c.cookies {
383 req.AddCookie(ck)
384 }
385 req.Header.Set("Content-Type", "application/json")
386 req.Header.Set("Origin", testOrigin)
387 resp, err := c.x.server().Client().Do(req)
388 if err != nil {
389 c.t.Fatalf("POST signal: %v", err)
390 }
391 b, err := io.ReadAll(resp.Body)
392 resp.Body.Close()
393 if err != nil {
394 c.t.Fatalf("POST signal: reading body: %v", err)
395 }
396 return resp, b
397}
398