package hub import ( "context" "testing" "time" ) // Guest isolation (spec §4.3, ruling 9, and two of §13's slice-2 gates). // // Five predicates in admission.go carry the rule that a guest learns // nothing about anyone it is not already in the mesh with, and that a // peer which has only knocked learns nothing at all. Whole-branch // review W-1 mutated each of the five away and found the whole suite // still green, so the tests below exist to kill exactly those five // mutations. Each one names the mutation it is written against: a test // whose subject is an audience filter is otherwise indistinguishable // from a test of the happy path, and the happy path is what the // pre-existing tests already covered. // // The shape that makes them discriminating is a room with someone still // waiting in it. Every one of these leaks is invisible when the guest // under test is the only guest in the room. // guestRoom builds that shape: one admitted member, one guest admitted // after it, and one guest still waiting, with every subscriber drained // so a test asserts only on what happens next. func guestRoom(t *testing.T, h *Hub) (member, admittedGuest, waitingGuest OpenResult) { t.Helper() ctx := context.Background() member, _ = h.Open("r1", "sm", 1, "Mia", KindMember) if err := h.Join(ctx, "r1", "sm", member.Peer); err != nil { t.Fatalf("member join: %v", err) } waitingGuest, _ = h.Open("r1", "sw", 101, "Wes", KindGuest) if err := h.Join(ctx, "r1", "sw", waitingGuest.Peer); err != nil { t.Fatalf("waiting guest join: %v", err) } admittedGuest, _ = h.Open("r1", "sg", 100, "Gus", KindGuest) if err := h.Join(ctx, "r1", "sg", admittedGuest.Peer); err != nil { t.Fatalf("guest join: %v", err) } if err := h.Admit(ctx, "r1", "sm", member.Peer, admittedGuest.Peer); err != nil { t.Fatalf("admit: %v", err) } drain(member.Sub) drain(admittedGuest.Sub) drain(waitingGuest.Sub) return member, admittedGuest, waitingGuest } // countOf reports how many of the drained events carry one type. func countOf(ms []Message, typ string) int { n := 0 for _, m := range ms { if m.Type == typ { n++ } } return n } // W-1 row 1 — mutation: rosterFor's `viewer.Kind == KindMember` → true. // // §13: "roster carrying the waiting queue to members and not to // guests". The roster an admitted guest is sent must never carry the // waiting queue, which is the display name of everyone knocking. The // guest is admitted here while another guest is already waiting, so // the Waiting field has something to leak — the pre-existing test // admitted the only guest in the room, where an empty queue proved // nothing about the filter. func TestAnAdmittedGuestIsNeverToldWhoIsWaiting(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) h := testHub(&now) ctx := context.Background() m, _ := h.Open("r1", "sm", 1, "Mia", KindMember) h.Join(ctx, "r1", "sm", m.Peer) w, _ := h.Open("r1", "sw", 101, "Wes", KindGuest) h.Join(ctx, "r1", "sw", w.Peer) g, _ := h.Open("r1", "sg", 100, "Gus", KindGuest) h.Join(ctx, "r1", "sg", g.Peer) drain(g.Sub) if err := h.Admit(ctx, "r1", "sm", m.Peer, g.Peer); err != nil { t.Fatal(err) } ev := drain(g.Sub) if len(ev) != 1 || ev[0].Type != TypeRoster { t.Fatalf("the admitted guest got %v, want one roster", types(ev)) } if n := len(ev[0].Roster.Waiting); n != 0 { t.Fatalf("the admitted guest was handed %d waiting peer(s): %+v", n, ev[0].Roster.Waiting) } // The same moment from a member's side, so the test also says what // the rule is for: the queue exists, and a member does see it. m2, _ := h.Open("r1", "sm2", 2, "Moe", KindMember) h.Join(ctx, "r1", "sm2", m2.Peer) mev := drain(m2.Sub) if len(mev) != 1 || mev[0].Type != TypeRoster { t.Fatalf("the joining member got %v, want one roster", types(mev)) } if len(mev[0].Roster.Waiting) != 1 || mev[0].Roster.Waiting[0].Peer != w.Peer { t.Fatalf("a member's roster must carry the queue, got %+v", mev[0].Roster.Waiting) } } // W-1 row 2 — mutation: rosterFor's `!p.Admitted` → `!p.Joined`. // // roster.peers is the mesh: everyone the viewer should be negotiating a // connection with. A peer that has only knocked is not in it, for // anyone — a member sees it in Waiting and nowhere else, and a guest // does not see it at all. func TestTheRosterMeshListsAdmittedPeersOnly(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) h := testHub(&now) ctx := context.Background() m, g, w := guestRoom(t, h) // A member joining now is sent the authoritative snapshot. m2, _ := h.Open("r1", "sm2", 2, "Moe", KindMember) if err := h.Join(ctx, "r1", "sm2", m2.Peer); err != nil { t.Fatal(err) } mev := drain(m2.Sub) if len(mev) != 1 || mev[0].Type != TypeRoster { t.Fatalf("joining member got %v", types(mev)) } assertMeshExcludesWaiting(t, "the joining member", mev[0].Roster, w.Peer, m.Peer, g.Peer, m2.Peer) // And the same snapshot as an admitted guest sees it, which is the // half with no Waiting field to carry the knock instead. g2, _ := h.Open("r1", "sg2", 102, "Gil", KindGuest) h.Join(ctx, "r1", "sg2", g2.Peer) drain(g2.Sub) if err := h.Admit(ctx, "r1", "sm", m.Peer, g2.Peer); err != nil { t.Fatal(err) } gev := drain(g2.Sub) if len(gev) != 1 || gev[0].Type != TypeRoster { t.Fatalf("admitted guest got %v", types(gev)) } assertMeshExcludesWaiting(t, "the admitted guest", gev[0].Roster, w.Peer, m.Peer, g.Peer, m2.Peer, g2.Peer) } func assertMeshExcludesWaiting(t *testing.T, who string, ro *Roster, waiting PeerID, want ...PeerID) { t.Helper() got := map[PeerID]bool{} for _, p := range ro.Peers { got[p.Peer] = true } if got[waiting] { t.Fatalf("%s: roster.peers listed a peer that has only knocked (%s): %+v", who, waiting, ro.Peers) } if len(got) != len(want) { t.Fatalf("%s: roster.peers has %d entries, want %d: %+v", who, len(got), len(want), ro.Peers) } for _, id := range want { if !got[id] { t.Fatalf("%s: roster.peers is missing %s: %+v", who, id, ro.Peers) } } } // W-1 row 3 — mutation: admitLocked's `q.Admitted` dropped from the // fan-out condition. // // §13: "a waiting guest receiving nothing but hello and its own // outcome". Every admission after the knock — another guest let in, a // member arriving — must leave the waiting subscriber silent, because // a `peer` event is an instruction to open a connection to someone the // guest has no business reaching. func TestAWaitingGuestReceivesNoPeerEventForAnyoneElsesAdmission(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) h := testHub(&now) ctx := context.Background() m, _, w := guestRoom(t, h) // Another guest is let in while this one is still waiting. g2, _ := h.Open("r1", "sg2", 102, "Gil", KindGuest) h.Join(ctx, "r1", "sg2", g2.Peer) if err := h.Admit(ctx, "r1", "sm", m.Peer, g2.Peer); err != nil { t.Fatal(err) } if ev := drain(w.Sub); len(ev) != 0 { t.Fatalf("the waiting guest was told about another admission: %v (%+v)", types(ev), ev) } // And a member arrives, which is an admission too. m2, _ := h.Open("r1", "sm2", 2, "Moe", KindMember) if err := h.Join(ctx, "r1", "sm2", m2.Peer); err != nil { t.Fatal(err) } if ev := drain(w.Sub); len(ev) != 0 { t.Fatalf("the waiting guest was told a member joined: %v (%+v)", types(ev), ev) } // The rule is an audience, not silence: the peer already in the mesh // was told both times. if n := countOf(drain(m.Sub), TypePeer); n != 2 { t.Fatalf("the member in the mesh got %d peer events, want 2", n) } } // W-1 row 4 — mutation: refreshRostersLocked's `q.Admitted` dropped. // // A kind change owes every *admitted* peer a fresh roster (§2.4). A // waiting guest is owed nothing, and the roster it would be handed is // the whole mesh — plus, when the changed peer is a member, the queue. func TestAKindChangeNeverHandsAWaitingGuestTheRoster(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) kinds := map[int64]Kind{1: KindMember, 100: KindGuest, 101: KindGuest} h := New(Config{ Now: func() time.Time { return now }, StandingFor: func(ctx context.Context, room string, personID int64) (Kind, bool, error) { return kinds[personID], true, nil }, }) ctx := context.Background() m, g, w := guestRoom(t, h) // The admitted guest is added to the team mid-call: the tick // re-derives its kind, and the change refreshes the rosters. kinds[100] = KindMember h.Tick(ctx) if ev := drain(w.Sub); len(ev) != 0 { t.Fatalf("a kind change reached the waiting guest: %v (%+v)", types(ev), ev) } // The peers that were owed the refresh got it, so the test is not // passing because nothing happened at all. for _, c := range []struct { who string r OpenResult }{{"the member", m}, {"the promoted guest", g}} { ev := drain(c.r.Sub) if len(ev) != 1 || ev[0].Type != TypeRoster { t.Fatalf("%s got %v, want the refreshed roster", c.who, types(ev)) } } } // W-1 row 5 — mutation: knock's audience `admittedMembers` → // `admittedPeers`. // // A knock carries the display name of the person asking to come in. // Only a member can act on it, and only a member may learn it: an // admitted guest is in the mesh, not on the door. knock-gone has the // same audience for the same reason. func TestAKnockReachesAdmittedMembersOnly(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) h := testHub(&now) ctx := context.Background() m, g, w := guestRoom(t, h) newcomer, _ := h.Open("r1", "sn", 103, "Nan", KindGuest) if err := h.Join(ctx, "r1", "sn", newcomer.Peer); err != nil { t.Fatal(err) } mev := drain(m.Sub) if len(mev) != 1 || mev[0].Type != TypeKnock || mev[0].From != newcomer.Peer || mev[0].Name != "Nan" { t.Fatalf("the member did not get the knock: %v (%+v)", types(mev), mev) } if ev := drain(g.Sub); len(ev) != 0 { t.Fatalf("an admitted guest learned who is knocking: %v (%+v)", types(ev), ev) } if ev := drain(w.Sub); len(ev) != 0 { t.Fatalf("a waiting guest learned who is knocking: %v (%+v)", types(ev), ev) } if err := h.Admit(ctx, "r1", "sm", m.Peer, newcomer.Peer); err != nil { t.Fatal(err) } got := types(drain(m.Sub)) if len(got) != 2 || got[0] != TypePeer || got[1] != TypeKnockGone { t.Fatalf("the member got %v, want peer then knock-gone", got) } if ev := types(drain(g.Sub)); len(ev) != 1 || ev[0] != TypePeer { t.Fatalf("the admitted guest got %v, want the peer event and no knock-gone", ev) } if ev := drain(w.Sub); len(ev) != 0 { t.Fatalf("the waiting guest got %v", types(ev)) } } // Review W-5: `Remove`'s refusal to take its own actor as a target. The // guard survived being deleted, and what it stops is worth a test — a // member removing itself would end its own participation through the // removal path, which announces `gone` and writes no tombstone, so the // tab would find itself out of the room with no way to resume and no // reason given. Leaving is `leave`, and it is a different ending. func TestAMemberCannotRemoveItself(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) h := testHub(&now) ctx := context.Background() m, g, _ := guestRoom(t, h) if err := h.Remove(ctx, "r1", "sm", m.Peer, m.Peer); err != ErrUnknownPeer { t.Fatalf("a member removing itself: %v, want ErrUnknownPeer", err) } if p, ok := h.Find("r1", "sm", m.Peer); !ok || !p.Admitted { t.Fatal("the member removed itself") } if ev := drain(m.Sub); len(ev) != 0 { t.Fatalf("the room was told something: %v", types(ev)) } // The refusal is the same sentinel an unknown target gets, so it // tells the caller nothing about itself either. if err := h.Remove(ctx, "r1", "sm", m.Peer, PeerID("0123456789abcdef0123456789abcdef")); err != ErrUnknownPeer { t.Fatalf("an unknown target: %v", err) } // And a real other peer is still removable, so the guard is not // simply refusing everything. if err := h.Remove(ctx, "r1", "sm", m.Peer, g.Peer); err != nil { t.Fatalf("removing someone else: %v", err) } }