oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package callscore
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "sync"
11 "testing"
12
13 "amadan.net/oficina/calls/protocol"
14)
15
16const testToken = "0123456789abcdef0123456789abcdef"
17
18// reportServer answers /m/room/{quality,problem} with status (and body),
19// recording what it was sent.
20type reportServer struct {
21 *httptest.Server
22 mu sync.Mutex
23 paths []string
24 tokens []string
25 origins []string
26 bodies []string
27}
28
29func newReportServer(t *testing.T, status int, body string) *reportServer {
30 s := &reportServer{}
31 s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
32 b, _ := io.ReadAll(req.Body)
33 s.mu.Lock()
34 s.paths = append(s.paths, req.Method+" "+req.URL.Path)
35 s.tokens = append(s.tokens, req.Header.Get("X-Calls-Report"))
36 s.origins = append(s.origins, req.Header.Get("Origin"))
37 s.bodies = append(s.bodies, string(b))
38 s.mu.Unlock()
39 w.WriteHeader(status)
40 _, _ = io.WriteString(w, body)
41 }))
42 t.Cleanup(s.Close)
43 return s
44}
45
46// helloed is a room that has been given token by a hello, as receive
47// would record it.
48func helloed(t *testing.T, srv *httptest.Server, token string) *Room {
49 r, err := New(srv.URL, "room", srv.Client(), func(Event) {})
50 if err != nil {
51 t.Fatal(err)
52 }
53 r.started = true
54 // Already joined, so the hello is a resume and sends no join.
55 r.state.Peer = "self"
56 if err := r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: token}); err != nil {
57 t.Fatal(err)
58 }
59 return r
60}
61
62func TestHelloReportTokenIsKeptLatestWins(t *testing.T) {
63 srv := newReportServer(t, http.StatusAccepted, "")
64 r := helloed(t, srv.Server, testToken)
65 defer r.Close()
66 if got := r.reportTokenNow(); got != testToken {
67 t.Fatalf("token = %q", got)
68 }
69 // A resumed hello without one (an older hub) keeps it; a new one wins.
70 _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self"})
71 if got := r.reportTokenNow(); got != testToken {
72 t.Fatalf("token after a hello without one = %q", got)
73 }
74 next := strings.Repeat("f", 32)
75 _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: next})
76 if got := r.reportTokenNow(); got != next {
77 t.Fatalf("token after a new hello = %q", got)
78 }
79}
80
81func TestSendQualityOutcomes(t *testing.T) {
82 report := []byte(`{"v":1,"seconds":30,"client":{"app":"mac"}}`)
83 for _, tc := range []struct {
84 status int
85 stop bool
86 outcome ReportOutcome
87 }{
88 {http.StatusAccepted, false, ReportSent},
89 {http.StatusNoContent, true, ReportSent},
90 {http.StatusNotFound, false, ReportGone},
91 {http.StatusTooManyRequests, false, ReportTooMany},
92 {http.StatusBadRequest, false, ReportFailed},
93 {http.StatusRequestEntityTooLarge, false, ReportFailed},
94 {http.StatusInternalServerError, false, ReportFailed},
95 } {
96 t.Run(fmt.Sprint(tc.status), func(t *testing.T) {
97 srv := newReportServer(t, tc.status, "")
98 r := helloed(t, srv.Server, testToken)
99 // After Close: the final report goes at leave, when the room
100 // is already shut.
101 r.Close()
102 stop, err := r.SendQuality(report)
103 if stop != tc.stop || OutcomeOf(err) != tc.outcome {
104 t.Fatalf("stop=%v err=%v, want stop=%v %s", stop, err, tc.stop, tc.outcome)
105 }
106 srv.mu.Lock()
107 defer srv.mu.Unlock()
108 if len(srv.paths) != 1 || srv.paths[0] != "POST /m/room/quality" || srv.tokens[0] != testToken ||
109 srv.origins[0] != srv.URL || srv.bodies[0] != string(report) {
110 t.Fatalf("request: %v %v %v %v", srv.paths, srv.tokens, srv.origins, srv.bodies)
111 }
112 })
113 }
114}
115
116func TestSendProblemAnswersTheReference(t *testing.T) {
117 report := []byte(`{"note":"it growled","diagnostics":"peer abc123","client":{"app":"mac"}}`)
118 srv := newReportServer(t, http.StatusOK, `{"ref":"K7Q2M9PD"}`)
119 r := helloed(t, srv.Server, testToken)
120 r.Leave()
121 ref, err := r.SendProblem(report)
122 if err != nil || ref != "K7Q2M9PD" {
123 t.Fatalf("ref=%q err=%v", ref, err)
124 }
125 srv.mu.Lock()
126 defer srv.mu.Unlock()
127 last := len(srv.paths) - 1
128 if srv.paths[last] != "POST /m/room/problem" || srv.tokens[last] != testToken || srv.bodies[last] != string(report) {
129 t.Fatalf("request: %v %v", srv.paths, srv.tokens)
130 }
131}
132
133func TestSendProblemRefusals(t *testing.T) {
134 report := []byte(`{"note":"","diagnostics":"","client":{"app":"mac"}}`)
135 for _, tc := range []struct {
136 status int
137 body string
138 outcome ReportOutcome
139 }{
140 {http.StatusTooManyRequests, `{"error":"rate"}`, ReportTooMany},
141 {http.StatusNotFound, `{"error":"not-found"}`, ReportGone},
142 {http.StatusBadRequest, `{"error":"bad-request"}`, ReportFailed},
143 // A 200 that is not a reference is not shown as one.
144 {http.StatusOK, `{"ref":"<script>"}`, ReportFailed},
145 {http.StatusOK, `not json`, ReportFailed},
146 } {
147 t.Run(fmt.Sprint(tc.status, tc.body), func(t *testing.T) {
148 srv := newReportServer(t, tc.status, tc.body)
149 r := helloed(t, srv.Server, testToken)
150 defer r.Close()
151 ref, err := r.SendProblem(report)
152 if ref != "" || OutcomeOf(err) != tc.outcome {
153 t.Fatalf("ref=%q err=%v, want %s", ref, err, tc.outcome)
154 }
155 })
156 }
157}
158
159func TestReportsNeedATokenAndABoundedBody(t *testing.T) {
160 srv := newReportServer(t, http.StatusAccepted, "")
161 // No token: a server from before §15.26 never gave one. Nothing is sent.
162 r := helloed(t, srv.Server, "")
163 defer r.Close()
164 if _, err := r.SendQuality([]byte(`{}`)); OutcomeOf(err) != ReportGone {
165 t.Fatalf("no token: %v", err)
166 }
167 if _, err := r.SendProblem([]byte(`{}`)); OutcomeOf(err) != ReportGone {
168 t.Fatalf("no token: %v", err)
169 }
170 r.mu.Lock()
171 r.report = testToken
172 r.mu.Unlock()
173 big, _ := json.Marshal(map[string]string{"x": strings.Repeat("a", protocol.MaxQualityBody)})
174 if _, err := r.SendQuality(big); OutcomeOf(err) != ReportFailed {
175 t.Fatalf("oversized quality: %v", err)
176 }
177 if _, err := r.SendQuality([]byte(`{not json`)); OutcomeOf(err) != ReportFailed {
178 t.Fatalf("invalid quality: %v", err)
179 }
180 huge, _ := json.Marshal(map[string]string{"x": strings.Repeat("a", protocol.MaxProblemBody)})
181 if _, err := r.SendProblem(huge); OutcomeOf(err) != ReportFailed {
182 t.Fatalf("oversized problem: %v", err)
183 }
184 srv.mu.Lock()
185 defer srv.mu.Unlock()
186 if len(srv.paths) != 0 {
187 t.Fatalf("sent anyway: %v", srv.paths)
188 }
189}
190
191func TestReportNeverFollowsARedirect(t *testing.T) {
192 reached := false
193 foreign := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true }))
194 defer foreign.Close()
195 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
196 http.Redirect(w, req, foreign.URL, http.StatusTemporaryRedirect)
197 }))
198 defer srv.Close()
199 r := helloed(t, srv, testToken)
200 defer r.Close()
201 if _, err := r.SendProblem([]byte(`{}`)); OutcomeOf(err) != ReportFailed || reached {
202 t.Fatalf("err=%v reached=%v", err, reached)
203 }
204}
205