// file: crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs // version: 4 const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); #[test] fn certificate_hash_preserves_exact_sha256_bytes() { let bytes = [7_u8; super::CERTIFICATE_HASH_SIZE]; let hash = super::WebTransportCertificateHash::from_sha256(bytes); assert_eq!(hash.as_bytes(), &bytes); } #[test] fn client_config_accepts_https_and_rejects_non_secure_schemes() { let hash = super::WebTransportCertificateHash::from_sha256([1_u8; super::CERTIFICATE_HASH_SIZE]); let secure = super::WebTransportClientConfig::new("https://127.0.0.1:4433/game", hash.clone()); assert!(secure.is_ok()); let insecure_http = super::WebTransportClientConfig::new("http://127.0.0.1:4433/game", hash.clone()); match insecure_http { Ok(_) => panic!("HTTP endpoint unexpectedly accepted"), Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration), } let websocket = super::WebTransportClientConfig::new("ws://127.0.0.1:4433/game", hash); match websocket { Ok(_) => panic!("WebSocket endpoint unexpectedly accepted"), Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration), } } #[test] fn frame_header_is_big_endian_and_configured_payload_bound_is_enforced() { let header = match super::frame_header(0x00_01_02_03, 1024 * 1024) { Ok(value) => value, Err(error) => panic!("valid frame header rejected: {error}"), }; assert_eq!(header, [0x00, 0x01, 0x02, 0x03]); let oversized = super::frame_header(33, 32); match oversized { Ok(_) => panic!("oversized WebTransport frame unexpectedly accepted"), Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::MessageTooLarge), } } #[test] fn stream_error_mapping_distinguishes_abort_close_and_protocol_failures() { let stopped = super::map_write_error(web_transport_quinn::WriteError::Stopped(7)); assert_eq!(stopped.kind(), game_realtime_transport_lib::TransportErrorKind::Aborted); let reset = super::map_read_error(web_transport_quinn::ReadError::Reset(8)); assert_eq!(reset.kind(), game_realtime_transport_lib::TransportErrorKind::Aborted); let closed = super::map_read_error(web_transport_quinn::ReadError::SessionError(web_transport_quinn::SessionError::WebTransportError( web_transport_quinn::WebTransportError::Closed(9, "done".to_owned()), ))); assert_eq!(closed.kind(), game_realtime_transport_lib::TransportErrorKind::Closed); let protocol = super::map_read_error(web_transport_quinn::ReadError::IllegalOrderedRead); assert_eq!(protocol.kind(), game_realtime_transport_lib::TransportErrorKind::Protocol); } #[test] fn generated_loopback_identity_has_sha256_fingerprint() { let identity = match super::WebTransportServerIdentity::generate_loopback() { Ok(value) => value, Err(error) => panic!("loopback identity generation failed: {error}"), }; assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE); } #[test] fn injected_identity_rejects_empty_certificate_or_key() { let missing_certificate = super::WebTransportServerIdentity::from_pkcs8_der(Vec::new(), vec![1]); match missing_certificate { Ok(_) => panic!("empty certificate unexpectedly accepted"), Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration), } let missing_key = super::WebTransportServerIdentity::from_pkcs8_der(vec![1], Vec::new()); match missing_key { Ok(_) => panic!("empty private key unexpectedly accepted"), Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration), } } #[tokio::test(flavor = "current_thread")] async fn cancelled_receive_preserves_partial_frame_state() { let (server_session, client_session) = establish_private_sessions().await; let (mut raw_sender, _raw_receiver) = match client_session.inner.open_bi().await { Ok(value) => value, Err(error) => panic!("raw client stream open failed: {error}"), }; let (server_sender, server_receiver) = match server_session.inner.accept_bi().await { Ok(value) => value, Err(error) => panic!("raw server stream accept failed: {error}"), }; let server_connection = super::WebTransportConnection::new(server_session.inner, server_sender, server_receiver, server_session.transport); let (_server_sender, mut server_receiver) = game_realtime_transport_lib::RealtimeConnection::split(server_connection); if let Err(error) = raw_sender.write_all(&[0x00, 0x00]).await { panic!("partial frame header write failed: {error}"); } tokio::time::sleep(std::time::Duration::from_millis(20)).await; let first_receive = tokio::time::timeout(std::time::Duration::from_millis(100), game_realtime_transport_lib::RealtimeReceiver::receive(&mut server_receiver)).await; assert!(first_receive.is_err()); assert_eq!(server_receiver.header_read, 2); if let Err(error) = raw_sender.write_all(&[0x00, 0x03, 0x10, 0x20, 0x30]).await { panic!("remaining frame write failed: {error}"); } let second_receive = tokio::time::timeout(TEST_TIMEOUT, game_realtime_transport_lib::RealtimeReceiver::receive(&mut server_receiver)).await; match second_receive { Ok(Ok(game_realtime_transport_lib::TransportReceive::Message(message))) => assert_eq!(message.as_bytes(), &[0x10, 0x20, 0x30]), Ok(Ok(value)) => panic!("resumed receive returned unexpected result: {value:?}"), Ok(Err(error)) => panic!("resumed receive failed: {error}"), Err(_) => panic!("resumed receive timed out"), } } #[tokio::test(flavor = "current_thread")] async fn truncated_frame_payload_is_a_protocol_failure() { let (server_session, client_session) = establish_private_sessions().await; let (mut raw_sender, _raw_receiver) = match client_session.inner.open_bi().await { Ok(value) => value, Err(error) => panic!("raw client stream open failed: {error}"), }; let (server_sender, server_receiver) = match server_session.inner.accept_bi().await { Ok(value) => value, Err(error) => panic!("raw server stream accept failed: {error}"), }; let server_connection = super::WebTransportConnection::new(server_session.inner, server_sender, server_receiver, server_session.transport); let (_server_sender, mut server_receiver) = game_realtime_transport_lib::RealtimeConnection::split(server_connection); if let Err(error) = raw_sender.write_all(&[0x00, 0x00, 0x00, 0x03, 0x10]).await { panic!("truncated frame payload write failed: {error}"); } if let Err(error) = raw_sender.finish() { panic!("raw client stream finish failed: {error}"); } let receive = tokio::time::timeout(TEST_TIMEOUT, game_realtime_transport_lib::RealtimeReceiver::receive(&mut server_receiver)).await; match receive { Ok(Ok(value)) => panic!("truncated frame payload produced a successful receive: {value:?}"), Ok(Err(error)) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::Protocol), Err(_) => panic!("truncated frame payload was not rejected within the test timeout"), } } #[tokio::test(flavor = "current_thread")] async fn truncated_frame_header_is_a_protocol_failure() { let (server_session, client_session) = establish_private_sessions().await; let (mut raw_sender, _raw_receiver) = match client_session.inner.open_bi().await { Ok(value) => value, Err(error) => panic!("raw client stream open failed: {error}"), }; let (server_sender, server_receiver) = match server_session.inner.accept_bi().await { Ok(value) => value, Err(error) => panic!("raw server stream accept failed: {error}"), }; let server_connection = super::WebTransportConnection::new(server_session.inner, server_sender, server_receiver, server_session.transport); let (_server_sender, mut server_receiver) = game_realtime_transport_lib::RealtimeConnection::split(server_connection); if let Err(error) = raw_sender.write_all(&[0x00, 0x00]).await { panic!("partial frame header write failed: {error}"); } if let Err(error) = raw_sender.finish() { panic!("raw client stream finish failed: {error}"); } let receive = tokio::time::timeout(TEST_TIMEOUT, game_realtime_transport_lib::RealtimeReceiver::receive(&mut server_receiver)).await; match receive { Ok(Ok(value)) => panic!("truncated frame header produced a successful receive: {value:?}"), Ok(Err(error)) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::Protocol), Err(_) => panic!("truncated frame header was not rejected within the test timeout"), } } async fn establish_private_sessions() -> (super::WebTransportSession, super::WebTransportSession) { let identity = match super::WebTransportServerIdentity::generate_loopback() { Ok(value) => value, Err(error) => panic!("loopback identity generation failed: {error}"), }; let certificate_hash = identity.certificate_hash().clone(); let server_config = super::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity); let mut listener = match super::WebTransportListener::bind(server_config) { Ok(value) => value, Err(error) => panic!("WebTransport listener bind failed: {error}"), }; let endpoint = format!("https://{}/private-test", listener.local_addr()); let client_config = match super::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) { Ok(value) => value, Err(error) => panic!("WebTransport client configuration failed: {error}"), }; let pair = tokio::time::timeout(TEST_TIMEOUT, async { return tokio::join!(listener.accept(), super::connect(&client_config)); }) .await; return match pair { Ok((Ok(server), Ok(client))) => (server, client), Ok((Err(error), _)) => panic!("WebTransport server establishment failed: {error}"), Ok((_, Err(error))) => panic!("WebTransport client establishment failed: {error}"), Err(_) => panic!("WebTransport private loopback establishment timed out"), }; }