orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "bufio"
5 "context"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 "time"
11)
12
13// The meta stream is what makes a turn started on the desktop show up on the
14// phone. It has to open immediately, since a proxy that buffers until it sees
15// output would otherwise hold the whole stream, and it has to stop when the tab
16// goes away rather than leaking a subscriber per reload.
17func TestEventsStreamsAndCleansUp(t *testing.T) {
18 s := &site{hub: NewHub()}
19 srv := httptest.NewServer(http.HandlerFunc(s.events))
20 defer srv.Close()
21
22 ctx, cancel := context.WithCancel(context.Background())
23 req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)
24 resp, err := http.DefaultClient.Do(req)
25 if err != nil {
26 t.Fatal(err)
27 }
28 defer resp.Body.Close()
29
30 if got := resp.Header.Get("Content-Type"); got != "text/event-stream" {
31 t.Errorf("content type = %q", got)
32 }
33
34 br := bufio.NewReader(resp.Body)
35 // The opening comment, so a buffering proxy lets the stream through.
36 line, err := br.ReadString('\n')
37 if err != nil || !strings.HasPrefix(line, ":") {
38 t.Fatalf("first line = %q, %v, want a comment", line, err)
39 }
40
41 // A subscriber has to be registered by now, or a publish would go nowhere.
42 deadline := time.Now().Add(2 * time.Second)
43 for s.hub.Len() == 0 && time.Now().Before(deadline) {
44 time.Sleep(5 * time.Millisecond)
45 }
46 if s.hub.Len() != 1 {
47 t.Fatalf("%d subscribers, want 1", s.hub.Len())
48 }
49
50 s.hub.Publish(HubEvent{Kind: "finished", ConvID: "c9", Title: "Named"})
51 var got string
52 for {
53 line, err := br.ReadString('\n')
54 if err != nil {
55 t.Fatalf("reading the stream: %v", err)
56 }
57 if strings.HasPrefix(line, "data: ") {
58 got = strings.TrimSpace(strings.TrimPrefix(line, "data: "))
59 break
60 }
61 }
62 if !strings.Contains(got, `"conversation_id":"c9"`) || !strings.Contains(got, `"kind":"finished"`) {
63 t.Errorf("frame = %s", got)
64 }
65
66 // A tab that goes away drops its subscriber rather than leaving one behind
67 // for every reload the phone does.
68 cancel()
69 resp.Body.Close()
70 for time.Now().Before(deadline) {
71 if s.hub.Len() == 0 {
72 return
73 }
74 time.Sleep(5 * time.Millisecond)
75 }
76 t.Errorf("%d subscribers left after the reader went away", s.hub.Len())
77}