Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at main 264 lines 7.1 kB view raw
1package collaboration 2 3import ( 4 "encoding/json" 5 "testing" 6 "time" 7) 8 9// stubClient creates a Client with a real send channel but no WebSocket conn. 10// It is usable for hub/room tests that don't exercise the network layer. 11func stubClient(hub *Hub, did, name, color, roomKey string) *Client { 12 return &Client{ 13 hub: hub, 14 conn: nil, // not used in hub logic 15 send: make(chan []byte, 256), 16 DID: did, 17 Name: name, 18 Color: color, 19 roomKey: roomKey, 20 } 21} 22 23// drain reads all messages from a client's send channel within timeout. 24func drain(c *Client, timeout time.Duration) [][]byte { 25 deadline := time.After(timeout) 26 var msgs [][]byte 27 for { 28 select { 29 case msg, ok := <-c.send: 30 if !ok { 31 return msgs 32 } 33 msgs = append(msgs, msg) 34 case <-deadline: 35 return msgs 36 } 37 } 38} 39 40// waitForMessages blocks until n messages arrive on c.send or timeout. 41func waitForMessages(c *Client, n int, timeout time.Duration) [][]byte { 42 deadline := time.After(timeout) 43 var msgs [][]byte 44 for len(msgs) < n { 45 select { 46 case msg, ok := <-c.send: 47 if !ok { 48 return msgs 49 } 50 msgs = append(msgs, msg) 51 case <-deadline: 52 return msgs 53 } 54 } 55 return msgs 56} 57 58// --- Hub tests --- 59 60func TestHub_GetOrCreateRoom_CreatesNew(t *testing.T) { 61 hub := NewHub() 62 room := hub.GetOrCreateRoom("doc1") 63 if room == nil { 64 t.Fatal("expected non-nil room") 65 } 66} 67 68func TestHub_GetOrCreateRoom_ReturnsSame(t *testing.T) { 69 hub := NewHub() 70 r1 := hub.GetOrCreateRoom("doc1") 71 r2 := hub.GetOrCreateRoom("doc1") 72 if r1 != r2 { 73 t.Error("expected same room instance for same rkey") 74 } 75} 76 77func TestHub_GetOrCreateRoom_DifferentRooms(t *testing.T) { 78 hub := NewHub() 79 r1 := hub.GetOrCreateRoom("doc1") 80 r2 := hub.GetOrCreateRoom("doc2") 81 if r1 == r2 { 82 t.Error("expected different rooms for different rkeys") 83 } 84} 85 86func TestHub_GetRoom_NilForUnknown(t *testing.T) { 87 hub := NewHub() 88 if hub.GetRoom("nonexistent") != nil { 89 t.Error("expected nil for unknown room") 90 } 91} 92 93func TestHub_GetRoom_AfterCreate(t *testing.T) { 94 hub := NewHub() 95 hub.GetOrCreateRoom("doc1") 96 if hub.GetRoom("doc1") == nil { 97 t.Error("expected room to be retrievable after creation") 98 } 99} 100 101// --- Room presence tests --- 102 103func TestRoom_RegisterClient_AppearsInPresence(t *testing.T) { 104 hub := NewHub() 105 room := hub.GetOrCreateRoom("doc-presence") 106 c := stubClient(hub, "did:plc:alice", "Alice", "#ff0000", "doc-presence") 107 108 room.RegisterClient(c) 109 110 // Wait for presence broadcast (register → broadcastPresence) 111 msgs := waitForMessages(c, 1, 2*time.Second) 112 if len(msgs) == 0 { 113 t.Fatal("expected presence message after register, got none") 114 } 115 116 var pres PresenceMessage 117 if err := json.Unmarshal(msgs[0], &pres); err != nil { 118 t.Fatalf("unmarshal presence: %v", err) 119 } 120 if pres.Type != "presence" { 121 t.Errorf("expected type=presence, got %q", pres.Type) 122 } 123 if len(pres.Users) != 1 { 124 t.Fatalf("expected 1 user in presence, got %d", len(pres.Users)) 125 } 126 if pres.Users[0].DID != "did:plc:alice" { 127 t.Errorf("expected DID did:plc:alice, got %q", pres.Users[0].DID) 128 } 129} 130 131func TestRoom_MultipleClients_PresenceContainsAll(t *testing.T) { 132 hub := NewHub() 133 room := hub.GetOrCreateRoom("doc-multi") 134 135 alice := stubClient(hub, "did:plc:alice", "Alice", "#ff0000", "doc-multi") 136 bob := stubClient(hub, "did:plc:bob", "Bob", "#0000ff", "doc-multi") 137 138 room.RegisterClient(alice) 139 // Drain alice's initial presence (just herself) 140 waitForMessages(alice, 1, time.Second) 141 142 room.RegisterClient(bob) 143 // Both get a new presence broadcast; wait for 1 more on each 144 aliceMsgs := waitForMessages(alice, 1, 2*time.Second) 145 bobMsgs := waitForMessages(bob, 1, 2*time.Second) 146 147 checkPresenceCount := func(name string, msgs [][]byte, want int) { 148 t.Helper() 149 if len(msgs) == 0 { 150 t.Fatalf("%s: expected presence message, got none", name) 151 } 152 var pres PresenceMessage 153 if err := json.Unmarshal(msgs[len(msgs)-1], &pres); err != nil { 154 t.Fatalf("%s: unmarshal: %v", name, err) 155 } 156 if len(pres.Users) != want { 157 t.Errorf("%s: expected %d users in presence, got %d", name, want, len(pres.Users)) 158 } 159 } 160 161 checkPresenceCount("alice", aliceMsgs, 2) 162 checkPresenceCount("bob", bobMsgs, 2) 163} 164 165func TestRoom_UnregisterClient_RemovedFromPresence(t *testing.T) { 166 hub := NewHub() 167 room := hub.GetOrCreateRoom("doc-unreg") 168 169 alice := stubClient(hub, "did:plc:alice", "Alice", "#ff0000", "doc-unreg") 170 bob := stubClient(hub, "did:plc:bob", "Bob", "#0000ff", "doc-unreg") 171 172 room.RegisterClient(alice) 173 room.RegisterClient(bob) 174 // Drain initial presence messages 175 time.Sleep(100 * time.Millisecond) 176 drain(alice, 200*time.Millisecond) 177 drain(bob, 200*time.Millisecond) 178 179 // Now unregister alice 180 room.UnregisterClient(alice) 181 182 // Bob should receive a presence update with 1 user 183 msgs := waitForMessages(bob, 1, 2*time.Second) 184 if len(msgs) == 0 { 185 t.Fatal("expected presence update after unregister, got none") 186 } 187 var pres PresenceMessage 188 if err := json.Unmarshal(msgs[len(msgs)-1], &pres); err != nil { 189 t.Fatalf("unmarshal: %v", err) 190 } 191 if len(pres.Users) != 1 { 192 t.Errorf("expected 1 user after alice unregisters, got %d", len(pres.Users)) 193 } 194 if pres.Users[0].DID != "did:plc:bob" { 195 t.Errorf("expected bob to remain, got %q", pres.Users[0].DID) 196 } 197} 198 199// --- Room broadcast tests --- 200 201func TestRoom_Broadcast_SendsToAll(t *testing.T) { 202 hub := NewHub() 203 room := hub.GetOrCreateRoom("doc-broadcast") 204 205 alice := stubClient(hub, "did:plc:alice", "Alice", "#ff0000", "doc-broadcast") 206 bob := stubClient(hub, "did:plc:bob", "Bob", "#0000ff", "doc-broadcast") 207 208 room.RegisterClient(alice) 209 room.RegisterClient(bob) 210 // Drain presence messages 211 time.Sleep(100 * time.Millisecond) 212 drain(alice, 200*time.Millisecond) 213 drain(bob, 200*time.Millisecond) 214 215 room.Broadcast([]byte(`{"type":"ping"}`)) 216 217 aliceMsgs := waitForMessages(alice, 1, time.Second) 218 bobMsgs := waitForMessages(bob, 1, time.Second) 219 220 if len(aliceMsgs) == 0 { 221 t.Error("alice: expected broadcast message") 222 } 223 if len(bobMsgs) == 0 { 224 t.Error("bob: expected broadcast message") 225 } 226} 227 228func TestRoom_BroadcastExcept_SkipsSender(t *testing.T) { 229 hub := NewHub() 230 room := hub.GetOrCreateRoom("doc-except") 231 232 alice := stubClient(hub, "did:plc:alice", "Alice", "#ff0000", "doc-except") 233 bob := stubClient(hub, "did:plc:bob", "Bob", "#0000ff", "doc-except") 234 235 room.RegisterClient(alice) 236 room.RegisterClient(bob) 237 time.Sleep(100 * time.Millisecond) 238 drain(alice, 200*time.Millisecond) 239 drain(bob, 200*time.Millisecond) 240 241 room.BroadcastExcept([]byte(`{"type":"edit"}`), alice) 242 243 // Bob should receive it; alice should not 244 bobMsgs := waitForMessages(bob, 1, time.Second) 245 aliceMsgs := drain(alice, 300*time.Millisecond) 246 247 if len(bobMsgs) == 0 { 248 t.Error("bob: expected broadcast message") 249 } 250 if len(aliceMsgs) > 0 { 251 t.Errorf("alice: should not receive her own broadcast, got %d messages", len(aliceMsgs)) 252 } 253} 254 255// --- GetPresence --- 256 257func TestRoom_GetPresence_Empty(t *testing.T) { 258 hub := NewHub() 259 room := hub.GetOrCreateRoom("doc-empty-presence") 260 users := room.GetPresence() 261 if len(users) != 0 { 262 t.Errorf("expected 0 users in new room, got %d", len(users)) 263 } 264}