test(relay): cover gateway restart lifecycle
TESTING.md asks for a lifecycle sanity check where the Windows client stays connected while the LAN gateway restarts. The relay integration tests covered a single gateway/client lifetime, but they did not prove that a replacement gateway receives the existing client catch-up state and can bridge frames after the first gateway leaves. Add a real-session relay test that connects a gateway and client, shuts down the first gateway, verifies the client receives the gateway leave event, connects a second gateway, verifies both sides receive the expected lifecycle state, and then sends Ethernet frames in both directions through the replacement gateway. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay reconnects_gateway_while_client_stays_joined \ -- --nocapture - cargo test -p lanparty-relay - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: MVP lifecycle sanity check
This commit is contained in:
@@ -1632,6 +1632,172 @@ mod tests {
|
||||
assert!(sessions.lock().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnects_gateway_while_client_stays_joined() {
|
||||
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(3).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 first_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.clone(),
|
||||
room.clone(),
|
||||
client_mac,
|
||||
1400,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first_gateway_peer_id = first_gateway.welcome().peer_id();
|
||||
assert_eq!(first_gateway_peer_id, 1);
|
||||
assert_eq!(client.welcome().peer_id(), 2);
|
||||
assert!(client.welcome().gateway_connected());
|
||||
assert_eq!(client.welcome().gateway_peer_id(), Some(1));
|
||||
|
||||
let ControlMessage::PeerJoined(peer) =
|
||||
tokio::time::timeout(Duration::from_secs(5), first_gateway.recv_control_event())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected first gateway to observe client join");
|
||||
};
|
||||
assert_eq!(peer.peer_id(), client.welcome().peer_id());
|
||||
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 first gateway catch-up event");
|
||||
};
|
||||
assert_eq!(peer.peer_id(), first_gateway_peer_id);
|
||||
assert_eq!(peer.role(), Role::Gateway);
|
||||
|
||||
first_gateway.shutdown("gateway restart").await;
|
||||
|
||||
let ControlMessage::PeerLeft { peer_id, reason } =
|
||||
tokio::time::timeout(Duration::from_secs(5), client.recv_control_event())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected client to observe first gateway leave");
|
||||
};
|
||||
assert_eq!(peer_id, first_gateway_peer_id);
|
||||
assert_eq!(reason, DisconnectReason::Normal);
|
||||
|
||||
let second_gateway = connect_gateway(
|
||||
GatewayConfig::new(
|
||||
server_addr,
|
||||
"lanparty-relay.local",
|
||||
cert_der,
|
||||
room.clone(),
|
||||
"eth0",
|
||||
1400,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(second_gateway.welcome().peer_id(), 3);
|
||||
|
||||
let ControlMessage::PeerJoined(peer) =
|
||||
tokio::time::timeout(Duration::from_secs(5), second_gateway.recv_control_event())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected second gateway to receive client catch-up event");
|
||||
};
|
||||
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 observe replacement gateway join");
|
||||
};
|
||||
assert_eq!(peer.peer_id(), second_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), second_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());
|
||||
second_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(),
|
||||
second_gateway.welcome().peer_id()
|
||||
);
|
||||
assert_eq!(received.payload(), lan_to_client.as_slice());
|
||||
|
||||
client.shutdown("test client done").await;
|
||||
second_gateway.shutdown("test gateway done").await;
|
||||
|
||||
let accepted = tokio::time::timeout(Duration::from_secs(5), server_task)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.len(), 3);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user