synvael/crates/net/tests/handshake.rs

102 lines
3.7 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
//! Loopback integration tests for the Synvael application handshake.
//!
//! Each test binds a real QUIC server endpoint on `127.0.0.1:0`, reads the OS-assigned port, and drives a client through the full `ClientHello` -> `HandshakeAck` / `HandshakeReject` exchange, exercising the async `read_frame`/`write_frame` path end-to-end.
use net::endpoint::{client_endpoint, server_endpoint};
use net::handshake::{HandshakeError, accept_connection, connect};
use shared::protocol::{ClientHello, FeatureFlags, PROTOCOL_VERSION, PlayerIdentity, RejectReason};
/// Builds a `ClientHello` for `display_name` advertising `protocol_version`.
fn hello(display_name: &str, protocol_version: u32) -> ClientHello {
ClientHello {
protocol_version,
client_build: "synvael-client-test".to_owned(),
player_identity: PlayerIdentity {
display_name: display_name.to_owned(),
},
installed_packs: vec![],
requested_features: FeatureFlags(0),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn happy_path_completes_handshake() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let server = server_endpoint("127.0.0.1:0".parse()?)?;
let server_addr = server.local_addr()?;
// Accept exactly one connection on the server, returning the observed identity. The connection is held open until the client closes it, so the ack frame is reliably delivered before teardown.
let server_task = tokio::spawn(async move {
let incoming = server.accept().await.ok_or("server endpoint closed")?;
let conn = accept_connection(incoming, "synvael-server-test".to_owned(), 20).await?;
let name = conn.hello.player_identity.display_name.clone();
conn.connection.closed().await;
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(name)
});
let client = client_endpoint()?;
let connected = connect(
&client,
server_addr,
"localhost",
hello("Tester", PROTOCOL_VERSION),
)
.await?;
assert_eq!(
connected.ack.protocol_version, PROTOCOL_VERSION,
"server must ack with the matching protocol version"
);
// Close the client connection so the server's `closed()` wait resolves.
drop(connected);
let observed_name = server_task.await??;
assert_eq!(
observed_name, "Tester",
"server must observe the client's display name"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn version_mismatch_is_rejected() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let server = server_endpoint("127.0.0.1:0".parse()?)?;
let server_addr = server.local_addr()?;
let server_task = tokio::spawn(async move {
let incoming = server.accept().await.ok_or("server endpoint closed")?;
// The server is expected to return VersionMismatch after sending the reject.
let result = accept_connection(incoming, "synvael-server-test".to_owned(), 20).await;
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(result.is_err())
});
let client = client_endpoint()?;
let result = connect(
&client,
server_addr,
"localhost",
hello("Tester", PROTOCOL_VERSION + 1),
)
.await;
match result {
Err(HandshakeError::Rejected(rej)) => {
assert_eq!(
rej.reason,
RejectReason::ProtocolMismatch,
"rejection must cite a protocol mismatch"
);
}
other => return Err(format!("expected a rejection, got {other:?}").into()),
}
assert!(
server_task.await??,
"server must return an error on mismatch"
);
Ok(())
}