package callscore import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "amadan.net/oficina/calls/protocol" ) // mediaRoom is a Room seeded as connected-but-not-admitted, with every // POST it makes captured, so the tests below can drive receive() by hand // and see exactly which reports go out. func mediaRoom(t *testing.T) (*Room, chan protocol.Signal) { t.Helper() posted := make(chan protocol.Signal, 16) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var s protocol.Signal _ = json.NewDecoder(req.Body).Decode(&s) posted <- s })) t.Cleanup(srv.Close) r, err := New(srv.URL, "room", srv.Client(), func(Event) {}) if err != nil { t.Fatal(err) } r.started = true r.state.Peer = "self" t.Cleanup(r.Close) return r, posted } func nextMedia(t *testing.T, posted chan protocol.Signal) protocol.MediaState { t.Helper() select { case s := <-posted: if s.Type != protocol.TypeMedia || s.To != "" || s.Seq != 0 || s.Gen != 0 || s.Peer != "self" { t.Fatalf("posted %+v, want an untargeted media report", s) } var m protocol.MediaState if err := json.Unmarshal(s.Payload, &m); err != nil { t.Fatal(err) } return m case <-time.After(time.Second): t.Fatal("no media report was posted") } return protocol.MediaState{} } func nothingPosted(t *testing.T, posted chan protocol.Signal, why string) { t.Helper() select { case s := <-posted: t.Fatalf("%s, but %+v was posted", why, s) case <-time.After(150 * time.Millisecond): } } func roster(peers ...protocol.RosterPeer) protocol.Message { return protocol.Message{Type: protocol.TypeRoster, Roster: &protocol.Roster{Peers: peers}} } // §15.19: the host's own state is reported when it is admitted, after a // resume whose roster disagrees, and on every change while admitted — // and not while waiting, where there is nobody to tell. func TestOwnMediaIsReportedOnAdmissionAndOnChange(t *testing.T) { r, posted := mediaRoom(t) // Muted in the lobby: remembered, not sent. if err := r.SetMedia(true, false); err != nil { t.Fatal(err) } nothingPosted(t, posted, "a client that is not admitted has nobody to tell") // Admitted, with no media for ourselves in the roster: the hub has // never heard from this participation, so the report goes now. if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: "other", Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); !m.Muted || m.CameraOff { t.Fatalf("admission reported %+v, want muted", m) } // A later roster that already says what we say: nothing to send. 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 { t.Fatal(err) } nothingPosted(t, posted, "a roster that agrees needs no report") // A change while admitted goes at once. if err := r.SetMedia(true, true); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); !m.Muted || !m.CameraOff { t.Fatalf("change reported %+v, want muted and camera off", m) } // A resume whose roster is behind — the report was lost while the // stream was down — is corrected. if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Muted: true}}, protocol.RosterPeer{Peer: "other", Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); !m.Muted || !m.CameraOff { t.Fatalf("resume reported %+v, want the host's current state", m) } } // Other peers' state arrives two ways — in the roster and as `media` // events — and both land on the snapshot's peers, where the native UI // reads it. A peer that never reported stays nil: unknown, not "on". func TestRemoteMediaReachesTheSnapshot(t *testing.T) { r, posted := mediaRoom(t) if err := r.receive(roster( protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: "old", Gen: 1}, protocol.RosterPeer{Peer: "new", Gen: 1, Media: &protocol.MediaState{Muted: true}}, )); err != nil { t.Fatal(err) } <-posted // our own first report; covered above before := r.Snapshot() if err := r.receive(protocol.Message{Type: protocol.TypeMedia, From: "new", Payload: json.RawMessage(`{"muted":false,"cameraOff":true}`)}); err != nil { t.Fatal(err) } // A malformed one, and one from nobody, change nothing. _ = r.receive(protocol.Message{Type: protocol.TypeMedia, From: "old", Payload: json.RawMessage(`"nope"`)}) _ = r.receive(protocol.Message{Type: protocol.TypeMedia, Payload: json.RawMessage(`{"muted":true,"cameraOff":true}`)}) after := r.Snapshot() byID := map[protocol.PeerID]*protocol.MediaState{} for _, p := range after.Peers { byID[p.Peer] = p.Media } if m := byID["new"]; m == nil || m.Muted || !m.CameraOff { t.Fatalf("new = %+v, want unmuted with camera off", m) } if byID["old"] != nil { t.Fatalf("a peer that never reported must stay unknown, got %+v", byID["old"]) } // The snapshot taken before is untouched: the event replaced the // pointer rather than writing through the one it had handed out. for _, p := range before.Peers { if p.Peer == "new" && (p.Media == nil || !p.Media.Muted || p.Media.CameraOff) { t.Fatalf("an earlier snapshot changed under its holder: %+v", p.Media) } } nothingPosted(t, posted, "hearing about somebody else sends nothing") } // A fresh join after a restart starts a new participation the hub knows // nothing about, so the host's state — kept across it — is reported again. func TestOwnMediaSurvivesAFreshJoin(t *testing.T) { r, posted := mediaRoom(t) _ = r.SetMedia(true, false) _ = r.receive(roster(protocol.RosterPeer{Peer: "self"})) nextMedia(t, posted) r.fresh() r.mu.Lock() r.state.Peer = "self2" r.mu.Unlock() _ = r.receive(roster(protocol.RosterPeer{Peer: "self2"})) select { case s := <-posted: var m protocol.MediaState _ = json.Unmarshal(s.Payload, &m) if s.Type != protocol.TypeMedia || s.Peer != "self2" || !m.Muted { t.Fatalf("after a restart posted %+v (%+v), want the kept muted state from the new peer", s, m) } case <-time.After(time.Second): t.Fatal("no report after the fresh join") } } // §15.20: the peers this client hears silence from ride its own report. // Only peers in the roster other than itself are kept, in roster order, // so a stale or foreign id never reaches the hub; a list that changes // nothing sends nothing; and clearing it is a change back to the // two-key report. func TestNoSoundFromIsReportedFromTheRoster(t *testing.T) { r, posted := mediaRoom(t) a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb") if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.NoSoundFrom) != 0 || m.AudioBlocked { t.Fatalf("admission report %+v, want the two-key report", m) } if err := r.SetNoSoundFrom([]protocol.PeerID{b, "self", "stranger", a}); err != nil { t.Fatal(err) } m := nextMedia(t, posted) if len(m.NoSoundFrom) != 2 || m.NoSoundFrom[0] != a || m.NoSoundFrom[1] != b { t.Fatalf("noSoundFrom %v, want [aaaa bbbb] in roster order", m.NoSoundFrom) } if err := r.SetNoSoundFrom([]protocol.PeerID{a, b}); err != nil { t.Fatal(err) } nothingPosted(t, posted, "the same list is not a change") if err := r.SetMedia(true, false); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); !m.Muted || len(m.NoSoundFrom) != 2 { t.Fatalf("muting kept the list? %+v", m) } if err := r.SetNoSoundFrom(nil); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.NoSoundFrom) != 0 || !m.Muted { t.Fatalf("cleared %+v", m) } } // §15.21: whose screen has stalled here is reported like noSoundFrom — // roster peers only, in roster order, a repeat is not a change — and it // travels beside the rest of the report. func TestScreenStalledIsReportedFromTheRoster(t *testing.T) { r, posted := mediaRoom(t) a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb") if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 { t.Fatalf("admission report %+v, want no stalled screens", m) } if err := r.SetScreenStalled([]protocol.PeerID{b, "self", "stranger"}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || m.ScreenStalled[0] != b { t.Fatalf("screenStalled %v, want [bbbb]", m.ScreenStalled) } if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil { t.Fatal(err) } nothingPosted(t, posted, "the same list is not a change") if err := r.SetNoSoundFrom([]protocol.PeerID{a}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || len(m.NoSoundFrom) != 1 { t.Fatalf("both lists travel together: %+v", m) } if err := r.SetScreenStalled(nil); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || len(m.NoSoundFrom) != 1 { t.Fatalf("cleared %+v", m) } } // A server from before §15.21 refuses a report naming stalled screens // with 400. The core sends the same report again without them, and every // report after that goes without them too, so mute and camera state are // never lost to an old server. func TestScreenStalledFallsBackOnAnOldServer(t *testing.T) { posted := make(chan protocol.Signal, 16) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var s protocol.Signal _ = json.NewDecoder(req.Body).Decode(&s) var m protocol.MediaState _ = json.Unmarshal(s.Payload, &m) posted <- s // A server from before §15.21 knows neither key (nor §15.24's). if len(m.ScreenStalled) > 0 || m.Hearing != nil { w.WriteHeader(http.StatusBadRequest) } })) t.Cleanup(srv.Close) r, err := New(srv.URL, "room", srv.Client(), func(Event) {}) if err != nil { t.Fatal(err) } r.started = true r.state.Peer = "self" t.Cleanup(r.Close) b := protocol.PeerID("bbbb") if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } nextMedia(t, posted) // with hearing, refused nextMedia(t, posted) // without it if err := r.SetMedia(true, false); err != nil { t.Fatal(err) } nextMedia(t, posted) if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 { t.Fatalf("first report %+v, want the list", m) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || !m.Muted { t.Fatalf("resent %+v, want the same report without the list", m) } if err := r.SetMedia(false, false); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || m.Muted { t.Fatalf("later report %+v, want it plain", m) } } // §15.24: this core always says who it hears — [] until it hears anyone, // so it is never mistaken for a client that does not say — and the list // is kept like noSoundFrom's: roster peers only, in roster order, a // repeat not a change. func TestHearingIsAlwaysReportedFromTheRoster(t *testing.T) { r, posted := mediaRoom(t) a, b := protocol.PeerID("aaaa"), protocol.PeerID("bbbb") if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: a, Gen: 1}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } select { case s := <-posted: if !strings.Contains(string(s.Payload), `"hearing":[]`) { t.Fatalf("admission report %s, want hearing: [] on the wire", s.Payload) } case <-time.After(time.Second): t.Fatal("no admission report") } if err := r.SetHearing([]protocol.PeerID{b, "self", "stranger", a}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.Hearing) != 2 || m.Hearing[0] != a || m.Hearing[1] != b { t.Fatalf("hearing %v, want [aaaa bbbb] in roster order", m.Hearing) } if err := r.SetHearing([]protocol.PeerID{a, b}); err != nil { t.Fatal(err) } nothingPosted(t, posted, "the same list is not a change") if err := r.SetHearing(nil); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); m.Hearing == nil || len(m.Hearing) != 0 { t.Fatalf("cleared %+v, want hearing: [] and not absent", m) } // A roster that carries the same [] needs nothing; one that lost the // key (a restarted hub) is told again. if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Hearing: []protocol.PeerID{}}}, protocol.RosterPeer{Peer: a, Gen: 1})); err != nil { t.Fatal(err) } nothingPosted(t, posted, "a roster that agrees needs no report") if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{}}, protocol.RosterPeer{Peer: a, Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); m.Hearing == nil { t.Fatalf("a roster without our hearing must be corrected, got %+v", m) } } // A server from before §15.24 refuses every report from this core, which // always carries hearing. The core sends the same report again without // it and leaves it out from then on, so a mute is never lost; a server // from before §15.21 as well refuses the stalled screens next, and they // go too. func TestHearingFallsBackOnAnOldServer(t *testing.T) { posted := make(chan protocol.Signal, 16) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var s protocol.Signal _ = json.NewDecoder(req.Body).Decode(&s) var m protocol.MediaState _ = json.Unmarshal(s.Payload, &m) posted <- s if m.Hearing != nil || len(m.ScreenStalled) > 0 { w.WriteHeader(http.StatusBadRequest) } })) t.Cleanup(srv.Close) r, err := New(srv.URL, "room", srv.Client(), func(Event) {}) if err != nil { t.Fatal(err) } r.started = true r.state.Peer = "self" t.Cleanup(r.Close) b := protocol.PeerID("bbbb") _ = r.SetMedia(true, false) if err := r.receive(roster(protocol.RosterPeer{Peer: "self"}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); m.Hearing == nil { t.Fatalf("first report %+v, want hearing", m) } if m := nextMedia(t, posted); m.Hearing != nil || !m.Muted { t.Fatalf("resent %+v, want the same report without hearing", m) } if err := r.SetScreenStalled([]protocol.PeerID{b}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 1 || m.Hearing != nil { t.Fatalf("report %+v, want stalled screens and no hearing", m) } if m := nextMedia(t, posted); len(m.ScreenStalled) != 0 || m.Hearing != nil || !m.Muted { t.Fatalf("resent %+v, want it plain and still muted", m) } if err := r.SetHearing([]protocol.PeerID{b}); err != nil { t.Fatal(err) } if m := nextMedia(t, posted); m.Hearing != nil { t.Fatalf("later report %+v, want no hearing", m) } // The roster agrees with what this server can hold: nothing to send. if err := r.receive(roster(protocol.RosterPeer{Peer: "self", Media: &protocol.MediaState{Muted: true}}, protocol.RosterPeer{Peer: b, Gen: 1})); err != nil { t.Fatal(err) } nothingPosted(t, posted, "a roster that agrees with the plain report needs nothing") }