feat(lib): chat-room protocol (list, join, leave, say) with events

Implement the Soulseek chat-room server messages against soulfind's
wire format:
- MessageFactory builders: room-list request (64), join (14),
  leave (15), say (13).
- Handlers parsing the server side: RoomList (64) names+counts,
  JoinRoom (14) membership, SayChatroom (13), UserJoinedRoom (16),
  UserLeftRoom (17), LeaveRoom (15).
- Surface everything to the client as a drained RoomEvent stream plus
  a room-list snapshot; new Client API: request_room_list, join_room,
  leave_room, say_in_room, room_list, take_room_events.

Unit tests cover each builder and handler; server-gated e2e tests
exercise message delivery and room listing against soulfind.
This commit is contained in:
michel
2026-07-19 19:47:58 +02:00
parent ae28f4236a
commit 7bc0ee2c3d
12 changed files with 718 additions and 4 deletions
+79
View File
@@ -292,6 +292,85 @@ fn a_private_message_is_delivered_between_users() {
assert_eq!(message.username(), "e2e_alice_pm");
}
#[test]
fn a_chat_room_message_is_delivered_between_users() {
use soulseek_rs::types::RoomEvent;
let server = server_or_skip!();
let room = "e2e_room_chat";
let mut alice =
Client::with_settings(server.settings("e2e_alice_room", "pw"));
let mut bob = Client::with_settings(server.settings("e2e_bob_room", "pw"));
alice.connect().expect("alice connect");
bob.connect().expect("bob connect");
assert!(alice.login().expect("alice login"));
assert!(bob.login().expect("bob login"));
alice.join_room(room).expect("alice joins room");
bob.join_room(room).expect("bob joins room");
// Give both joins time to register on the server before speaking.
std::thread::sleep(Duration::from_millis(500));
let _ = alice.take_room_events();
let _ = bob.take_room_events();
let body = "hello room, this is alice";
alice.say_in_room(room, body).expect("alice says in room");
// Delivery is asynchronous; poll Bob's room events until the message lands.
let deadline = Instant::now() + Duration::from_secs(5);
let mut got = None;
while Instant::now() < deadline {
for event in bob.take_room_events() {
if let RoomEvent::Message {
room: r,
username,
message,
} = event
&& r == room
&& message == body
{
got = Some(username);
}
}
if got.is_some() {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
assert_eq!(
got.as_deref(),
Some("e2e_alice_room"),
"bob should receive alice's room message"
);
}
#[test]
fn the_room_list_includes_a_joined_room() {
let server = server_or_skip!();
let room = "e2e_room_listed";
let mut alice =
Client::with_settings(server.settings("e2e_alice_list", "pw"));
alice.connect().expect("alice connect");
assert!(alice.login().expect("alice login"));
alice.join_room(room).expect("alice joins room");
// Once a user is in the room the server should advertise it in RoomList.
let deadline = Instant::now() + Duration::from_secs(5);
let mut listed = false;
while Instant::now() < deadline {
alice.request_room_list().expect("request room list");
std::thread::sleep(Duration::from_millis(250));
if alice.room_list().iter().any(|r| r.name == room) {
listed = true;
break;
}
}
assert!(listed, "the joined room should appear in the room list");
}
#[test]
fn login_succeeds_with_listener_enabled() {
let server = server_or_skip!();