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 "net/http"
5 "strings"
6 "testing"
7 "time"
8)
9
10// Spec §15.19 over the real handler: a peer's own media report reaches
11// the other admitted peer as `media`, from the hub's envelope, with the
12// payload re-encoded from the two booleans the handler judged — never
13// the bytes the sender chose — and the sender hears nothing back.
14func TestMediaReachesTheOtherPeerCanonicallyAndNotTheSender(t *testing.T) {
15 a, sqid, pa, sb := chatPair(t)
16
17 // Whitespace, and the keys in the other order: both legal, and
18 // neither must survive onto Bob's stream.
19 body := `{"peer":"` + pa + `","type":"media","payload":{ "cameraOff" : false , "muted" : true }}`
20 res, rb := a.signalRaw(sqid, body)
21 if res.StatusCode != http.StatusOK {
22 t.Fatalf("media: %d %s", res.StatusCode, rb)
23 }
24 ev, data, ok := sb.nextOf("media", 2*time.Second)
25 if !ok || ev != "media" {
26 t.Fatal("Bob did not receive the media report")
27 }
28 s := string(data)
29 if !strings.Contains(s, `"from":"`+pa+`"`) || !strings.Contains(s, `"name":"paul"`) ||
30 !strings.Contains(s, `"payload":{"muted":true,"cameraOff":false}`) {
31 t.Fatalf("envelope %s", s)
32 }
33 if strings.Contains(s, `"seq"`) || strings.Contains(s, `"to"`) || strings.Contains(s, `"gen"`) {
34 t.Fatalf("a media envelope carries no seq, to or gen: %s", s)
35 }
36
37 // The same state again is accepted and silent.
38 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":true,"cameraOff":false}}`)
39 if res.StatusCode != http.StatusOK {
40 t.Fatalf("repeat: %d", res.StatusCode)
41 }
42 if ev, _, ok := sb.next(300 * time.Millisecond); ok && ev == "media" {
43 t.Fatal("a report that changed nothing reached Bob")
44 }
45}
46
47// §15.19's "state, not event": a peer who joins after the report reads
48// it from the roster it is admitted with, and a peer who has never
49// reported — an old client — is in that roster with no media at all.
50func TestMediaIsInTheRosterALateJoinerGets(t *testing.T) {
51 x, sqid, _ := memberWithMeeting(t)
52 a := x.browser(t)
53 a.signIn("paul@example.com")
54 sa := a.stream("/m/" + sqid + "/stream")
55 defer sa.close()
56 pa, _ := sa.hello()
57 a.signal(sqid, pa, `{"type":"join"}`)
58 sa.nextOf("roster", 2*time.Second)
59 if res, rb := a.signal(sqid, pa, `{"type":"media","payload":{"muted":true,"cameraOff":true}}`); res.StatusCode != http.StatusOK {
60 t.Fatalf("media: %d %s", res.StatusCode, rb)
61 }
62
63 b := newMemberBrowser(t, x, "paul@rslw.com")
64 sb := b.stream("/m/" + sqid + "/stream")
65 defer sb.close()
66 pb, _ := sb.hello()
67 b.signal(sqid, pb, `{"type":"join"}`)
68 _, data, ok := sb.nextOf("roster", 2*time.Second)
69 if !ok {
70 t.Fatal("Bob got no roster")
71 }
72 s := string(data)
73 want := `"peer":"` + pa + `","name":"paul","kind":"member","gen":1,"media":{"muted":true,"cameraOff":true}`
74 if !strings.Contains(s, want) {
75 t.Fatalf("Bob's roster should carry Paul's media; got %s", s)
76 }
77 // Bob has not reported, so his own entry carries no media key.
78 if strings.Count(s, `"media"`) != 1 {
79 t.Fatalf("only Paul has reported; roster %s", s)
80 }
81}
82
83// The shape is exact, and judged on raw bytes the way chat and share
84// are (§15.15): both keys, spelled exactly, each once, booleans, and
85// nothing else — and never targeted.
86func TestMediaShapes(t *testing.T) {
87 a, sqid, pa, _ := chatPair(t)
88 bad := []struct {
89 name, payload string
90 }{
91 {"missing cameraOff", `{"muted":true}`},
92 {"missing muted", `{"cameraOff":true}`},
93 {"empty object", `{}`},
94 {"a third key", `{"muted":true,"cameraOff":false,"deafened":true}`},
95 {"a capitalised key", `{"Muted":true,"cameraOff":false}`},
96 {"snake case", `{"muted":true,"camera_off":false}`},
97 {"a duplicated key", `{"muted":true,"muted":false,"cameraOff":false}`},
98 {"a string for a bool", `{"muted":"yes","cameraOff":false}`},
99 {"null for a bool", `{"muted":null,"cameraOff":false}`},
100 {"a number for a bool", `{"muted":1,"cameraOff":0}`},
101 {"not an object", `[true,false]`},
102 {"trailing content", `{"muted":true,"cameraOff":false} {}`},
103 // §15.20's two optional members, each held as hard as the two
104 // required ones.
105 {"audioBlocked null", `{"muted":true,"cameraOff":false,"audioBlocked":null}`},
106 {"audioBlocked a string", `{"muted":true,"cameraOff":false,"audioBlocked":"yes"}`},
107 {"audioBlocked twice", `{"muted":true,"cameraOff":false,"audioBlocked":true,"audioBlocked":false}`},
108 {"audioBlocked alone", `{"audioBlocked":true}`},
109 {"noSoundFrom null", `{"muted":true,"cameraOff":false,"noSoundFrom":null}`},
110 {"noSoundFrom an object", `{"muted":true,"cameraOff":false,"noSoundFrom":{"` + strings.Repeat("a", 32) + `":true}}`},
111 {"noSoundFrom a short id", `{"muted":true,"cameraOff":false,"noSoundFrom":["abc"]}`},
112 {"noSoundFrom an upper-case id", `{"muted":true,"cameraOff":false,"noSoundFrom":["` + strings.Repeat("A", 32) + `"]}`},
113 {"noSoundFrom a number", `{"muted":true,"cameraOff":false,"noSoundFrom":[1]}`},
114 {"noSoundFrom a repeat", `{"muted":true,"cameraOff":false,"noSoundFrom":["` + strings.Repeat("a", 32) + `","` + strings.Repeat("a", 32) + `"]}`},
115 {"noSoundFrom sixteen ids", `{"muted":true,"cameraOff":false,"noSoundFrom":[` + sixteenIDs() + `]}`},
116 // §15.21's screenStalled, held exactly as noSoundFrom is.
117 {"screenStalled null", `{"muted":true,"cameraOff":false,"screenStalled":null}`},
118 {"screenStalled a bool", `{"muted":true,"cameraOff":false,"screenStalled":true}`},
119 {"screenStalled a short id", `{"muted":true,"cameraOff":false,"screenStalled":["abc"]}`},
120 {"screenStalled a repeat", `{"muted":true,"cameraOff":false,"screenStalled":["` + strings.Repeat("a", 32) + `","` + strings.Repeat("a", 32) + `"]}`},
121 {"screenStalled sixteen ids", `{"muted":true,"cameraOff":false,"screenStalled":[` + sixteenIDs() + `]}`},
122 {"screenStalled twice", `{"muted":true,"cameraOff":false,"screenStalled":[],"screenStalled":[]}`},
123 {"screenStalled snake case", `{"muted":true,"cameraOff":false,"screen_stalled":[]}`},
124 // §15.24's hearing, held exactly as noSoundFrom is.
125 {"hearing null", `{"muted":true,"cameraOff":false,"hearing":null}`},
126 {"hearing a bool", `{"muted":true,"cameraOff":false,"hearing":true}`},
127 {"hearing an object", `{"muted":true,"cameraOff":false,"hearing":{"` + strings.Repeat("a", 32) + `":true}}`},
128 {"hearing a short id", `{"muted":true,"cameraOff":false,"hearing":["abc"]}`},
129 {"hearing an upper-case id", `{"muted":true,"cameraOff":false,"hearing":["` + strings.Repeat("A", 32) + `"]}`},
130 {"hearing a number", `{"muted":true,"cameraOff":false,"hearing":[1]}`},
131 {"hearing a repeat", `{"muted":true,"cameraOff":false,"hearing":["` + strings.Repeat("a", 32) + `","` + strings.Repeat("a", 32) + `"]}`},
132 {"hearing sixteen ids", `{"muted":true,"cameraOff":false,"hearing":[` + sixteenIDs() + `]}`},
133 {"hearing twice", `{"muted":true,"cameraOff":false,"hearing":[],"hearing":[]}`},
134 {"hearing capitalised", `{"muted":true,"cameraOff":false,"Hearing":[]}`},
135 {"hearing alone", `{"hearing":[]}`},
136 }
137 for _, c := range bad {
138 t.Run(c.name, func(t *testing.T) {
139 body := `{"peer":"` + pa + `","type":"media","payload":` + c.payload + `}`
140 res, rb := a.signalRaw(sqid, body)
141 if res.StatusCode != http.StatusBadRequest || !strings.Contains(string(rb), `"error":"bad-request"`) {
142 t.Fatalf("%d %s, want 400 bad-request", res.StatusCode, rb)
143 }
144 })
145 }
146 t.Run("no payload at all", func(t *testing.T) {
147 res, rb := a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media"}`)
148 if res.StatusCode != http.StatusBadRequest {
149 t.Fatalf("%d %s, want 400", res.StatusCode, rb)
150 }
151 })
152 t.Run("targeted", func(t *testing.T) {
153 res, rb := a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media","to":"`+strings.Repeat("a", 32)+`","payload":{"muted":true,"cameraOff":false}}`)
154 if res.StatusCode != http.StatusBadRequest {
155 t.Fatalf("%d %s, want 400", res.StatusCode, rb)
156 }
157 })
158 t.Run("with a seq", func(t *testing.T) {
159 res, rb := a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media","seq":3,"payload":{"muted":true,"cameraOff":false}}`)
160 if res.StatusCode != http.StatusBadRequest {
161 t.Fatalf("%d %s, want 400", res.StatusCode, rb)
162 }
163 })
164 t.Run("padded past the cap", func(t *testing.T) {
165 payload := `{"muted":true,` + strings.Repeat(" ", 3072) + `"cameraOff":false}`
166 res, rb := a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media","payload":`+payload+`}`)
167 if res.StatusCode != http.StatusRequestEntityTooLarge || !strings.Contains(string(rb), `"error":"too-large"`) {
168 t.Fatalf("%d %s, want 413 too-large", res.StatusCode, rb)
169 }
170 })
171}
172
173func sixteenIDs() string {
174 ids := make([]string, 16)
175 for i := range ids {
176 ids[i] = `"` + strings.Repeat("0", 30) + string("0123456789abcdef"[i]) + `0"`
177 }
178 return strings.Join(ids, ",")
179}
180
181// Spec §15.20: the receiving half rides the same report. A browser that
182// was refused sound, and one hearing silence from a peer, say so; the
183// hub carries it canonically to the others and in rosters; the ordinary
184// report is still the two-key object, so a report that clears the bad
185// news is a change that goes out as exactly that.
186func TestMediaCarriesWhatTheReceiverCanHear(t *testing.T) {
187 a, sqid, pa, sb := chatPair(t)
188 silent := strings.Repeat("0", 31) + "1"
189 body := `{"peer":"` + pa + `","type":"media","payload":{"noSoundFrom":["` + silent + `"], "audioBlocked":true,"cameraOff":false,"muted":false}}`
190 res, rb := a.signalRaw(sqid, body)
191 if res.StatusCode != http.StatusOK {
192 t.Fatalf("media: %d %s", res.StatusCode, rb)
193 }
194 _, data, ok := sb.nextOf("media", 2*time.Second)
195 if !ok {
196 t.Fatal("Bob did not receive the report")
197 }
198 want := `"payload":{"muted":false,"cameraOff":false,"audioBlocked":true,"noSoundFrom":["` + silent + `"]}`
199 if !strings.Contains(string(data), want) {
200 t.Fatalf("envelope %s, want %s", data, want)
201 }
202
203 // Fifteen ids is the most there can be, and is accepted.
204 ids := strings.Split(sixteenIDs(), ",")[:15]
205 res, rb = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"noSoundFrom":[`+strings.Join(ids, ",")+`]}}`)
206 if res.StatusCode != http.StatusOK {
207 t.Fatalf("fifteen: %d %s", res.StatusCode, rb)
208 }
209 sb.nextOf("media", 2*time.Second)
210
211 // Cleared: back to the two-key report, which is a change.
212 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"noSoundFrom":[]}}`)
213 if res.StatusCode != http.StatusOK {
214 t.Fatalf("clear: %d", res.StatusCode)
215 }
216 _, data, ok = sb.nextOf("media", 2*time.Second)
217 if !ok || !strings.Contains(string(data), `"payload":{"muted":false,"cameraOff":false}`) {
218 t.Fatalf("cleared report %s", data)
219 }
220 // And an empty list is the same report as no list at all.
221 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"audioBlocked":false}}`)
222 if res.StatusCode != http.StatusOK {
223 t.Fatalf("repeat: %d", res.StatusCode)
224 }
225 if ev, _, ok := sb.next(300 * time.Millisecond); ok && ev == "media" {
226 t.Fatal("a report that changed nothing reached Bob")
227 }
228}
229
230// Spec §15.21: a receiver whose view of someone's screen has stalled says
231// so in the same report, beside §15.20's lists; the hub carries it in the
232// canonical order, and the longest honest report — both lists full — is
233// accepted.
234func TestMediaCarriesStalledScreens(t *testing.T) {
235 a, sqid, pa, sb := chatPair(t)
236 sharer := strings.Repeat("0", 31) + "2"
237 res, rb := a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media","payload":{"screenStalled":["`+sharer+`"],"muted":false,"cameraOff":true}}`)
238 if res.StatusCode != http.StatusOK {
239 t.Fatalf("media: %d %s", res.StatusCode, rb)
240 }
241 _, data, ok := sb.nextOf("media", 2*time.Second)
242 want := `"payload":{"muted":false,"cameraOff":true,"screenStalled":["` + sharer + `"]}`
243 if !ok || !strings.Contains(string(data), want) {
244 t.Fatalf("envelope %s, want %s", data, want)
245 }
246 ids := strings.Join(strings.Split(sixteenIDs(), ",")[:15], ",")
247 res, rb = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"audioBlocked":true,"noSoundFrom":[`+ids+`],"screenStalled":[`+ids+`]}}`)
248 if res.StatusCode != http.StatusOK {
249 t.Fatalf("both lists full: %d %s", res.StatusCode, rb)
250 }
251 _, data, ok = sb.nextOf("media", 2*time.Second)
252 if !ok || !strings.Contains(string(data), `"noSoundFrom":[`+ids+`],"screenStalled":[`+ids+`]}`) {
253 t.Fatalf("full report %s", data)
254 }
255 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"screenStalled":[]}}`)
256 if res.StatusCode != http.StatusOK {
257 t.Fatalf("clear: %d", res.StatusCode)
258 }
259 _, data, ok = sb.nextOf("media", 2*time.Second)
260 if !ok || !strings.Contains(string(data), `"payload":{"muted":false,"cameraOff":false}`) {
261 t.Fatalf("cleared report %s", data)
262 }
263}
264
265// Spec §15.24: who this client can actually hear. Good news, unlike the
266// lists before it, and its presence is news too: an empty list ("nobody
267// yet") travels as [] and is a different report from one without the
268// key, which is a client that does not say. The longest honest report —
269// all three lists full — fits under the cap.
270func TestMediaCarriesWhoTheReceiverHears(t *testing.T) {
271 a, sqid, pa, sb := chatPair(t)
272 res, rb := a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"hearing":[]}}`)
273 if res.StatusCode != http.StatusOK {
274 t.Fatalf("empty hearing: %d %s", res.StatusCode, rb)
275 }
276 _, data, ok := sb.nextOf("media", 2*time.Second)
277 if !ok || !strings.Contains(string(data), `"payload":{"muted":false,"cameraOff":false,"hearing":[]}`) {
278 t.Fatalf("empty hearing must travel as []: %s", data)
279 }
280 heard := strings.Repeat("0", 31) + "3"
281 res, rb = a.signalRaw(sqid, `{"peer":"`+pa+`","type":"media","payload":{"hearing":["`+heard+`"],"muted":true,"cameraOff":false,"noSoundFrom":["`+heard+`"]}}`)
282 if res.StatusCode != http.StatusOK {
283 t.Fatalf("hearing: %d %s", res.StatusCode, rb)
284 }
285 _, data, ok = sb.nextOf("media", 2*time.Second)
286 want := `"payload":{"muted":true,"cameraOff":false,"noSoundFrom":["` + heard + `"],"hearing":["` + heard + `"]}`
287 if !ok || !strings.Contains(string(data), want) {
288 t.Fatalf("envelope %s, want %s", data, want)
289 }
290 ids := strings.Join(strings.Split(sixteenIDs(), ",")[:15], ",")
291 full := `{"muted":false,"cameraOff":false,"audioBlocked":true,"noSoundFrom":[` + ids + `],"screenStalled":[` + ids + `],"hearing":[` + ids + `]}`
292 res, rb = a.signal(sqid, pa, `{"type":"media","payload":`+full+`}`)
293 if res.StatusCode != http.StatusOK {
294 t.Fatalf("all three lists full: %d %s", res.StatusCode, rb)
295 }
296 _, data, ok = sb.nextOf("media", 2*time.Second)
297 if !ok || !strings.Contains(string(data), `"payload":`+full) {
298 t.Fatalf("full report %s", data)
299 }
300 // Without the key: a different report from the empty list, so it is
301 // a change and goes out as the two-key object.
302 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false,"hearing":[]}}`)
303 if res.StatusCode != http.StatusOK {
304 t.Fatalf("back to empty: %d", res.StatusCode)
305 }
306 sb.nextOf("media", 2*time.Second)
307 res, _ = a.signal(sqid, pa, `{"type":"media","payload":{"muted":false,"cameraOff":false}}`)
308 if res.StatusCode != http.StatusOK {
309 t.Fatalf("no key: %d", res.StatusCode)
310 }
311 _, data, ok = sb.nextOf("media", 2*time.Second)
312 if !ok || !strings.Contains(string(data), `"payload":{"muted":false,"cameraOff":false}`) {
313 t.Fatalf("absent hearing must be a change from [], got %s", data)
314 }
315}
316
317// A waiting guest has nobody to tell: its report is the usual 404, and
318// it hears nothing of the room's.
319func TestMediaFromAWaitingGuestIsTheUsual404(t *testing.T) {
320 x, sqid, tok := memberWithMeeting(t)
321 enableWaitingRoom(t, x, sqid)
322 a := x.browser(t)
323 a.signIn("paul@example.com")
324 sa := a.stream("/m/" + sqid + "/stream")
325 defer sa.close()
326 pa, _ := sa.hello()
327 a.signal(sqid, pa, `{"type":"join"}`)
328 sa.nextOf("roster", 2*time.Second)
329
330 g := x.browser(t)
331 g.get("/j/" + tok)
332 g.signIn("waiting-guest@example.com")
333 gs := g.stream("/m/" + sqid + "/stream")
334 defer gs.close()
335 gpeer, _ := gs.hello()
336 g.signal(sqid, gpeer, `{"type":"join"}`)
337 sa.nextOf("knock", 2*time.Second)
338
339 res, rb := g.signal(sqid, gpeer, `{"type":"media","payload":{"muted":true,"cameraOff":false}}`)
340 if res.StatusCode != http.StatusNotFound || !strings.Contains(string(rb), `"error":"not-found"`) {
341 t.Fatalf("a waiting guest's media must be the usual 404: %d %s", res.StatusCode, rb)
342 }
343 a.signal(sqid, pa, `{"type":"media","payload":{"muted":true,"cameraOff":false}}`)
344 if ev, _, ok := gs.next(400 * time.Millisecond); ok && ev == "media" {
345 t.Fatal("a waiting guest heard a media report")
346 }
347}
348