oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package hub
2
3import (
4 "context"
5 "io"
6 "log/slog"
7 "testing"
8 "time"
9)
10
11// testCtx is the context the four request-path methods now take, so a
12// member's standing can be re-derived on every request (spec §15.12).
13// Nothing in these tests cancels it: the standing they exercise comes
14// from testHub's StandingFor, not from the context.
15var testCtx = context.Background()
16
17func testHub(now *time.Time) *Hub {
18 return New(Config{
19 Now: func() time.Time { return *now },
20 Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
21 StandingFor: func(ctx context.Context, room string, personID int64) (Kind, bool, error) {
22 if personID < 100 {
23 return KindMember, true, nil
24 }
25 return KindGuest, true, nil
26 },
27 })
28}
29
30func TestOpenIssuesADistinctPeerBoundToTheSession(t *testing.T) {
31 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
32 h := testHub(&now)
33 a, err := h.Open("r1", "sessA", 1, "Ann", KindMember)
34 if err != nil {
35 t.Fatal(err)
36 }
37 b, _ := h.Open("r1", "sessB", 2, "Bob", KindMember)
38 if a.Peer == b.Peer || len(a.Peer) != 32 {
39 t.Fatalf("peer ids %q %q", a.Peer, b.Peer)
40 }
41 if _, ok := h.Find("r1", "sessB", a.Peer); ok {
42 t.Fatal("a peer was found through another session")
43 }
44 if _, ok := h.Find("r2", "sessA", a.Peer); ok {
45 t.Fatal("a peer was found through another room")
46 }
47 if p, ok := h.Find("r1", "sessA", a.Peer); !ok || p.Admitted {
48 t.Fatal("fresh participation should exist and be unjoined")
49 }
50}
51
52func TestOneLivePeerPerSessionPerRoom(t *testing.T) {
53 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
54 h := testHub(&now)
55 a, _ := h.Open("r1", "sessA", 1, "Ann", KindMember)
56 _, err := h.Open("r1", "sessA", 1, "Ann", KindMember)
57 if err != ErrSessionHasPeer {
58 t.Fatalf("second stream: %v", err)
59 }
60 h.Detach("r1", a.Peer, nil)
61 if _, err := h.Open("r1", "sessA", 1, "Ann", KindMember); err != ErrSessionHasPeer {
62 t.Fatal("a detached peer inside its resume window should still block a fresh stream")
63 }
64 now = now.Add(31 * time.Second)
65 h.Tick(context.Background())
66 if _, err := h.Open("r1", "sessA", 1, "Ann", KindMember); err != nil {
67 t.Fatalf("after the window: %v", err)
68 }
69}
70
71func TestSixteenParticipationsPerRoomThenRefuse(t *testing.T) {
72 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
73 h := testHub(&now)
74 for i := 0; i < 16; i++ {
75 if _, err := h.Open("r1", "s"+string(rune('a'+i)), int64(100+i), "g", KindGuest); err != nil {
76 t.Fatalf("open %d: %v", i, err)
77 }
78 }
79 if _, err := h.Open("r1", "sZ", 200, "g", KindGuest); err != ErrRoomFull {
80 t.Fatalf("seventeenth: %v", err)
81 }
82}
83
84func TestUnjoinedParticipationDropsAfterThirtySeconds(t *testing.T) {
85 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
86 h := testHub(&now)
87 a, _ := h.Open("r1", "sessA", 1, "Ann", KindMember)
88 now = now.Add(31 * time.Second)
89 h.Tick(context.Background())
90 if _, ok := h.Find("r1", "sessA", a.Peer); ok {
91 t.Fatal("unjoined participation survived the window")
92 }
93 if !a.Sub.Lost() {
94 t.Fatal("its subscriber was not closed")
95 }
96}
97
98func TestFourStreamsPerPerson(t *testing.T) {
99 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
100 h := testHub(&now)
101 for i := 0; i < 4; i++ {
102 if _, err := h.Open("r"+string(rune('1'+i)), "s"+string(rune('1'+i)), 1, "Ann", KindMember); err != nil {
103 t.Fatal(err)
104 }
105 }
106 if _, err := h.Open("r9", "s9", 1, "Ann", KindMember); err != ErrPersonStreams {
107 t.Fatalf("fifth stream: %v", err)
108 }
109}
110
111func TestSlowSubscriberIsDeclaredLost(t *testing.T) {
112 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
113 h := testHub(&now)
114 a, _ := h.Open("r1", "sessA", 1, "Ann", KindMember)
115 for i := 0; i < 70; i++ {
116 h.deliver("r1", a.Peer, Message{Type: "peer"})
117 }
118 if !a.Sub.Lost() {
119 t.Fatal("65th undelivered event did not declare the subscriber lost")
120 }
121 var last Message
122 for m := range a.Sub.C() {
123 last = m
124 }
125 if last.Type != "lost" || last.Reason != "buffer" {
126 t.Fatalf("last event %+v, want lost/buffer", last)
127 }
128}
129
130func TestTerminateDoesNotEvictWhenNotFull(t *testing.T) {
131 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
132 h := testHub(&now)
133 a, _ := h.Open("r1", "sessA", 1, "Ann", KindMember)
134 h.deliver("r1", a.Peer, Message{Type: TypeGone, From: "someone"})
135 a.Sub.terminate(Message{Type: TypeLost, Reason: ReasonRemoved})
136
137 var got []Message
138 for m := range a.Sub.C() {
139 got = append(got, m)
140 }
141 if len(got) != 2 {
142 t.Fatalf("got %d events %+v, want the queued one and the lost", len(got), got)
143 }
144 if got[0].Type != TypeGone || got[0].From != "someone" {
145 t.Fatalf("first event %+v, want the queued gone", got[0])
146 }
147 if got[1].Type != TypeLost || got[1].Reason != ReasonRemoved {
148 t.Fatalf("second event %+v, want lost/removed", got[1])
149 }
150}
151
152// A second stream that resumes an attached participation displaces the
153// first (lost{replaced}, ruling above). The displaced handler then does
154// what every stream handler does on its way out — Detach — and that
155// must not take the replacement's attachment down with it.
156func TestDetachOfADisplacedStreamLeavesTheReplacementAttached(t *testing.T) {
157 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
158 h := testHub(&now)
159 a, _ := h.Open("r1", "sa", 1, "Ann", KindMember)
160 res, err := h.Resume(testCtx, "r1", "sa", a.Peer, nil)
161 if err != nil {
162 t.Fatal(err)
163 }
164 if ev := drain(a.Sub); len(ev) != 1 || ev[0].Reason != ReasonReplaced {
165 t.Fatalf("displaced stream got %+v", ev)
166 }
167 h.Detach("r1", a.Peer, a.Sub) // the displaced handler, on its way out
168 if res.Sub.Lost() {
169 t.Fatal("the replacement stream was closed by the displaced one's Detach")
170 }
171 p, ok := h.Find("r1", "sa", a.Peer)
172 if !ok || p.sub != res.Sub || !p.resumeUntil.IsZero() {
173 t.Fatalf("participation lost its attachment: %+v", p)
174 }
175 // The replacement's own Detach still detaches.
176 h.Detach("r1", a.Peer, res.Sub)
177 if !res.Sub.Lost() || !p.resumeUntil.Equal(now.Add(ResumeWindow)) {
178 t.Fatalf("the attached stream's Detach did nothing: %+v", p)
179 }
180}
181
182// PeerNameFor is Remove's own authority, read rather than acted on: it
183// must answer a name in exactly the cases Remove would go on to act,
184// and false everywhere else, so a confirm page can never name — or
185// offer to remove — a peer the caller could not actually remove.
186func TestPeerNameForIsExactlyRemovesAuthority(t *testing.T) {
187 now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC)
188 h := testHub(&now)
189 ctx := context.Background()
190
191 member, _ := h.Open("r1", "sMember", 1, "Ann", KindMember)
192 if err := h.Join(ctx, "r1", "sMember", member.Peer); err != nil {
193 t.Fatal(err)
194 }
195 target, _ := h.Open("r1", "sTarget", 2, "Bob", KindMember)
196
197 if name, ok := h.PeerNameFor("r1", "sMember", target.Peer); !ok || name != "Bob" {
198 t.Fatalf("member actor + present target: %q, %v", name, ok)
199 }
200
201 guest, _ := h.Open("r1", "sGuest", 100, "Cid", KindGuest)
202 if err := h.Join(ctx, "r1", "sGuest", guest.Peer); err != nil {
203 t.Fatal(err)
204 }
205 if p, ok := h.Find("r1", "sGuest", guest.Peer); !ok || p.Admitted {
206 t.Fatal("the guest should be waiting, not admitted, for this case to test anything")
207 }
208 if _, ok := h.PeerNameFor("r1", "sGuest", target.Peer); ok {
209 t.Fatal("a waiting guest is not admitted and must not name anyone")
210 }
211
212 other, _ := h.Open("r2", "sOther", 3, "Deb", KindMember)
213 if err := h.Join(ctx, "r2", "sOther", other.Peer); err != nil {
214 t.Fatal(err)
215 }
216 if _, ok := h.PeerNameFor("r1", "sMember", other.Peer); ok {
217 t.Fatal("a target in another room must not be named")
218 }
219
220 if _, ok := h.PeerNameFor("r1", "sMember", member.Peer); ok {
221 t.Fatal("the actor is not its own target")
222 }
223
224 if _, ok := h.PeerNameFor("r1", "sNobody", target.Peer); ok {
225 t.Fatal("a session with no participation in this room must not name anyone")
226 }
227}
228