From b17b6f068372ddac1147041aa2c682bd27f8243c Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 23:07:20 +0200 Subject: [PATCH] test(relay): cover real client and gateway sessions Add a relay-server test that connects the production client-core session and the production gateway session to the same in-process relay. The test verifies room join catch-up, negotiated peer identities, Ethernet datagrams in both directions, stats reporting, and room/session cleanup after graceful shutdown. The existing relay tests covered forwarding with raw Quinn peers. This adds a higher-level proof that the three MVP components agree on the same control and datagram contracts before the final Windows TAP and physical LAN test. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay bridges_real_client_and_gateway_sessions -- --nocapture - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md Phase 1 relay/client/gateway proof --- Cargo.lock | 2 + crates/lanparty-relay/Cargo.toml | 4 + crates/lanparty-relay/src/server.rs | 139 ++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9d6719c..e6355b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,7 +540,9 @@ dependencies = [ "anyhow", "bytes", "clap", + "lanparty-client-core", "lanparty-ctrl", + "lanparty-gateway", "lanparty-net", "lanparty-obs", "lanparty-proto", diff --git a/crates/lanparty-relay/Cargo.toml b/crates/lanparty-relay/Cargo.toml index 206ae85..5624896 100644 --- a/crates/lanparty-relay/Cargo.toml +++ b/crates/lanparty-relay/Cargo.toml @@ -16,3 +16,7 @@ rcgen.workspace = true rustls.workspace = true thiserror.workspace = true tokio.workspace = true + +[dev-dependencies] +lanparty-client-core = { path = "../lanparty-client-core" } +lanparty-gateway = { path = "../lanparty-gateway" } diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index a99af70..c702e96 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -905,7 +905,9 @@ mod tests { }; use bytes::Bytes; + use lanparty_client_core::{ClientSessionConfig, connect_client}; use lanparty_ctrl::{RoomCode, decode_control_frame, encode_control_message}; + use lanparty_gateway::{GatewayConfig, connect_gateway}; use lanparty_proto::{FrameType, MacAddr, decode_datagram, encode_datagram}; use quinn::{ClientConfig, crypto::rustls::QuicClientConfig}; @@ -1223,6 +1225,139 @@ mod tests { assert!(sessions.lock().await.is_empty()); } + #[tokio::test] + async fn bridges_real_client_and_gateway_sessions() { + let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); + let rooms = Arc::clone(&server.rooms); + let sessions = Arc::clone(&server.sessions); + 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 cert_der = certificate.as_ref().to_vec(); + let room = RoomCode::new("TESTROOM").unwrap(); + let client_mac = client_mac(1); + let gateway = connect_gateway( + GatewayConfig::new( + server_addr, + "lanparty-relay.local", + cert_der.clone(), + room.clone(), + "eth0", + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + let client = connect_client( + ClientSessionConfig::new( + server_addr, + "lanparty-relay.local", + cert_der, + room.clone(), + client_mac, + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(gateway.welcome().room_id(), client.welcome().room_id()); + assert_eq!(gateway.welcome().peer_id(), 1); + assert_eq!(client.welcome().peer_id(), 2); + assert!(client.welcome().gateway_connected()); + assert_eq!( + gateway.quic_max_datagram_size(), + client.quic_max_datagram_size() + ); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), gateway.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected gateway to observe client join"); + }; + assert_eq!(peer.peer_id(), client.welcome().peer_id()); + assert_eq!(peer.role(), Role::Client); + assert_eq!(peer.mac(), Some(client_mac)); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), client.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected client to receive gateway catch-up event"); + }; + assert_eq!(peer.peer_id(), gateway.welcome().peer_id()); + assert_eq!(peer.role(), Role::Gateway); + + let client_to_lan = ethernet_frame(gateway_mac(), client_mac); + assert_eq!( + client + .relay_io() + .send_ethernet_with_outcome(&client_to_lan) + .unwrap(), + lanparty_client_core::ClientSendOutcome::Sent + ); + let received = tokio::time::timeout(Duration::from_secs(5), gateway.recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), client.welcome().peer_id()); + assert_eq!(received.payload(), client_to_lan.as_slice()); + + let lan_to_client = ethernet_frame(client_mac, gateway_mac()); + gateway.send_ethernet(&lan_to_client).unwrap(); + let received = + tokio::time::timeout(Duration::from_secs(5), client.relay_io().recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), gateway.welcome().peer_id()); + assert_eq!(received.payload(), lan_to_client.as_slice()); + + client.send_stats_snapshot().await.unwrap(); + gateway.send_stats_snapshot().await.unwrap(); + wait_for_peer_stats( + &sessions, + &room, + client.welcome().peer_id(), + &client.stats_snapshot(), + ) + .await; + wait_for_peer_stats( + &sessions, + &room, + gateway.welcome().peer_id(), + &gateway.stats_snapshot(), + ) + .await; + + client.shutdown("test client done").await; + gateway.shutdown("test gateway done").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); + assert!(sessions.lock().await.is_empty()); + } + #[tokio::test] async fn forwards_graceful_disconnect_reason_to_remaining_peers() { let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); @@ -1422,6 +1557,10 @@ mod tests { MacAddr::new([0x02, 0, 0, 0, 0, last]) } + fn gateway_mac() -> MacAddr { + MacAddr::new([0x0a, 0, 0, 0, 0, 1]) + } + fn ethernet_frame(destination: MacAddr, source: MacAddr) -> Vec { let mut frame = Vec::new(); frame.extend_from_slice(&destination.octets());