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 "net/http"
6 "net/http/httptest"
7 "strings"
8 "testing"
9 "time"
10
11 "amadan.net/oficina/calls/protocol"
12)
13
14// mediaRoom is a Room seeded as connected-but-not-admitted, with every
15// POST it makes captured, so the tests below can drive receive() by hand
16// and see exactly which reports go out.
17func mediaRoom(t *testing.T) (*Room, chan protocol.Signal) {
18 t.Helper()
19 posted := make(chan protocol.Signal, 16)
20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
21 var s protocol.Signal
22 _ = json.NewDecoder(req.Body).Decode(&s)
23 posted <- s
24 }))
25 t.Cleanup(srv.Close)
26 r, err := New(srv.URL, "room", srv.Client(), func(Event) {})
27 if err != nil {
28 t.Fatal(err)
29 }
30 r.started = true
31 r.state.Peer = "self"
32 t.Cleanup(r.Close)
33 return r, posted
34}
35
36func nextMedia(t *testing.T, posted chan protocol.Signal) protocol.MediaState {
37 t.Helper()
38 select {
39 case s := <-posted:
40 if s.Type != protocol.TypeMedia || s.To != "" || s.Seq != 0 || s.Gen != 0 || s.Peer != "self" {
41 t.Fatalf("posted %+v, want an untargeted media report", s)
42 }
43 var m protocol.MediaState
44 if err := json.Unmarshal(s.Payload, &m); err != nil {
45 t.Fatal(err)
46 }
47 return m
48 case <-time.After(time.Second):
49 t.Fatal("no media report was posted")
50 }
51 return protocol.MediaState{}
52}
53
54func nothingPosted(t *testing.T, posted chan protocol.Signal, why string) {
55 t.Helper()
56 select {
57 case s := <-posted:
58 t.Fatalf("%s, but %+v was posted", why, s)
59 case <-time.After(150 * time.Millisecond):
60 }
61}
62
63func roster(peers ...protocol.RosterPeer) protocol.Message {
64 return protocol.Message{Type: protocol.TypeRoster, Roster: &protocol.Roster{Peers: peers}}
65}
66
67// §15.19: the host's own state is reported when it is admitted, after a
68// resume whose roster disagrees, and on every change while admitted —
69// and not while waiting, where there is nobody to tell.
70func TestOwnMediaIsReportedOnAdmissionAndOnChange(t *testing.T) {
71 r, posted := mediaRoom(t)
72
73 // Muted in the lobby: remembered, not sent.
74 if err := r.SetMedia(true, false); err != nil {
75 t.Fatal(err)
76 }
77 nothingPosted(t, posted, "a client that is not admitted has nobody to tell")
78
79 // Admitted, with no media for ourselves in the roster: the hub has
80 // never heard from this participation, so the report goes now.
81 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: "other", Gen: 1})); err != nil {
82 t.Fatal(err)
83 }
84 if m := nextMedia(t, posted); !m.Muted || m.CameraOff {
85 t.Fatalf("admission reported %+v, want muted", m)
86 }
87
88 // A later roster that already says what we say: nothing to send.
89 if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Muted: true, Hearing: []protocol.PeerID{}}}, protocol.RosterPeer{Peer: "other", Gen: 1})); err != nil {
90 t.Fatal(err)
91 }
92 nothingPosted(t, posted, "a roster that agrees needs no report")
93
94 // A change while admitted goes at once.
95 if err := r.SetMedia(true, true); err != nil {
96 t.Fatal(err)
97 }
98 if m := nextMedia(t, posted); !m.Muted || !m.CameraOff {
99 t.Fatalf("change reported %+v, want muted and camera off", m)
100 }
101
102 // A resume whose roster is behind — the report was lost while the
103 // stream was down — is corrected.
104 if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Muted: true}}, protocol.RosterPeer{Peer: "other", Gen: 1})); err != nil {
105 t.Fatal(err)
106 }
107 if m := nextMedia(t, posted); !m.Muted || !m.CameraOff {
108 t.Fatalf("resume reported %+v, want the host's current state", m)
109 }
110}
111
112// Other peers' state arrives two ways — in the roster and as `media`
113// events — and both land on the snapshot's peers, where the native UI
114// reads it. A peer that never reported stays nil: unknown, not "on".
115func TestRemoteMediaReachesTheSnapshot(t *testing.T) {
116 r, posted := mediaRoom(t)
117 if err := r.receive(roster(
118 protocol.RosterPeer{Peer: "self"},
119 protocol.RosterPeer{Peer: "old", Gen: 1},
120 protocol.RosterPeer{Peer: "new", Gen: 1, Media: &protocol.MediaState{Muted: true}},
121 )); err != nil {
122 t.Fatal(err)
123 }
124 <-posted // our own first report; covered above
125 before := r.Snapshot()
126
127 if err := r.receive(protocol.Message{Type: protocol.TypeMedia, From: "new", Payload: json.RawMessage(`{"muted":false,"cameraOff":true}`)}); err != nil {
128 t.Fatal(err)
129 }
130 // A malformed one, and one from nobody, change nothing.
131 _ = r.receive(protocol.Message{Type: protocol.TypeMedia, From: "old", Payload: json.RawMessage(`"nope"`)})
132 _ = r.receive(protocol.Message{Type: protocol.TypeMedia, Payload: json.RawMessage(`{"muted":true,"cameraOff":true}`)})
133
134 after := r.Snapshot()
135 byID := map[protocol.PeerID]*protocol.MediaState{}
136 for _, p := range after.Peers {
137 byID[p.Peer] = p.Media
138 }
139 if m := byID["new"]; m == nil || m.Muted || !m.CameraOff {
140 t.Fatalf("new = %+v, want unmuted with camera off", m)
141 }
142 if byID["old"] != nil {
143 t.Fatalf("a peer that never reported must stay unknown, got %+v", byID["old"])
144 }
145 // The snapshot taken before is untouched: the event replaced the
146 // pointer rather than writing through the one it had handed out.
147 for _, p := range before.Peers {
148 if p.Peer == "new" && (p.Media == nil || !p.Media.Muted || p.Media.CameraOff) {
149 t.Fatalf("an earlier snapshot changed under its holder: %+v", p.Media)
150 }
151 }
152 nothingPosted(t, posted, "hearing about somebody else sends nothing")
153}
154
155// A fresh join after a restart starts a new participation the hub knows
156// nothing about, so the host's state — kept across it — is reported again.
157func TestOwnMediaSurvivesAFreshJoin(t *testing.T) {
158 r, posted := mediaRoom(t)
159 _ = r.SetMedia(true, false)
160 _ = r.receive(roster(protocol.RosterPeer{Peer: "self"}))
161 nextMedia(t, posted)
162
163 r.fresh()
164 r.mu.Lock()
165 r.state.Peer = "self2"
166 r.mu.Unlock()
167 _ = r.receive(roster(protocol.RosterPeer{Peer: "self2"}))
168 select {
169 case s := <-posted:
170 var m protocol.MediaState
171 _ = json.Unmarshal(s.Payload, &m)
172 if s.Type != protocol.TypeMedia || s.Peer != "self2" || !m.Muted {
173 t.Fatalf("after a restart posted %+v (%+v), want the kept muted state from the new peer", s, m)
174 }
175 case <-time.After(time.Second):
176 t.Fatal("no report after the fresh join")
177 }
178}
179
180// §15.20: the peers this client hears silence from ride its own report.
181// Only peers in the roster other than itself are kept, in roster order,
182// so a stale or foreign id never reaches the hub; a list that changes
183// nothing sends nothing; and clearing it is a change back to the
184// two-key report.
185func TestNoSoundFromIsReportedFromTheRoster(t *testing.T) {
186 r, posted := mediaRoom(t)
187 a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb")
188 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
189 t.Fatal(err)
190 }
191 if m := nextMedia(t, posted); len(m.NoSoundFrom) != 0 || m.AudioBlocked {
192 t.Fatalf("admission report %+v, want the two-key report", m)
193 }
194 if err := r.SetNoSoundFrom([]protocol.PeerID{b, "self", "stranger", a}); err != nil {
195 t.Fatal(err)
196 }
197 m := nextMedia(t, posted)
198 if len(m.NoSoundFrom) != 2 || m.NoSoundFrom[0] != a || m.NoSoundFrom[1] != b {
199 t.Fatalf("noSoundFrom %v, want [aaaa bbbb] in roster order", m.NoSoundFrom)
200 }
201 if err := r.SetNoSoundFrom([]protocol.PeerID{a, b}); err != nil {
202 t.Fatal(err)
203 }
204 nothingPosted(t, posted, "the same list is not a change")
205 if err := r.SetMedia(true, false); err != nil {
206 t.Fatal(err)
207 }
208 if m := nextMedia(t, posted); !m.Muted || len(m.NoSoundFrom) != 2 {
209 t.Fatalf("muting kept the list? %+v", m)
210 }
211 if err := r.SetNoSoundFrom(nil); err != nil {
212 t.Fatal(err)
213 }
214 if m := nextMedia(t, posted); len(m.NoSoundFrom) != 0 || !m.Muted {
215 t.Fatalf("cleared %+v", m)
216 }
217}
218
219// §15.21: whose screen has stalled here is reported like noSoundFrom —
220// roster peers only, in roster order, a repeat is not a change — and it
221// travels beside the rest of the report.
222func TestScreenStalledIsReportedFromTheRoster(t *testing.T) {
223 r, posted := mediaRoom(t)
224 a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb")
225 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
226 t.Fatal(err)
227 }
228 if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 {
229 t.Fatalf("admission report %+v, want no stalled screens", m)
230 }
231 if err := r.SetScreenStalled([]protocol.PeerID{b, "self", "stranger"}); err != nil {
232 t.Fatal(err)
233 }
234 if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || m.ScreenStalled[0] != b {
235 t.Fatalf("screenStalled %v, want [bbbb]", m.ScreenStalled)
236 }
237 if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil {
238 t.Fatal(err)
239 }
240 nothingPosted(t, posted, "the same list is not a change")
241 if err := r.SetNoSoundFrom([]protocol.PeerID{a}); err != nil {
242 t.Fatal(err)
243 }
244 if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || len(m.NoSoundFrom) != 1 {
245 t.Fatalf("both lists travel together: %+v", m)
246 }
247 if err := r.SetScreenStalled(nil); err != nil {
248 t.Fatal(err)
249 }
250 if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || len(m.NoSoundFrom) != 1 {
251 t.Fatalf("cleared %+v", m)
252 }
253}
254
255// A server from before §15.21 refuses a report naming stalled screens
256// with 400. The core sends the same report again without them, and every
257// report after that goes without them too, so mute and camera state are
258// never lost to an old server.
259func TestScreenStalledFallsBackOnAnOldServer(t *testing.T) {
260 posted := make(chan protocol.Signal, 16)
261 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
262 var s protocol.Signal
263 _ = json.NewDecoder(req.Body).Decode(&s)
264 var m protocol.MediaState
265 _ = json.Unmarshal(s.Payload, &m)
266 posted <- s
267 // A server from before §15.21 knows neither key (nor §15.24's).
268 if len(m.ScreenStalled) > 0 || m.Hearing != nil {
269 w.WriteHeader(http.StatusBadRequest)
270 }
271 }))
272 t.Cleanup(srv.Close)
273 r, err := New(srv.URL, "room", srv.Client(), func(Event) {})
274 if err != nil {
275 t.Fatal(err)
276 }
277 r.started = true
278 r.state.Peer = "self"
279 t.Cleanup(r.Close)
280 b := protocol.PeerID("bbbb")
281 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
282 t.Fatal(err)
283 }
284 nextMedia(t, posted) // with hearing, refused
285 nextMedia(t, posted) // without it
286 if err := r.SetMedia(true, false); err != nil {
287 t.Fatal(err)
288 }
289 nextMedia(t, posted)
290 if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil {
291 t.Fatal(err)
292 }
293 if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 {
294 t.Fatalf("first report %+v, want the list", m)
295 }
296 if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || !m.Muted {
297 t.Fatalf("resent %+v, want the same report without the list", m)
298 }
299 if err := r.SetMedia(false, false); err != nil {
300 t.Fatal(err)
301 }
302 if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || m.Muted {
303 t.Fatalf("later report %+v, want it plain", m)
304 }
305}
306
307// §15.24: this core always says who it hears — [] until it hears anyone,
308// so it is never mistaken for a client that does not say — and the list
309// is kept like noSoundFrom's: roster peers only, in roster order, a
310// repeat not a change.
311func TestHearingIsAlwaysReportedFromTheRoster(t *testing.T) {
312 r, posted := mediaRoom(t)
313 a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb")
314 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
315 t.Fatal(err)
316 }
317 select {
318 case s := <-posted:
319 if !strings.Contains(string(s.Payload), `"hearing":[]`) {
320 t.Fatalf("admission report %s, want hearing: [] on the wire", s.Payload)
321 }
322 case <-time.After(time.Second):
323 t.Fatal("no admission report")
324 }
325 if err := r.SetHearing([]protocol.PeerID{b, "self", "stranger", a}); err != nil {
326 t.Fatal(err)
327 }
328 if m := nextMedia(t, posted); len(m.Hearing) != 2 || m.Hearing[0] != a || m.Hearing[1] != b {
329 t.Fatalf("hearing %v, want [aaaa bbbb] in roster order", m.Hearing)
330 }
331 if err := r.SetHearing([]protocol.PeerID{a, b}); err != nil {
332 t.Fatal(err)
333 }
334 nothingPosted(t, posted, "the same list is not a change")
335 if err := r.SetHearing(nil); err != nil {
336 t.Fatal(err)
337 }
338 if m := nextMedia(t, posted); m.Hearing == nil || len(m.Hearing) != 0 {
339 t.Fatalf("cleared %+v, want hearing: [] and not absent", m)
340 }
341 // A roster that carries the same [] needs nothing; one that lost the
342 // key (a restarted hub) is told again.
343 if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Hearing: []protocol.PeerID{}}}, protocol.RosterPeer{Peer: a, Gen: 1})); err != nil {
344 t.Fatal(err)
345 }
346 nothingPosted(t, posted, "a roster that agrees needs no report")
347 if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{}}, protocol.RosterPeer{Peer: a, Gen: 1})); err != nil {
348 t.Fatal(err)
349 }
350 if m := nextMedia(t, posted); m.Hearing == nil {
351 t.Fatalf("a roster without our hearing must be corrected, got %+v", m)
352 }
353}
354
355// A server from before §15.24 refuses every report from this core, which
356// always carries hearing. The core sends the same report again without
357// it and leaves it out from then on, so a mute is never lost; a server
358// from before §15.21 as well refuses the stalled screens next, and they
359// go too.
360func TestHearingFallsBackOnAnOldServer(t *testing.T) {
361 posted := make(chan protocol.Signal, 16)
362 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
363 var s protocol.Signal
364 _ = json.NewDecoder(req.Body).Decode(&s)
365 var m protocol.MediaState
366 _ = json.Unmarshal(s.Payload, &m)
367 posted <- s
368 if m.Hearing != nil || len(m.ScreenStalled) > 0 {
369 w.WriteHeader(http.StatusBadRequest)
370 }
371 }))
372 t.Cleanup(srv.Close)
373 r, err := New(srv.URL, "room", srv.Client(), func(Event) {})
374 if err != nil {
375 t.Fatal(err)
376 }
377 r.started = true
378 r.state.Peer = "self"
379 t.Cleanup(r.Close)
380 b := protocol.PeerID("bbbb")
381 _ = r.SetMedia(true, false)
382 if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
383 t.Fatal(err)
384 }
385 if m := nextMedia(t, posted); m.Hearing == nil {
386 t.Fatalf("first report %+v, want hearing", m)
387 }
388 if m := nextMedia(t, posted); m.Hearing != nil || !m.Muted {
389 t.Fatalf("resent %+v, want the same report without hearing", m)
390 }
391 if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil {
392 t.Fatal(err)
393 }
394 if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || m.Hearing != nil {
395 t.Fatalf("report %+v, want stalled screens and no hearing", m)
396 }
397 if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || m.Hearing != nil || !m.Muted {
398 t.Fatalf("resent %+v, want it plain and still muted", m)
399 }
400 if err := r.SetHearing([]protocol.PeerID{b}); err != nil {
401 t.Fatal(err)
402 }
403 if m := nextMedia(t, posted); m.Hearing != nil {
404 t.Fatalf("later report %+v, want no hearing", m)
405 }
406 // The roster agrees with what this server can hold: nothing to send.
407 if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Muted: true}}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil {
408 t.Fatal(err)
409 }
410 nothingPosted(t, posted, "a roster that agrees with the plain report needs nothing")
411}
412