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
This commit is contained in:
2026-05-22 06:34:48 +02:00
parent 1d469d437b
commit 14524f1593
+78
View File
@@ -1004,6 +1004,71 @@ mod tests {
assert_eq!(rooms.lock().await.room_count(), 0); 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] #[tokio::test]
async fn writes_development_certificate_when_configured() { async fn writes_development_certificate_when_configured() {
let cert_path = unique_temp_cert_path(); let cert_path = unique_temp_cert_path();
@@ -1548,6 +1613,19 @@ mod tests {
welcome 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( async fn accepted_client_for_forwarding(
rooms: &Arc<Mutex<RoomRegistry>>, rooms: &Arc<Mutex<RoomRegistry>>,
mac: MacAddr, mac: MacAddr,