From 14524f1593ef8bc7d77150834c0aac3f68ca5e77 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 06:34:48 +0200 Subject: [PATCH] test(relay): cover join notification ordering Add a relay regression test for the join ordering used by gateway MAC-state seeding. The test sends a second client's hello but intentionally delays reading that client's welcome until after the existing peer receives PeerJoined. This guards the ordering from the relay admission path: existing peers are notified before the joining peer can proceed from its welcome and begin sending Ethernet datagrams. That matters for first DHCP/ARP frames after a Windows client joins a room with an existing LAN gateway. Test Plan: - cargo test -p lanparty-relay notifies_existing_peer_before_join_welcome - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Refs: PLAN.md MVP relay lifecycle and gateway MAC learning --- crates/lanparty-relay/src/server.rs | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 964a4b6..32fc18c 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -1004,6 +1004,71 @@ mod tests { assert_eq!(rooms.lock().await.room_count(), 0); } + #[tokio::test] + async fn notifies_existing_peer_before_join_welcome() { + let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); + let rooms = Arc::clone(&server.rooms); + let server_addr = server.local_addr().unwrap(); + let server_task = tokio::spawn(async move { + let handles = server.accept_many_for_test(2).await.unwrap(); + let mut accepted = Vec::with_capacity(handles.len()); + + for handle in handles { + accepted.push(handle.await.unwrap().unwrap().unwrap()); + } + + server.shutdown("test complete").await; + accepted + }); + + let first_endpoint = client_endpoint(certificate.clone()).unwrap(); + let first_connection = first_endpoint + .connect(server_addr, "lanparty-relay.local") + .unwrap() + .await + .unwrap(); + let second_endpoint = client_endpoint(certificate).unwrap(); + let second_connection = second_endpoint + .connect(server_addr, "lanparty-relay.local") + .unwrap() + .await + .unwrap(); + + let first_welcome = welcome_for_client(&first_connection, client_mac(1)).await; + let mut second_response = + send_client_hello_without_reading_response(&second_connection, client_mac(2)).await; + + let ControlMessage::PeerJoined(peer) = read_control_event(&first_connection).await else { + panic!("expected existing peer to receive peer joined event"); + }; + assert_eq!(peer.peer_id(), 2); + assert_eq!(peer.role(), Role::Client); + assert_eq!(peer.mac(), Some(client_mac(2))); + + let response = second_response + .read_to_end(MAX_CONTROL_FRAME_LEN) + .await + .unwrap(); + let ControlMessage::Welcome(second_welcome) = decode_control_frame(&response).unwrap() + else { + panic!("expected joining peer welcome"); + }; + assert_eq!(second_welcome.peer_id(), 2); + assert_eq!(second_welcome.room_id(), first_welcome.room_id()); + + first_connection.close(0_u32.into(), b"test complete"); + second_connection.close(0_u32.into(), b"test complete"); + first_endpoint.wait_idle().await; + second_endpoint.wait_idle().await; + + let accepted = tokio::time::timeout(Duration::from_secs(5), server_task) + .await + .unwrap() + .unwrap(); + assert_eq!(accepted.len(), 2); + assert_eq!(rooms.lock().await.room_count(), 0); + } + #[tokio::test] async fn writes_development_certificate_when_configured() { let cert_path = unique_temp_cert_path(); @@ -1548,6 +1613,19 @@ mod tests { welcome } + async fn send_client_hello_without_reading_response( + connection: &quinn::Connection, + mac: MacAddr, + ) -> RecvStream { + let hello = EndpointHello::client(RoomCode::new("TESTROOM").unwrap(), mac, 1400).unwrap(); + let (mut send, recv) = connection.open_bi().await.unwrap(); + let request = encode_control_message(&ControlMessage::Hello(hello)).unwrap(); + send.write_all(&request).await.unwrap(); + send.finish().unwrap(); + + recv + } + async fn accepted_client_for_forwarding( rooms: &Arc>, mac: MacAddr,