From e30cca68257538c16db435baff5d4b9919bf5c6a Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Wed, 9 Sep 2026 17:16:10 +0000 Subject: [PATCH] test(e2e): a room announces members arriving and leaving UserJoinedRoom and UserLeftRoom (codes 16 and 17) were parsed and applied to the roster with nothing proving either end of that, which is the kind of gap that only shows up when someone relies on it. --- docs/protocol-coverage.md | 4 +- soulseek-rs-lib/tests/e2e.rs | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/docs/protocol-coverage.md b/docs/protocol-coverage.md index 9a4bebc..dbb96c7 100644 --- a/docs/protocol-coverage.md +++ b/docs/protocol-coverage.md @@ -23,8 +23,8 @@ Message names and codes follow the Soulseek protocol as implemented by | 13 | SayChatroom | yes | e2e — a message between two users in a room | | 14 | JoinRoom | yes | e2e — public rooms, and private ones | | 15 | LeaveRoom | yes | e2e — room list and membership | -| 16 | UserJoinedRoom | yes | e2e — room membership statistics | -| 17 | UserLeftRoom | yes | e2e — room membership statistics | +| 16 | UserJoinedRoom | yes | e2e — a member arriving is announced | +| 17 | UserLeftRoom | yes | e2e — a member leaving is announced | | 18 | ConnectToPeer | yes | e2e — firewalled download and browse | | 22 | MessageUser | yes | e2e — a private message between users | | 23 | MessageAcked | yes | e2e — an offline message is delivered once | diff --git a/soulseek-rs-lib/tests/e2e.rs b/soulseek-rs-lib/tests/e2e.rs index cf6a37d..059a991 100644 --- a/soulseek-rs-lib/tests/e2e.rs +++ b/soulseek-rs-lib/tests/e2e.rs @@ -5212,3 +5212,75 @@ fn a_peer_tells_us_about_itself_when_asked() { let _ = std::fs::remove_dir_all(share_dir); } + +#[test] +fn a_room_reports_members_arriving_and_leaving() { + use soulseek_rs::types::RoomEvent; + let server = server_or_skip!(); + + let room = "e2e_room_comings"; + let mut alice = + Client::with_settings(server.settings("e2e_comings_alice", "pw")); + alice.connect().expect("alice connect"); + assert!(alice.login().expect("alice login")); + alice.join_room(room).expect("alice joins"); + std::thread::sleep(Duration::from_millis(500)); + let _ = alice.take_room_events(); + + // Bob arrives after her: she is told (code 16), and her roster grows. + let mut bob = + Client::with_settings(server.settings("e2e_comings_bob", "pw")); + bob.connect().expect("bob connect"); + assert!(bob.login().expect("bob login")); + bob.join_room(room).expect("bob joins"); + + let joined = + await_room_event(&alice, Duration::from_secs(5), |event| match event { + RoomEvent::UserJoined { room: r, username } + if r == room && username == "e2e_comings_bob" => + { + Some(()) + } + _ => None, + }); + assert!(joined.is_some(), "alice should be told bob arrived"); + assert!( + alice + .room_members(room) + .iter() + .any(|u| u == "e2e_comings_bob"), + "the roster should carry bob, got {:?}", + alice.room_members(room) + ); + + // And when he leaves (code 17) the roster loses him again. + bob.leave_room(room).expect("bob leaves"); + let left = + await_room_event(&alice, Duration::from_secs(5), |event| match event { + RoomEvent::UserLeft { room: r, username } + if r == room && username == "e2e_comings_bob" => + { + Some(()) + } + _ => None, + }); + assert!(left.is_some(), "alice should be told bob left"); + + let deadline = Instant::now() + Duration::from_secs(3); + while Instant::now() < deadline + && alice + .room_members(room) + .iter() + .any(|u| u == "e2e_comings_bob") + { + std::thread::sleep(Duration::from_millis(100)); + } + assert!( + !alice + .room_members(room) + .iter() + .any(|u| u == "e2e_comings_bob"), + "the roster should drop him, got {:?}", + alice.room_members(room) + ); +}