diff --git a/internal/hub/hub_integration_test.go b/internal/hub/hub_integration_test.go index 25c6bae..2860517 100644 --- a/internal/hub/hub_integration_test.go +++ b/internal/hub/hub_integration_test.go @@ -24,7 +24,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *Hub) { return server, h } -// helper: dial a WebSocket connection to the test server +// helper: dial a WebSocket connection to the test server (default room) func dialWS(t *testing.T, server *httptest.Server) *websocket.Conn { t.Helper() wsURL := "ws" + strings.TrimPrefix(server.URL, "http") @@ -35,6 +35,17 @@ func dialWS(t *testing.T, server *httptest.Server) *websocket.Conn { return conn } +// helper: dial a WebSocket connection to a specific room +func dialWSWithRoom(t *testing.T, server *httptest.Server, room string) *websocket.Conn { + t.Helper() + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "?room=" + room + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + t.Fatalf("Failed to dial WebSocket (room=%q): %v", room, err) + } + return conn +} + // helper: wait until hub reaches expected client count or timeout func waitForClients(t *testing.T, h *Hub, expected int, timeout time.Duration) { t.Helper() @@ -356,3 +367,137 @@ func TestIntegration_LargeMessage(t *testing.T) { t.Errorf("Expected message length %d, got %d", 64*1024, len(msg)) } } + +func TestIntegration_RoomIsolation_MessagesOnlyGoToSameRoom(t *testing.T) { + server, h := setupTestServer(t) + defer server.Close() + defer h.Shutdown() + + // Connect 2 clients to room-a, 1 client to room-b + connA1 := dialWSWithRoom(t, server, "room-a") + defer connA1.Close() + connA2 := dialWSWithRoom(t, server, "room-a") + defer connA2.Close() + connB1 := dialWSWithRoom(t, server, "room-b") + defer connB1.Close() + + waitForClients(t, h, 3, time.Second) + + // Send a message from client A1 + testMsg := "hello room-a" + if err := connA1.WriteMessage(websocket.TextMessage, []byte(testMsg)); err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + // Both room-a clients should receive the message + for i, conn := range []*websocket.Conn{connA1, connA2} { + conn.SetReadDeadline(time.Now().Add(time.Second)) + _, msg, err := conn.ReadMessage() + if err != nil { + t.Fatalf("Room-a client %d failed to read message: %v", i+1, err) + } + if string(msg) != testMsg { + t.Errorf("Room-a client %d expected %q, got %q", i+1, testMsg, string(msg)) + } + } + + // Room-b client should NOT receive the message + connB1.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, _, err := connB1.ReadMessage() + if err == nil { + t.Fatal("Room-b client should NOT have received a message from room-a") + } + // Timeout error is expected — message was correctly not delivered +} + +func TestIntegration_RoomIsolation_MultipleRoomsIndependent(t *testing.T) { + server, h := setupTestServer(t) + defer server.Close() + defer h.Shutdown() + + // Connect clients to two different rooms + connA := dialWSWithRoom(t, server, "alpha") + defer connA.Close() + connB := dialWSWithRoom(t, server, "beta") + defer connB.Close() + + waitForClients(t, h, 2, time.Second) + + // Send message from room alpha + msgAlpha := "alpha message" + if err := connA.WriteMessage(websocket.TextMessage, []byte(msgAlpha)); err != nil { + t.Fatalf("Failed to send alpha message: %v", err) + } + + // Room alpha client receives its own message + connA.SetReadDeadline(time.Now().Add(time.Second)) + _, msg, err := connA.ReadMessage() + if err != nil { + t.Fatalf("Alpha client failed to read: %v", err) + } + if string(msg) != msgAlpha { + t.Errorf("Alpha client expected %q, got %q", msgAlpha, string(msg)) + } + + // Send message from room beta + msgBeta := "beta message" + if err := connB.WriteMessage(websocket.TextMessage, []byte(msgBeta)); err != nil { + t.Fatalf("Failed to send beta message: %v", err) + } + + // Room beta client receives its own message + connB.SetReadDeadline(time.Now().Add(time.Second)) + _, msg, err = connB.ReadMessage() + if err != nil { + t.Fatalf("Beta client failed to read: %v", err) + } + if string(msg) != msgBeta { + t.Errorf("Beta client expected %q, got %q", msgBeta, string(msg)) + } + + // Verify alpha did NOT receive beta's message + connA.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, _, err = connA.ReadMessage() + if err == nil { + t.Fatal("Alpha client should NOT have received beta's message") + } + + // Verify beta did NOT receive alpha's message + connB.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, _, err = connB.ReadMessage() + if err == nil { + t.Fatal("Beta client should NOT have received alpha's message") + } +} + +func TestIntegration_BroadcastToEmptyRoom(t *testing.T) { + server, h := setupTestServer(t) + defer server.Close() + defer h.Shutdown() + + // Connect a client to room-a and a separate client to verify hub is functional + connA := dialWSWithRoom(t, server, "room-a") + defer connA.Close() + + waitForClients(t, h, 1, time.Second) + + // Send directly to broadcast channel targeting a non-existent room. + // This should be handled gracefully (no panic, no delivery). + h.broadcast <- broadcastMsg{room: "non-existent", data: []byte("ghost message")} + + // Give the hub time to process + time.Sleep(50 * time.Millisecond) + + // Now send a real message to room-a to confirm hub is still functional + h.broadcast <- broadcastMsg{room: "room-a", data: []byte("real message")} + + connA.SetReadDeadline(time.Now().Add(time.Second)) + _, msg, err := connA.ReadMessage() + if err != nil { + t.Fatalf("Room-a client failed to read real message after empty-room broadcast: %v", err) + } + if string(msg) != "real message" { + t.Errorf("Expected %q, got %q", "real message", string(msg)) + } +} + diff --git a/internal/hub/hub_test.go b/internal/hub/hub_test.go index dd4f6df..e455e93 100644 --- a/internal/hub/hub_test.go +++ b/internal/hub/hub_test.go @@ -245,3 +245,38 @@ func TestUnregisterUnknownConnNoPanic(t *testing.T) { t.Errorf("Expected 0 clients, got %d", count) } } + +func TestBroadcastRoomIsolation(t *testing.T) { + h := New(newTestLogger()) + go h.Run() + defer h.Shutdown() + + connA := dialTestHub(t, h, "room-a") + defer connA.Close() + connB := dialTestHub(t, h, "room-b") + defer connB.Close() + + // Allow registers to be processed + time.Sleep(50 * time.Millisecond) + + // Send message to room-a via broadcast channel + h.broadcast <- broadcastMsg{room: "room-a", data: []byte("for-a-only")} + + // Room-a client should receive the message + connA.SetReadDeadline(time.Now().Add(time.Second)) + _, msg, err := connA.ReadMessage() + if err != nil { + t.Fatalf("Room-a client failed to read: %v", err) + } + if string(msg) != "for-a-only" { + t.Errorf("Expected %q, got %q", "for-a-only", string(msg)) + } + + // Room-b client should NOT receive the message (short timeout) + connB.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, _, err = connB.ReadMessage() + if err == nil { + t.Fatal("Room-b client should NOT have received room-a's message") + } +} +