package callstest import ( "encoding/json" "strings" "testing" "time" ) // Guest isolation over HTTP — §13's slice-2 gates, end to end. // // The hub half lives in internal/calls/hub/isolation_test.go. These are // the two §13 names it in so many words: // // "roster carrying the waiting queue to members and not to guests" // "a waiting guest receiving nothing but hello and its own outcome" // // plus the knock audience, which is the same rule and the one slice 4's // chat fan-out will extend. Whole-branch review W-1 found all three // ungated: the filters could be deleted and every test stayed green. // // What makes these discriminating is a second guest left waiting. With // only one guest in the room the waiting queue is empty and no // admission happens while anyone is waiting, so nothing can leak and // nothing is proved. // guestRoom signs a member in, opens a stream for it, and leaves two // guests knocking on the same link: `waiting` stays waiting for the // whole test, and `newcomer` is whoever the test is about to admit. // Returns the harness, the room, the member's browser and its peer, // and the two guests' readers and peers. type isolationRoom struct { x *harness sqid string token string member *client memberSSE *sseReader memberID string waiting *sseReader waitingID string guest *sseReader guestID string } func openIsolationRoom(t *testing.T) *isolationRoom { t.Helper() x, sqid, tok := memberWithMeeting(t) enableWaitingRoom(t, x, sqid) m := x.browser(t) m.signIn("paul@example.com") sm := m.stream("/m/" + sqid + "/stream") t.Cleanup(sm.close) pm, _ := sm.hello() m.signal(sqid, pm, `{"type":"join"}`) if ev, _, ok := sm.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the member never joined the mesh (got %q)", ev) } // The guest that stays waiting for the whole test. It knocks first, // so it is in the queue before anything below happens. w := x.browser(t) w.get("/j/" + tok) w.signIn("waiter@example.com") sw := w.stream("/m/" + sqid + "/stream") t.Cleanup(sw.close) pw, _ := sw.hello() w.signal(sqid, pw, `{"type":"join"}`) if ev, _, ok := sm.nextOf("knock", 2*time.Second); !ok { t.Fatalf("the first guest never reached the queue (got %q)", ev) } g := x.browser(t) g.get("/j/" + tok) g.signIn("guest@example.com") sg := g.stream("/m/" + sqid + "/stream") t.Cleanup(sg.close) pg, _ := sg.hello() g.signal(sqid, pg, `{"type":"join"}`) if ev, _, ok := sm.nextOf("knock", 2*time.Second); !ok { t.Fatalf("the second guest never reached the queue (got %q)", ev) } return &isolationRoom{x: x, sqid: sqid, token: tok, member: m, memberSSE: sm, memberID: pm, waiting: sw, waitingID: pw, guest: sg, guestID: pg} } // silence asserts a reader carries no event at all for a short window. // The heartbeat is an SSE comment and never reaches this channel, so an // idle stream really is silent here. func silence(t *testing.T, s *sseReader, who string) { t.Helper() if ev, data, ok := s.next(400 * time.Millisecond); ok { t.Fatalf("%s received %q: %s", who, ev, data) } } // rosterBody is the roster event as it goes over the wire: the roster // rides inside the message envelope, so a test that unmarshals the // event data straight into a peers/waiting struct gets two empty slices // and proves nothing. type rosterBody struct { Peers []struct{ Peer, Name, Kind string } `json:"peers"` Waiting []struct{ Peer, Name, Kind string } `json:"waiting"` } func parseRoster(t *testing.T, data []byte) rosterBody { t.Helper() var msg struct { Type string `json:"type"` Roster rosterBody `json:"roster"` } if err := json.Unmarshal(data, &msg); err != nil { t.Fatalf("roster %s: %v", data, err) } if msg.Type != "roster" { t.Fatalf("event is %q, want roster: %s", msg.Type, data) } if len(msg.Roster.Peers) == 0 { t.Fatalf("roster carries no peers at all, which no admitted peer ever sees: %s", data) } return msg.Roster } // §13: "roster carrying the waiting queue to members and not to // guests" (W-1 row 1, and rows 2 and 4 as they appear on the wire). // The guest is admitted while another guest is still knocking, so the // waiting queue has a name in it to leak. func TestTheWaitingQueueReachesMembersAndNeverAGuest(t *testing.T) { r := openIsolationRoom(t) r.member.signal(r.sqid, r.memberID, `{"type":"admit","payload":{"peer":"`+r.guestID+`"}}`) ev, data, ok := r.guest.nextOf("roster", 2*time.Second) if !ok { t.Fatalf("the guest was never admitted (got %q)", ev) } roster := parseRoster(t, data) if len(roster.Waiting) != 0 { t.Fatalf("an admitted guest was handed the waiting queue: %s", data) } // The queue is not merely absent from the parsed struct: the name of // the person knocking is nowhere in the bytes. if strings.Contains(string(data), r.waitingID) || strings.Contains(string(data), "waiter") { t.Fatalf("the waiting guest appears in an admitted guest's roster: %s", data) } // And roster.peers is the mesh, not everyone joined: the guest still // knocking is not in it either (W-1 row 2 over the wire). for _, p := range roster.Peers { if p.Peer == r.waitingID { t.Fatalf("roster.peers listed a peer that has only knocked: %s", data) } } if len(roster.Peers) != 2 { t.Fatalf("roster.peers has %d entries, want the member and the guest: %s", len(roster.Peers), data) } // The member half of the same sentence, so the test says what the // rule is for and cannot pass by the queue being empty: a second // member joining is sent the queue the guest was not. m2 := newMemberBrowser(t, r.x, "paul@rslw.com") s2 := m2.stream("/m/" + r.sqid + "/stream") defer s2.close() p2, _ := s2.hello() m2.signal(r.sqid, p2, `{"type":"join"}`) ev, data, ok = s2.nextOf("roster", 2*time.Second) if !ok { t.Fatalf("the second member never joined (got %q)", ev) } roster = parseRoster(t, data) if len(roster.Waiting) != 1 || roster.Waiting[0].Peer != r.waitingID { t.Fatalf("a member's roster must carry the queue, got %s", data) } } // §13: "a waiting guest receiving nothing but hello and its own // outcome" (W-1 row 3 over the wire). Its own knock is delivered to the // members; everything that happens next in the room — another guest // admitted, a member arriving — is addressed to the mesh, and the // waiting subscriber is not in it. func TestAWaitingGuestIsSentNothingButHelloAndItsOwnOutcome(t *testing.T) { r := openIsolationRoom(t) silence(t, r.waiting, "a guest that has only knocked") // Another guest is let in. r.member.signal(r.sqid, r.memberID, `{"type":"admit","payload":{"peer":"`+r.guestID+`"}}`) if ev, _, ok := r.guest.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the other guest was never admitted (got %q)", ev) } silence(t, r.waiting, "a waiting guest, after someone else was admitted") // A second member arrives, which is an admission too. m2 := newMemberBrowser(t, r.x, "paul@rslw.com") s2 := m2.stream("/m/" + r.sqid + "/stream") defer s2.close() p2, _ := s2.hello() m2.signal(r.sqid, p2, `{"type":"join"}`) if ev, _, ok := s2.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the second member never joined (got %q)", ev) } // The mesh was told, so the silence above is an audience and not a // server that stopped sending. if ev, _, ok := r.guest.nextOf("peer", 2*time.Second); !ok { t.Fatalf("the admitted guest was not told the member arrived (got %q)", ev) } silence(t, r.waiting, "a waiting guest, after a member joined") // Its own outcome still reaches it, which is the other half of the // §13 sentence. r.member.signal(r.sqid, r.memberID, `{"type":"admit","payload":{"peer":"`+r.waitingID+`"}}`) if ev, _, ok := r.waiting.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the waiting guest was never told its own outcome (got %q)", ev) } } // W-1 row 5 over the wire: a knock carries the display name of the // person asking to come in, so it goes to admitted members and to // nobody else. Slice 4's chat fan-out is the same audience question. func TestAKnockReachesAdmittedMembersAndNoGuest(t *testing.T) { r := openIsolationRoom(t) r.member.signal(r.sqid, r.memberID, `{"type":"admit","payload":{"peer":"`+r.guestID+`"}}`) if ev, _, ok := r.guest.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the guest was never admitted (got %q)", ev) } // A third outsider knocks while an admitted guest is in the mesh. third := r.x.browser(t) third.get("/j/" + r.token) third.signIn("newcomer@example.com") s3 := third.stream("/m/" + r.sqid + "/stream") defer s3.close() p3, _ := s3.hello() third.signal(r.sqid, p3, `{"type":"join"}`) ev, data, ok := r.memberSSE.nextOf("knock", 2*time.Second) if !ok { t.Fatalf("the member did not get the knock (got %q)", ev) } if !strings.Contains(string(data), p3) { t.Fatalf("the knock names someone else: %s", data) } silence(t, r.guest, "an admitted guest, while someone knocks") silence(t, r.waiting, "a waiting guest, while someone else knocks") }