#![allow(clippy::await_holding_lock)] // The process-wide guard serializes socket integration tests. #![allow(clippy::float_cmp)] // Wire fixtures use exactly representable values. #![allow(clippy::items_after_statements)] // Local constants stay next to their concurrency scenario. #![allow(clippy::too_many_lines)] // End-to-end fixtures keep each protocol exchange contiguous. use libremetaverse::http::DownloadManager; use libremetaverse::interfaces::IMessage; use libremetaverse::messages::linden::{ EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock, }; use libremetaverse::packets::{ AgentThrottlePacket, CompleteAgentMovementPacket, DisableSimulatorPacket, LogoutReplyPacket, LogoutReplyPacketInventoryDataBlock, LogoutRequestPacket, Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket, SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket, }; use libremetaverse::{ AgentThrottle, CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient, InventoryItem, LoginState, LoginStatus, NetworkManager, NetworkManagerDisconnectType, NetworkManagerLoginResponseCallback, PacketEventDictionary, Simulator, }; use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; use libremetaverse_types::compat::{ CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri, }; use libremetaverse_types::{AssetType, Error, UUID, Utils}; use std::collections::{BTreeMap, HashMap}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::{Arc, Barrier, Mutex, MutexGuard, OnceLock, Weak}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; fn block_on(future: F) -> F::Output { use std::task::{Context, Poll, Wake, Waker}; struct ThreadWake(thread::Thread); impl Wake for ThreadWake { fn wake(self: Arc) { self.0.unpark(); } } let waker = Waker::from(Arc::new(ThreadWake(thread::current()))); let mut context = Context::from_waker(&waker); let mut future = std::pin::pin!(future); loop { match future.as_mut().poll(&mut context) { Poll::Ready(output) => return output, Poll::Pending => thread::park(), } } } fn loopback(port: u16) -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port) } fn simulator(client: &GridClient) -> Simulator { Simulator::new( client.clone(), loopback(13000), 0x1000, Some(256), Some(256), ) .expect("simulator") } fn base_packet(packet_type: PacketType) -> Packet { Packet::build_packet_with_packet_type(packet_type).expect("base packet") } fn wait_until(mut condition: impl FnMut() -> bool) { let deadline = Instant::now() + Duration::from_secs(2); while !condition() { assert!(Instant::now() < deadline, "condition timed out"); thread::sleep(Duration::from_millis(2)); } } fn network_test_guard() -> MutexGuard<'static, ()> { static NETWORK_TEST_LOCK: OnceLock> = OnceLock::new(); NETWORK_TEST_LOCK .get_or_init(|| Mutex::new(())) .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } #[test] fn packet_callbacks_preserve_filtering_order_async_policy_and_reentrancy() { let client = GridClient::new().expect("client"); let sim = simulator(&client); let events = Arc::new(PacketEventDictionary::new(client).expect("packet events")); let order = Arc::new(Mutex::new(Vec::new())); let default_order = Arc::clone(&order); events .register_event( PacketType::Default, Arc::new(move |_| default_order.lock().unwrap().push("default")), false, ) .unwrap(); let specific_order = Arc::clone(&order); events .register_event( PacketType::StartPingCheck, Arc::new(move |_| specific_order.lock().unwrap().push("specific")), false, ) .unwrap(); events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim.clone(), ) .unwrap(); assert_eq!(*order.lock().unwrap(), ["default", "specific"]); order.lock().unwrap().clear(); events .invoke_raise_event( PacketType::CompletePingCheck, base_packet(PacketType::CompletePingCheck), sim.clone(), ) .unwrap(); assert_eq!(*order.lock().unwrap(), ["default"]); let reentrant_count = Arc::new(AtomicUsize::new(0)); let weak_events: Weak = Arc::downgrade(&events); let count = Arc::clone(&reentrant_count); let reentrant: EventHandler<_> = Arc::new(move |_| { count.fetch_add(1, Ordering::Relaxed); if let Some(events) = weak_events.upgrade() { events .register_event(PacketType::UseCircuitCode, Arc::new(|_| {}), false) .unwrap(); } }); events .register_event(PacketType::UseCircuitCode, reentrant, false) .unwrap(); events .invoke_raise_event( PacketType::UseCircuitCode, base_packet(PacketType::UseCircuitCode), sim.clone(), ) .unwrap(); assert_eq!(reentrant_count.load(Ordering::Relaxed), 1); let async_events = PacketEventDictionary::new(sim.client.clone()).unwrap(); let removed_async: EventHandler<_> = Arc::new(|_| {}); async_events .register_event(PacketType::StartPingCheck, Arc::clone(&removed_async), true) .unwrap(); let caller_thread = thread::current().id(); let (thread_sender, thread_receiver) = channel(); async_events .register_event( PacketType::StartPingCheck, Arc::new(move |_| thread_sender.send(thread::current().id()).unwrap()), false, ) .unwrap(); async_events .unregister_event(PacketType::StartPingCheck, removed_async) .unwrap(); async_events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim, ) .unwrap(); assert_ne!( thread_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), caller_thread, "C# keeps a packet type asynchronous after its first async registration" ); } #[test] fn decoded_packet_callbacks_preserve_optional_raw_datagrams() { let client = GridClient::new().expect("client"); let sim = simulator(&client); let events = PacketEventDictionary::new(client).expect("packet events"); let observed = Arc::new(Mutex::new(None)); let captured = Arc::clone(&observed); events .register_event( PacketType::Default, Arc::new(move |event| *captured.lock().unwrap() = event.raw_data()), false, ) .unwrap(); let raw = vec![0x40, 0, 0, 0, 0, 0, 1, 7, 0, 0, 0, 0]; events .invoke_raise_event_with_raw_data( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim.clone(), raw.clone(), ) .unwrap(); assert_eq!(*observed.lock().unwrap(), Some(raw)); *observed.lock().unwrap() = Some(vec![1]); events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim, ) .unwrap(); assert_eq!(*observed.lock().unwrap(), None); } #[test] fn synchronous_specific_dispatch_suppresses_default_async_chain_like_reference() { let mixed_events = PacketEventDictionary::new(GridClient::new().unwrap()).unwrap(); let (default_sender, default_receiver) = channel(); mixed_events .register_event( PacketType::Default, Arc::new(move |_| default_sender.send(()).unwrap()), true, ) .unwrap(); let specific_calls = Arc::new(AtomicUsize::new(0)); let calls = Arc::clone(&specific_calls); mixed_events .register_event( PacketType::StartPingCheck, Arc::new(move |_| { calls.fetch_add(1, Ordering::Relaxed); }), false, ) .unwrap(); mixed_events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), simulator(&mixed_events.client), ) .unwrap(); assert_eq!(specific_calls.load(Ordering::Relaxed), 1); assert!( default_receiver .recv_timeout(Duration::from_millis(50)) .is_err(), "C# returns after a synchronous specific chain without queuing the default async chain" ); } #[test] fn abort_and_callback_unregistration_preserve_reference_noop_behavior() { let client = GridClient::new().unwrap(); let manager = client.network(); let progress = Arc::new(Mutex::new(Vec::new())); let observed = Arc::clone(&progress); let _guard = manager.subscribe_login_progress(Arc::new(move |args| { observed.lock().unwrap().push(args); })); let callback = NetworkManagerLoginResponseCallback::from_handler(|_, _, _, _, _| {}); manager .register_login_response_callback_with_login_response_callback(callback.clone()) .unwrap(); manager .unregister_login_response_callback(callback.clone()) .unwrap(); manager .unregister_login_response_callback(callback) .unwrap(); manager.abort_login().unwrap(); assert_eq!(manager.login_status_code(), LoginStatus::Failed); assert_eq!(manager.login_message(), "Abort Requested"); let progress = progress.lock().unwrap(); assert_eq!(progress.len(), 1); assert_eq!(progress[0].status(), LoginStatus::Failed); assert_eq!(progress[0].message(), "Abort Requested"); } struct TestCapsMessage; impl IMessage for TestCapsMessage { fn deserialize(&mut self, _map: OSDMap) -> Result<(), Error> { Ok(()) } fn serialize(&self) -> Result { OSDMap::new_with_constructor() } } #[test] fn caps_callbacks_are_ordered_and_subscription_guards_unregister() { let client = GridClient::new().expect("client"); let sim = simulator(&client); let events = CapsEventDictionary::new(client).expect("CAPS events"); let order = Arc::new(Mutex::new(Vec::new())); let default_order = Arc::clone(&order); let default = CapsEventQueueCallback::from_handler(move |_, _, _| { default_order.lock().unwrap().push("default"); }); let specific_order = Arc::clone(&order); let specific = CapsEventQueueCallback::from_handler(move |_, _, _| { specific_order.lock().unwrap().push("specific"); }); let default_guard = events.subscribe(String::new(), default); let specific_guard = events.subscribe("EnableSimulator".into(), specific); events.invoke_raise_event("EnableSimulator", &TestCapsMessage, sim.clone()); assert_eq!(*order.lock().unwrap(), ["default", "specific"]); drop(specific_guard); order.lock().unwrap().clear(); events.invoke_raise_event("EnableSimulator", &TestCapsMessage, sim); assert_eq!(*order.lock().unwrap(), ["default"]); drop(default_guard); } #[test] fn concurrent_registration_and_removal_is_deterministic() { const CALLBACKS: usize = 16; let client = GridClient::new().expect("client"); let sim = simulator(&client); let events = Arc::new(PacketEventDictionary::new(client).unwrap()); let barrier = Arc::new(Barrier::new(CALLBACKS + 1)); let calls = Arc::new(AtomicUsize::new(0)); let (guard_sender, guard_receiver) = channel(); thread::scope(|scope| { for _ in 0..CALLBACKS { let events = Arc::clone(&events); let barrier = Arc::clone(&barrier); let calls = Arc::clone(&calls); let guard_sender = guard_sender.clone(); scope.spawn(move || { let guard = events.subscribe( PacketType::StartPingCheck, Arc::new(move |_| { calls.fetch_add(1, Ordering::Relaxed); }), false, ); guard_sender.send(guard).unwrap(); barrier.wait(); }); } barrier.wait(); }); drop(guard_sender); let guards: Vec<_> = guard_receiver.into_iter().collect(); events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim.clone(), ) .unwrap(); assert_eq!(calls.load(Ordering::Relaxed), CALLBACKS); drop(guards); calls.store(0, Ordering::Relaxed); events .invoke_raise_event( PacketType::StartPingCheck, base_packet(PacketType::StartPingCheck), sim, ) .unwrap(); assert_eq!(calls.load(Ordering::Relaxed), 0); } #[test] fn sim_connecting_can_cancel_without_leaking_collection_state() { let client = GridClient::new().expect("client"); let manager = NetworkManager::new(client).expect("manager"); let guard = manager.subscribe_sim_connecting(Arc::new(|mut args| args.set_cancel(true))); let connected = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( loopback(9), 0x2000, true, Some(Uri("http://caps.invalid/seed".into())), 512, 256, ) .unwrap(); assert!(connected.is_none()); assert!(manager.simulators.is_empty()); assert!( manager.connected(), "C# marks the manager connected before raising the cancellable event" ); assert!(manager.current_sim().is_none()); drop(guard); } enum ServerCommand { Ping, Stats, Disable, ExpectEconomy, Handoff, ExpectThrottle, ExpectLogout, LogoutReply(Vec), Stop, } #[derive(Debug, Eq, PartialEq)] enum ServerReport { Ack(u32), Circuit(u32), HandshakeReply(u32), Economy, Logout(UUID, UUID), Movement(u32), Ping(u8), Throttle(u32, Vec), } struct FakeServer { endpoint: SocketAddr, commands: Sender, reports: Receiver, handle: Option>, } impl Drop for FakeServer { fn drop(&mut self) { let _ = self.commands.send(ServerCommand::Stop); if let Some(handle) = self.handle.take() { let _ = handle.join(); } } } fn packet_type(bytes: &[u8]) -> Option { let mut packet_end = i32::try_from(bytes.len()).ok()?.checked_sub(1)?; Packet::build_packet_with_bytes_int32_bytes(bytes.to_vec(), &mut packet_end, vec![0; 8192]) .ok() .map(|packet| packet.type_) } fn ack(sequence: u32) -> Vec { let mut packet = PacketAckPacket::new_with_constructor().unwrap(); packet.packets = vec![PacketAckPacketPacketsBlock { id: sequence }]; let mut bytes = packet.to_bytes_with_method().unwrap(); bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); bytes } fn serve_ping_command( socket: &UdpSocket, client_endpoint: SocketAddr, report_sender: &Sender, buffer: &mut [u8], ) { let mut ping = StartPingCheckPacket::new_with_constructor().unwrap(); ping.ping_id.ping_id = 42; ping.ping_id.oldest_unacked = 1; let mut bytes = ping.to_bytes_with_method().unwrap(); bytes[0] &= !Helpers::MSG_ZEROCODED; bytes[0] |= Helpers::MSG_RELIABLE; bytes[1..5].copy_from_slice(&99_u32.to_be_bytes()); socket.send_to(&bytes, client_endpoint).unwrap(); loop { let (length, _) = socket.recv_from(buffer).expect("ping response"); match packet_type(&buffer[..length]) { Some(PacketType::PacketAck) => { let mut position = 0; let response = PacketAckPacket::new_with_bytes_int32(buffer[..length].to_vec(), &mut position) .unwrap(); assert_eq!(response.packets.len(), 1); report_sender .send(ServerReport::Ack(response.packets[0].id)) .unwrap(); } Some(PacketType::CompletePingCheck) => { let mut position = 0; let response = libremetaverse::packets::CompletePingCheckPacket::new_with_bytes_int32( buffer[..length].to_vec(), &mut position, ) .unwrap(); report_sender .send(ServerReport::Ping(response.ping_id.ping_id)) .unwrap(); break; } _ => {} } } } fn spawn_fake_server() -> FakeServer { let socket = UdpSocket::bind(loopback(0)).expect("fake UDP server"); socket .set_read_timeout(Some(Duration::from_secs(2))) .unwrap(); let endpoint = socket.local_addr().unwrap(); let (command_sender, command_receiver) = channel(); let (report_sender, report_receiver) = channel(); let handle = thread::spawn(move || { let mut buffer = [0_u8; 4096]; let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); assert_eq!( packet_type(&buffer[..length]), Some(PacketType::UseCircuitCode) ); let mut position = 0; let use_circuit = UseCircuitCodePacket::new_with_bytes_int32(buffer[..length].to_vec(), &mut position) .expect("decode UseCircuitCode"); report_sender .send(ServerReport::Circuit(use_circuit.circuit_code.code)) .unwrap(); let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); socket.send_to(&ack(sequence), client_endpoint).unwrap(); let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap(); handshake.region_info.sim_name = b"Fake Region\0".to_vec(); handshake.region_info.water_height = 21.5; handshake.region_info2.region_id = UUID::new_with_u_int64(99).unwrap(); let raw = handshake.to_bytes_with_method().unwrap(); let mut bytes = vec![0_u8; raw.len().saturating_mul(2).saturating_add(2)]; let length = Helpers::zero_encode( Some(&raw), i32::try_from(raw.len()).unwrap(), Some(&mut bytes), ) .unwrap(); bytes.truncate(usize::try_from(length).unwrap()); socket.send_to(&bytes, client_endpoint).unwrap(); loop { let (length, _) = socket.recv_from(&mut buffer).expect("handshake reply"); let received_type = packet_type(&buffer[..length]); if received_type == Some(PacketType::RegionHandshakeReply) { let mut position = 0; let mut packet_end = i32::try_from(length).unwrap() - 1; let mut zero_buffer = vec![0_u8; 8192]; let mut response = RegionHandshakeReplyPacket::new_with_constructor().unwrap(); response .from_bytes_with_bytes_int32_int32_bytes( buffer[..length].to_vec(), &mut position, &mut packet_end, Some(&mut zero_buffer), ) .unwrap(); report_sender .send(ServerReport::HandshakeReply(response.region_info.flags)) .unwrap(); break; } } while let Ok(command) = command_receiver.recv() { match command { ServerCommand::Ping => { serve_ping_command(&socket, client_endpoint, &report_sender, &mut buffer); } ServerCommand::Stats => { let mut stats = SimStatsPacket::new_with_constructor().unwrap(); stats.stat = vec![ SimStatsPacketStatBlock { stat_id: 0, stat_value: 0.75, }, SimStatsPacketStatBlock { stat_id: 1, stat_value: 44.5, }, SimStatsPacketStatBlock { stat_id: 11, stat_value: 321.0, }, ]; let mut bytes = stats.to_bytes_with_method().unwrap(); bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); socket.send_to(&bytes, client_endpoint).unwrap(); } ServerCommand::Disable => { let disable = DisableSimulatorPacket::new_with_constructor().unwrap(); let mut bytes = disable.to_bytes_with_method().unwrap(); bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); socket.send_to(&bytes, client_endpoint).unwrap(); } ServerCommand::ExpectEconomy => loop { let (length, _) = socket.recv_from(&mut buffer).expect("EconomyDataRequest"); if packet_type(&buffer[..length]) == Some(PacketType::EconomyDataRequest) { report_sender.send(ServerReport::Economy).unwrap(); break; } }, ServerCommand::Handoff => { loop { let (length, _) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); if packet_type(&buffer[..length]) != Some(PacketType::UseCircuitCode) { continue; } let mut position = 0; let use_circuit = UseCircuitCodePacket::new_with_bytes_int32( buffer[..length].to_vec(), &mut position, ) .unwrap(); report_sender .send(ServerReport::Circuit(use_circuit.circuit_code.code)) .unwrap(); let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); socket.send_to(&ack(sequence), client_endpoint).unwrap(); break; } loop { let (length, _) = socket .recv_from(&mut buffer) .expect("CompleteAgentMovement"); if packet_type(&buffer[..length]) != Some(PacketType::CompleteAgentMovement) { continue; } let mut position = 0; let movement = CompleteAgentMovementPacket::new_with_bytes_int32( buffer[..length].to_vec(), &mut position, ) .unwrap(); report_sender .send(ServerReport::Movement(movement.agent_data.circuit_code)) .unwrap(); break; } } ServerCommand::ExpectThrottle => loop { let (length, _) = socket.recv_from(&mut buffer).expect("AgentThrottle"); if packet_type(&buffer[..length]) != Some(PacketType::AgentThrottle) { continue; } let mut bytes = buffer[..length].to_vec(); if bytes[0] & Helpers::MSG_ZEROCODED != 0 { let mut decoded = vec![0; 8192]; let decoded_length = Helpers::zero_decode( Some(&bytes), i32::try_from(bytes.len()).unwrap(), Some(&mut decoded), ) .unwrap(); decoded.truncate(usize::try_from(decoded_length).unwrap()); decoded[0] &= !Helpers::MSG_ZEROCODED; bytes = decoded; } let mut position = 0; let packet = AgentThrottlePacket::new_with_bytes_int32(bytes, &mut position).unwrap(); report_sender .send(ServerReport::Throttle( packet.agent_data.circuit_code, packet.throttle.throttles, )) .unwrap(); break; }, ServerCommand::ExpectLogout => loop { let (length, _) = socket.recv_from(&mut buffer).expect("LogoutRequest"); if packet_type(&buffer[..length]) != Some(PacketType::LogoutRequest) { continue; } let mut position = 0; let request = LogoutRequestPacket::new_with_bytes_int32( buffer[..length].to_vec(), &mut position, ) .unwrap(); report_sender .send(ServerReport::Logout( request.agent_data.agent_id, request.agent_data.session_id, )) .unwrap(); break; }, ServerCommand::LogoutReply(inventory_items) => loop { let (length, _) = socket.recv_from(&mut buffer).expect("LogoutRequest"); if packet_type(&buffer[..length]) != Some(PacketType::LogoutRequest) { continue; } let mut position = 0; let request = LogoutRequestPacket::new_with_bytes_int32( buffer[..length].to_vec(), &mut position, ) .unwrap(); report_sender .send(ServerReport::Logout( request.agent_data.agent_id, request.agent_data.session_id, )) .unwrap(); let mut reply = LogoutReplyPacket::new_with_constructor().unwrap(); reply.agent_data.agent_id = request.agent_data.agent_id; reply.agent_data.session_id = request.agent_data.session_id; reply.inventory_data = inventory_items .into_iter() .map(|item_id| LogoutReplyPacketInventoryDataBlock { item_id }) .collect(); let mut bytes = reply.to_bytes_with_method().unwrap(); bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); socket.send_to(&bytes, client_endpoint).unwrap(); break; }, ServerCommand::Stop => break, } } }); FakeServer { endpoint, commands: command_sender, reports: report_receiver, handle: Some(handle), } } fn spawn_ack_only_server() -> (SocketAddr, JoinHandle<()>) { let socket = UdpSocket::bind(loopback(0)).expect("ACK-only UDP server"); socket .set_read_timeout(Some(Duration::from_secs(2))) .unwrap(); let endpoint = socket.local_addr().unwrap(); let handle = thread::spawn(move || { let mut buffer = [0_u8; 4096]; let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); assert_eq!( packet_type(&buffer[..length]), Some(PacketType::UseCircuitCode) ); let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); socket.send_to(&ack(sequence), client_endpoint).unwrap(); }); (endpoint, handle) } fn assert_ping_exchange(server: &FakeServer, packet_receiver: &Receiver) { server.commands.send(ServerCommand::Ping).unwrap(); assert_eq!( packet_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), PacketType::StartPingCheck ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Ack(99) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Ping(42) ); } fn assert_reuse_preserves_connected_override( manager: &mut NetworkManager, server: &FakeServer, simulator: &Simulator, ) { manager.set_connected(false); let reused = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, simulator.handle, false, None, simulator.size_x, simulator.size_y, ) .unwrap() .unwrap(); assert_eq!(reused, simulator.clone()); assert!( !manager.connected(), "C# does not alter manager Connected when reusing a connected simulator" ); manager.set_connected(true); } #[test] fn grid_client_throttle_sender_writes_agent_throttle_to_the_simulator() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client.clone()).expect("manager"); manager.set_circuit_code(0x4455_6677); let simulator = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 0x1234, true, None, 256, 256, ) .unwrap() .expect("simulator"); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Circuit(0x4455_6677) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); let throttle = AgentThrottle::new_with_grid_client(client).unwrap(); server.commands.send(ServerCommand::ExpectThrottle).unwrap(); throttle.set_with_simulator(Some(simulator)).unwrap(); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Throttle(0x4455_6677, throttle.to_bytes().unwrap()) ); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[test] fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(0x1122_3344); let (changed_sender, changed_receiver) = channel(); let (connected_sender, connected_receiver) = channel(); let (packet_sender, packet_receiver) = channel(); let (sim_disconnected_sender, sim_disconnected_receiver) = channel(); let (disconnected_sender, disconnected_receiver) = channel(); let guards = [ manager.subscribe_sim_changed(Arc::new(move |args| { changed_sender .send(args.previous_simulator().is_none()) .unwrap(); })), manager.subscribe_sim_connected(Arc::new(move |args| { connected_sender.send(args.simulator().handle).unwrap(); })), manager.subscribe_sim_disconnected(Arc::new(move |args| { sim_disconnected_sender .send((args.reason(), args.was_connected())) .unwrap(); })), manager.subscribe_disconnected(Arc::new(move |args| { disconnected_sender.send(args.reason()).unwrap(); })), ]; manager .register_callback_with_packet_type_event_handler_boolean( PacketType::StartPingCheck, Arc::new(move |args| packet_sender.send(args.packet().type_).unwrap()), false, ) .unwrap(); let seed = Uri("http://caps.invalid/seed".into()); let sim = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 0x1234_0000, true, Some(seed.clone()), 512, 256, ) .unwrap() .expect("connected simulator"); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Circuit(0x1122_3344) ); assert!( changed_receiver .recv_timeout(Duration::from_secs(1)) .unwrap() ); assert_eq!( connected_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), sim.handle ); assert_eq!(manager.current_sim(), Some(sim.clone())); assert_eq!(sim.seed_capability(), Some(seed)); assert_eq!(sim.size_x, 512); assert_eq!(sim.size_y, 256); assert_eq!(sim.name, "Fake Region"); assert_eq!(sim.region_id, UUID::new_with_u_int64(99).unwrap()); assert_eq!(sim.water_height, 21.5); wait_until(|| sim.handshake_complete()); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); assert_reuse_preserves_connected_override(&mut manager, &server, &sim); assert_ping_exchange(&server, &packet_receiver); server.commands.send(ServerCommand::Stats).unwrap(); wait_until(|| sim.stats.objects() == 321); assert_eq!(sim.stats.dilation(), 0.75); assert_eq!(sim.stats.fps(), 44); assert!(sim.stats.get_recv_packets().unwrap() >= 3); assert!(sim.stats.get_recv_bytes().unwrap() > 0); assert!(sim.stats.get_sent_packets().unwrap() >= 3); assert!(sim.stats.get_sent_bytes().unwrap() > 0); assert_eq!(sim.stats.get_dropped_packets().unwrap(), 0); server.commands.send(ServerCommand::Disable).unwrap(); assert_eq!( sim_disconnected_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), (NetworkManagerDisconnectType::NetworkTimeout, true) ); assert_eq!( disconnected_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), NetworkManagerDisconnectType::SimShutdown ); assert!(manager.simulators.is_empty()); assert!(manager.current_sim().is_none()); assert!(!manager.connected()); drop(guards); } #[test] fn concurrent_disconnect_emits_each_transition_once() { let _network_guard = network_test_guard(); const CALLERS: usize = 12; let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(7); let sim_disconnects = Arc::new(AtomicUsize::new(0)); let global_disconnects = Arc::new(AtomicUsize::new(0)); let sim_count = Arc::clone(&sim_disconnects); let sim_guard = manager.subscribe_sim_disconnected(Arc::new(move |args| { assert!(args.was_connected()); sim_count.fetch_add(1, Ordering::Relaxed); })); let global_count = Arc::clone(&global_disconnects); let global_guard = manager.subscribe_disconnected(Arc::new(move |_| { global_count.fetch_add(1, Ordering::Relaxed); })); let sim = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 9, true, None, 256, 256, ) .unwrap() .unwrap(); let manager = Arc::new(manager); let barrier = Arc::new(Barrier::new(CALLERS)); thread::scope(|scope| { for _ in 0..CALLERS { let manager = Arc::clone(&manager); let simulator = sim.clone(); let barrier = Arc::clone(&barrier); scope.spawn(move || { barrier.wait(); manager.disconnect_sim(simulator, false).unwrap(); }); } }); assert_eq!(sim_disconnects.load(Ordering::Relaxed), 1); assert_eq!(global_disconnects.load(Ordering::Relaxed), 1); assert!(manager.simulators.is_empty()); assert!(!manager.connected()); drop(sim_guard); drop(global_guard); } #[test] fn sim_disconnected_callback_can_reenter_disconnect_without_deadlock() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(8); let simulator = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 10, true, None, 256, 256, ) .unwrap() .unwrap(); let manager = Arc::new(manager); let weak_manager = Arc::downgrade(&manager); let callback_completed = Arc::new(AtomicBool::new(false)); let completed = Arc::clone(&callback_completed); let guard = manager.subscribe_sim_disconnected(Arc::new(move |args| { let manager = weak_manager.upgrade().expect("manager during callback"); manager .disconnect_sim(args.simulator(), false) .expect("reentrant disconnect"); completed.store(true, Ordering::Release); })); manager.disconnect_sim(simulator, false).unwrap(); assert!(callback_completed.load(Ordering::Acquire)); assert!(manager.simulators.is_empty()); assert!(!manager.connected()); drop(guard); } #[test] fn keepalive_requires_two_silent_intervals_before_network_timeout() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(11); let (disconnected_sender, disconnected_receiver) = channel(); let guard = manager.subscribe_disconnected(Arc::new(move |args| { disconnected_sender.send(args.reason()).unwrap(); })); let sim = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 12, true, None, 256, 256, ) .unwrap() .unwrap(); wait_until(|| sim.handshake_complete()); manager.poll_keepalive(); assert!(manager.connected()); assert!( disconnected_receiver .recv_timeout(Duration::from_millis(50)) .is_err() ); manager.poll_keepalive(); assert_eq!( disconnected_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), NetworkManagerDisconnectType::NetworkTimeout ); assert!(!manager.connected()); assert!(manager.current_sim().is_none()); drop(guard); } #[test] fn enable_simulator_caps_event_adds_each_new_region_once() { let _network_guard = network_test_guard(); let first_server = spawn_fake_server(); let second_server = spawn_fake_server(); let mut client = GridClient::new().expect("client"); client.settings().agent_settings_mut().multiple_sims = true; let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(17); let current = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( first_server.endpoint, 18, true, None, 256, 256, ) .unwrap() .unwrap(); let mut message = EnableSimulatorMessage::new().unwrap(); let mut info = EnableSimulatorMessageSimulatorInfoBlock::new().unwrap(); info.ip = second_server.endpoint.ip(); info.port = i32::from(second_server.endpoint.port()); info.region_handle = 19; info.region_size_x = 1024; info.region_size_y = 512; message.simulators = vec![info]; manager.dispatch_caps_event("EnableSimulator", &message, current.clone()); manager.dispatch_caps_event("EnableSimulator", &message, current); assert_eq!(manager.simulators.len(), 2); let added = manager .find_simulator_with_ip_end_point(second_server.endpoint) .unwrap() .expect("enabled simulator"); assert_eq!(added.handle, 19); assert_eq!(added.size_x, 1024); assert_eq!(added.size_y, 512); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(17) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[test] fn connected_region_handoff_preserves_old_sim_and_sends_complete_agent_movement() { let _network_guard = network_test_guard(); let first_server = spawn_fake_server(); let second_server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(0x7788); let first = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( first_server.endpoint, 100, true, None, 256, 256, ) .unwrap() .unwrap(); let second = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( second_server.endpoint, 200, false, None, 512, 256, ) .unwrap() .unwrap(); for server in [&first_server, &second_server] { assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Circuit(0x7788) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); } let (changed_sender, changed_receiver) = channel(); let _changed = manager.subscribe_sim_changed(Arc::new(move |args| { changed_sender.send(args.previous_simulator()).unwrap(); })); second_server.commands.send(ServerCommand::Handoff).unwrap(); let selected = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( second_server.endpoint, 200, true, None, 512, 256, ) .unwrap() .unwrap(); assert_eq!(selected, second); assert_eq!(manager.current_sim(), Some(second.clone())); assert_eq!( changed_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), Some(first.clone()) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(0x7788) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Movement(0x7788) ); assert!(first.connected()); assert!(second.connected()); assert_eq!(manager.simulators.len(), 2); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[test] fn manager_reconnects_with_fresh_workers_after_complete_shutdown() { let _network_guard = network_test_guard(); let first_server = spawn_fake_server(); let second_server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(0x7878); manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( first_server.endpoint, 250, true, None, 256, 256, ) .unwrap() .unwrap(); assert_eq!( first_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(0x7878) ); assert_eq!( first_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); assert!(!manager.connected()); assert!(manager.current_sim().is_none()); assert!(manager.simulators.is_empty()); manager.set_circuit_code(0x7979); let reconnected = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( second_server.endpoint, 251, true, None, 512, 256, ) .unwrap() .unwrap(); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(0x7979) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); assert!(manager.connected()); assert_eq!(manager.current_sim(), Some(reconnected)); assert_eq!(manager.simulators.len(), 1); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[test] fn logout_reply_orders_events_returns_inventory_and_tears_down_once() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(0x8899); manager.set_login_seed_capability(Some(Uri("https://caps.invalid/secret".into()))); let login_response = manager .login_response_data .as_mut() .expect("login response storage"); login_response.set_session_id( UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap(), ); login_response.set_secure_session_id( UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap(), ); login_response.set_mfa_hash("secret-mfa".into()); login_response.set_seed_capability("https://caps.invalid/secret".into()); login_response.set_next_url("https://login.invalid/secret-token".into()); manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 300, true, None, 256, 256, ) .unwrap() .unwrap(); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Circuit(0x8899) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); let order = Arc::new(Mutex::new(Vec::new())); let logged_order = Arc::clone(&order); let (inventory_sender, inventory_receiver) = channel(); let _logged_out = manager.subscribe_logged_out(Arc::new(move |args| { logged_order.lock().unwrap().push("logged_out"); inventory_sender.send(args.inventory_items).unwrap(); })); let sim_order = Arc::clone(&order); let _sim_disconnected = manager.subscribe_sim_disconnected(Arc::new(move |_| { sim_order.lock().unwrap().push("sim_disconnected"); })); let disconnected_order = Arc::clone(&order); let disconnected_count = Arc::new(AtomicUsize::new(0)); let count = Arc::clone(&disconnected_count); let _disconnected = manager.subscribe_disconnected(Arc::new(move |_| { count.fetch_add(1, Ordering::Relaxed); disconnected_order.lock().unwrap().push("disconnected"); })); let inventory = vec![ UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()).unwrap(), UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()).unwrap(), ]; server .commands .send(ServerCommand::LogoutReply(inventory.clone())) .unwrap(); manager.logout_with_method().unwrap(); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Logout(UUID::zero(), UUID::zero()) ); assert_eq!( inventory_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), inventory ); assert_eq!( *order.lock().unwrap(), ["logged_out", "sim_disconnected", "disconnected"] ); assert!(!manager.connected()); assert!(manager.current_sim().is_none()); assert!(manager.simulators.is_empty()); assert_eq!(manager.circuit_code(), 0); assert!(manager.login_seed_capability().is_none()); let login_response = manager .login_response_data .as_ref() .expect("login response storage"); assert_eq!(login_response.session_id(), UUID::zero()); assert_eq!(login_response.secure_session_id(), UUID::zero()); assert!(login_response.mfa_hash().is_empty()); assert!(login_response.seed_capability().is_empty()); assert!(login_response.next_url().is_empty()); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); assert_eq!(disconnected_count.load(Ordering::Relaxed), 1); } #[test] fn async_logout_timeout_and_cancellation_have_distinct_reference_results() { let _network_guard = network_test_guard(); let timeout_server = spawn_fake_server(); let mut timeout_client = GridClient::new().expect("client"); timeout_client.settings().timing().logout_timeout = 25; let mut timeout_manager = NetworkManager::new(timeout_client).expect("manager"); timeout_manager.set_circuit_code(0x9911); timeout_manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( timeout_server.endpoint, 400, true, None, 256, 256, ) .unwrap() .unwrap(); let _ = timeout_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(); let _ = timeout_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(); let (reason_sender, reason_receiver) = channel(); let _disconnected = timeout_manager.subscribe_disconnected(Arc::new(move |args| { reason_sender.send(args.reason()).unwrap(); })); block_on(timeout_manager.logout_with_cancellation_token(None)).unwrap(); assert_eq!( reason_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), NetworkManagerDisconnectType::NetworkTimeout ); assert!(!timeout_manager.connected()); let cancel_server = spawn_fake_server(); let mut cancel_client = GridClient::new().expect("client"); cancel_client.settings().timing().logout_timeout = 1_000; let mut cancel_manager = NetworkManager::new(cancel_client).expect("manager"); cancel_manager.set_circuit_code(0x9922); cancel_manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( cancel_server.endpoint, 500, true, None, 256, 256, ) .unwrap() .unwrap(); let _ = cancel_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(); let _ = cancel_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(); let cancellation = CancellationTokenSource::new(); cancellation.cancel(); let result = block_on(cancel_manager.logout_with_cancellation_token(Some(cancellation.token()))); assert_eq!(result, Err(libremetaverse::Error::Cancelled)); assert!(cancel_manager.connected()); cancel_manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[test] fn begin_logout_times_out_nonblocking_and_emits_logged_out_after_teardown() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let mut client = GridClient::new().expect("client"); client.settings().timing().logout_timeout = 250; let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(0x9933); manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 600, true, None, 256, 256, ) .unwrap() .unwrap(); let _ = server.reports.recv_timeout(Duration::from_secs(1)).unwrap(); let _ = server.reports.recv_timeout(Duration::from_secs(1)).unwrap(); let order = Arc::new(Mutex::new(Vec::new())); let disconnected_order = Arc::clone(&order); let _disconnected = manager.subscribe_disconnected(Arc::new(move |_| { disconnected_order.lock().unwrap().push("disconnected"); })); let logged_order = Arc::clone(&order); let (logged_sender, logged_receiver) = channel(); let _logged = manager.subscribe_logged_out(Arc::new(move |args| { logged_order.lock().unwrap().push("logged_out"); logged_sender.send(args.inventory_items).unwrap(); })); let started = Instant::now(); manager.begin_logout().unwrap(); assert!(started.elapsed() < Duration::from_millis(100)); assert_eq!( logged_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), Vec::::new() ); assert_eq!(*order.lock().unwrap(), ["disconnected", "logged_out"]); assert!(!manager.connected()); assert!(manager.simulators.is_empty()); } #[test] fn explicit_shutdown_cancels_a_pending_begin_logout_without_retaining_client() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let mut client = GridClient::new().expect("client"); client.settings().timing().logout_timeout = 5_000; let mut manager = NetworkManager::new(client.clone()).expect("manager"); manager.set_circuit_code(0x9944); manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( server.endpoint, 700, true, None, 256, 256, ) .unwrap() .unwrap(); let _ = server.reports.recv_timeout(Duration::from_secs(1)).unwrap(); let _ = server.reports.recv_timeout(Duration::from_secs(1)).unwrap(); server.commands.send(ServerCommand::ExpectLogout).unwrap(); manager.begin_logout().unwrap(); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Logout(UUID::zero(), UUID::zero()) ); assert_eq!(manager.pending_logout_tasks(), 1); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); wait_until(|| manager.pending_logout_tasks() == 0); assert_eq!( manager.last_logout_error(), Some(libremetaverse::Error::Cancelled) ); assert!(!manager.connected()); assert!(manager.simulators.is_empty()); } #[test] fn async_connect_and_shutdown_do_not_require_an_ambient_tokio_runtime() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let client = GridClient::new().expect("client"); let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(23); let simulator = block_on( manager.connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32_20e51cc0( server.endpoint, 24, true, None, 256, 256, ), ) .unwrap() .expect("async connection"); assert!(simulator.connected()); assert_eq!(manager.current_sim(), Some(simulator)); block_on(manager.shutdown_with_disconnect_type_string_7641243c( NetworkManagerDisconnectType::ClientInitiated, "ClientInitiated".into(), )) .unwrap(); assert!(!manager.connected()); } #[test] fn handshake_timeout_preserves_reference_connect_result_and_shutdown_tears_down_current_sim() { let _network_guard = network_test_guard(); let (endpoint, server) = spawn_ack_only_server(); let mut client = GridClient::new().expect("client"); client.settings().timing().login_timeout = 25; let mut manager = NetworkManager::new(client).expect("manager"); manager.set_circuit_code(25); let sim_disconnects = Arc::new(AtomicUsize::new(0)); let disconnect_count = Arc::clone(&sim_disconnects); let guard = manager.subscribe_sim_disconnected(Arc::new(move |_| { disconnect_count.fetch_add(1, Ordering::Relaxed); })); let simulator = manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( endpoint, 26, true, None, 256, 256, ) .unwrap() .expect("C# returns the UDP-connected simulator after handshake timeout"); server.join().unwrap(); assert!(!simulator.handshake_complete()); assert!(manager.simulators.is_empty()); assert_eq!(manager.current_sim(), Some(simulator.clone())); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); assert!(!simulator.connected()); assert_eq!(sim_disconnects.load(Ordering::Relaxed), 1); drop(guard); } fn login_http_response(body: OSD) -> HttpResponse { let body = OSDParser::serialize_llsd_xml_bytes(body).expect("serialize LLSD"); HttpResponse { status_code: 200, headers: BTreeMap::from([("Content-Length".into(), body.len().to_string())]), content_type: Some("application/llsd+xml".into()), body, } } fn successful_login_response(endpoint: SocketAddr, seed: &str) -> OSD { OSD::Map(HashMap::from([ ("login".into(), OSD::Boolean(true)), ("message".into(), OSD::String("Welcome".into())), ( "agent_id".into(), OSD::UUID( UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap(), ), ), ( "session_id".into(), OSD::UUID( UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap(), ), ), ( "secure_session_id".into(), OSD::UUID( UUID::new_with_string("cccccccc-cccc-cccc-cccc-cccccccccccc".into()).unwrap(), ), ), ("circuit_code".into(), OSD::Integer(0x1234)), ("region_x".into(), OSD::Integer(1000)), ("region_y".into(), OSD::Integer(2000)), ("region_size_x".into(), OSD::Integer(256)), ("region_size_y".into(), OSD::Integer(256)), ("sim_ip".into(), OSD::String(endpoint.ip().to_string())), ("sim_port".into(), OSD::Integer(i32::from(endpoint.port()))), ("seed_capability".into(), OSD::String(seed.into())), ( "account_level_benefits".into(), OSD::Map(HashMap::from([( "group_membership_limit".into(), OSD::Integer(70), )])), ), ])) } #[tokio::test(flavor = "current_thread")] async fn login_hashes_credentials_parses_response_and_hands_off_session_to_udp_and_caps() { let _network_guard = network_test_guard(); let server = spawn_fake_server(); let requests = Arc::new(Mutex::new(Vec::::new())); let recorded = Arc::clone(&requests); let endpoint = server.endpoint; let handler = HttpMessageHandler::new(move |request, _| { let recorded = Arc::clone(&recorded); async move { let response = if request.uri.0.contains("/login") { login_http_response(successful_login_response( endpoint, "http://caps.test/seed?token=secret", )) } else { login_http_response(OSD::Map(HashMap::new())) }; recorded.lock().unwrap().push(request); response } }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 2_000; client.settings().connection_mut().mfa_enabled = true; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let callback_calls = Arc::new(AtomicUsize::new(0)); let observed_calls = Arc::clone(&callback_calls); let callback = NetworkManagerLoginResponseCallback::from_handler( move |success, redirect, message, reason, response| { assert!(success); assert!(!redirect); assert_eq!(message, "Welcome"); assert!(reason.is_empty()); assert!(response.is_some_and(|value| value.success())); observed_calls.fetch_add(1, Ordering::Relaxed); }, ); manager .register_login_response_callback_with_login_response_callback_string_array( callback, Some(vec!["custom-login-option".into()]), ) .unwrap(); let progress = Arc::new(Mutex::new(Vec::new())); let observed = Arc::clone(&progress); let _progress_guard = manager.subscribe_login_progress(Arc::new(move |args| { observed.lock().unwrap().push(args.status()); })); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "http://login.test/login".into(); params.token = "123456".into(); params.mfa_hash = "saved-mfa-hash".into(); let response = manager .login_with_response_with_login_params_cancellation_token(params, None) .await .unwrap() .expect("successful response"); assert_eq!(response.login(), LoginState::True); assert_eq!(manager.login_status_code(), LoginStatus::Success); assert_eq!(manager.login_message(), "Welcome"); assert_eq!(manager.max_agent_groups(), 70); assert_eq!(callback_calls.load(Ordering::Relaxed), 1); assert!(manager.connected()); assert!(manager.current_sim().is_some()); assert_eq!( manager.login_seed_capability(), Some(Uri("http://caps.test/seed?token=secret".into())) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Circuit(0x1234) ); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); server.commands.send(ServerCommand::ExpectEconomy).unwrap(); assert_eq!( server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), ServerReport::Economy ); let login_request = requests .lock() .unwrap() .iter() .find(|request| request.uri.0.contains("/login")) .cloned() .expect("login request"); let request = OSDParser::deserialize_with_bytes(login_request.body).unwrap(); let OSD::Map(request) = request else { panic!("login request must be an LLSD map"); }; assert_eq!( request["passwd"].as_string().unwrap(), Utils::md5_with_string("plain-secret".into()).unwrap() ); assert_eq!(request["token"].as_string().unwrap(), "123456"); assert_eq!(request["mfa_hash"].as_string().unwrap(), "saved-mfa-hash"); assert_eq!(request["start"].as_string().unwrap(), "last"); assert_eq!(request["channel"].as_string().unwrap(), "MetaCrate"); assert_eq!(request["version"].as_string().unwrap(), "1.0"); assert_eq!( request["platform"].as_string().unwrap(), NetworkManager::get_platform().unwrap() ); assert!(!request["platform_version"].as_string().unwrap().is_empty()); assert_eq!( request["mac"].as_string().unwrap(), request["id0"].as_string().unwrap() ); let OSD::Array(options) = &request["options"] else { panic!("login options must be an LLSD array"); }; assert!( options .iter() .any(|option| option.as_string().as_deref() == Ok("custom-login-option")) ); assert_eq!( *progress.lock().unwrap(), [ LoginStatus::ConnectingToLogin, LoginStatus::ReadingResponse, LoginStatus::ConnectingToSim, LoginStatus::Success, ] ); manager .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) .unwrap(); } #[tokio::test(flavor = "current_thread")] async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task_leaks() { let _network_guard = network_test_guard(); let first_server = spawn_fake_server(); let second_server = spawn_fake_server(); let mut enable = EnableSimulatorMessage::new().unwrap(); let mut simulator_info = EnableSimulatorMessageSimulatorInfoBlock::new().unwrap(); simulator_info.ip = second_server.endpoint.ip(); simulator_info.port = i32::from(second_server.endpoint.port()); simulator_info.region_handle = 0x2000; simulator_info.region_size_x = 512; simulator_info.region_size_y = 256; enable.simulators = vec![simulator_info]; let enable_body = enable.serialize().unwrap().snapshot(); let release_seed = Arc::new(AtomicBool::new(false)); let release_queue = Arc::new(AtomicBool::new(false)); let queue_calls = Arc::new(AtomicUsize::new(0)); let queue_cancelled = Arc::new(AtomicBool::new(false)); let queue_done = Arc::new(AtomicBool::new(false)); let request_uris = Arc::new(Mutex::new(Vec::new())); let handler = HttpMessageHandler::new({ let release_seed = Arc::clone(&release_seed); let release_queue = Arc::clone(&release_queue); let queue_calls = Arc::clone(&queue_calls); let queue_cancelled = Arc::clone(&queue_cancelled); let queue_done = Arc::clone(&queue_done); let request_uris = Arc::clone(&request_uris); let first_endpoint = first_server.endpoint; move |request, cancellation| { let release_seed = Arc::clone(&release_seed); let release_queue = Arc::clone(&release_queue); let queue_calls = Arc::clone(&queue_calls); let queue_cancelled = Arc::clone(&queue_cancelled); let queue_done = Arc::clone(&queue_done); let request_uris = Arc::clone(&request_uris); let enable_body = enable_body.clone(); async move { let uri = request.uri.0.clone(); request_uris.lock().unwrap().push(uri.clone()); if uri.contains("/login") { return login_http_response(successful_login_response( first_endpoint, "https://fake.grid/seed?token=seed-secret", )); } if uri.contains("/seed") { while !release_seed.load(Ordering::Acquire) { tokio::time::sleep(Duration::from_millis(1)).await; } return login_http_response(OSD::Map(HashMap::from([ ( "EventQueueGet".into(), OSD::Uri(Uri("https://fake.grid/queue?token=queue-secret".into())), ), ( "GetTexture".into(), OSD::Uri(Uri("https://fake.grid/asset?token=asset-secret".into())), ), ]))); } if uri.contains("/queue") { let done = matches!( OSDParser::deserialize_llsd_xml_with_bytes(request.body.clone()), Ok(OSD::Map(ref map)) if map .get("done") .and_then(|value| value.as_boolean().ok()) == Some(true) ); if done { queue_done.store(true, Ordering::Release); return HttpResponse { status_code: 499, headers: BTreeMap::new(), content_type: Some(HttpCapsClient::LLSD_XML.into()), body: Vec::new(), }; } let call = queue_calls.fetch_add(1, Ordering::AcqRel); if call == 0 { while !release_queue.load(Ordering::Acquire) { tokio::time::sleep(Duration::from_millis(1)).await; } return login_http_response(OSD::Map(HashMap::from([ ("id".into(), OSD::Integer(1)), ( "events".into(), OSD::Array(vec![OSD::Map(HashMap::from([ ("message".into(), OSD::String("EnableSimulator".into())), ("body".into(), OSD::Map(enable_body)), ]))]), ), ]))); } let observed_cancellation = Arc::clone(&queue_cancelled); let _cancellation_guard = cancellation.register_callback(Arc::new(move || { observed_cancellation.store(true, Ordering::Release); })); cancellation.cancelled().await; return HttpResponse { status_code: 499, headers: BTreeMap::new(), content_type: Some(HttpCapsClient::LLSD_XML.into()), body: Vec::new(), }; } if uri.contains("/asset") { return HttpResponse { status_code: 200, headers: BTreeMap::new(), content_type: Some("application/octet-stream".into()), body: b"fake-grid-asset".to_vec(), }; } HttpResponse { status_code: 404, headers: BTreeMap::new(), content_type: None, body: Vec::new(), } } } }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 2_000; client.settings().agent_settings_mut().multiple_sims = true; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let manager_event_order = Arc::new(Mutex::new(Vec::new())); let (queue_sender, queue_receiver) = channel(); let queue_order = Arc::clone(&manager_event_order); let _queue_guard = manager.subscribe_event_queue_running(Arc::new(move |args| { queue_order.lock().unwrap().push("event_queue_running"); queue_sender.send(args.simulator().handle).unwrap(); })); let (logout_sender, logout_receiver) = channel(); let logout_order = Arc::clone(&manager_event_order); let _logout_guard = manager.subscribe_logged_out(Arc::new(move |args| { logout_order.lock().unwrap().push("logged_out"); logout_sender.send(args.inventory_items).unwrap(); })); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "https://fake.grid/login".into(); assert!( manager .login_with_login_params_cancellation_token(params, None) .await .unwrap() ); assert_eq!( first_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(0x1234) ); assert_eq!( first_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); let first = manager.current_sim().expect("initial simulator"); let caps = first.clone().caps.expect("seed capability client"); release_seed.store(true, Ordering::Release); wait_until(|| { caps.capability_uri("GetTexture".into()) .ok() .flatten() .is_some() }); release_queue.store(true, Ordering::Release); assert_eq!( queue_receiver.recv_timeout(Duration::from_secs(2)).unwrap(), first.handle ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(2)) .unwrap(), ServerReport::Circuit(0x1234) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) ); let second = manager .find_simulator_with_ip_end_point(second_server.endpoint) .unwrap() .expect("event-queue simulator"); let downloads = DownloadManager::new(client.clone()).unwrap(); let (_, asset) = downloads .queue_download_with_uri_string_i_progress_cancellation_token_int32( caps.capability_uri("GetTexture".into()).unwrap().unwrap(), None, None, None, Some(0), ) .await .unwrap(); assert_eq!(asset, b"fake-grid-asset"); assert_eq!(downloads.active_download_count(), 0); // Cross-manager construction is deliberately performed only after the // authenticated simulator and seed capabilities are available. Every // manager below shares the same client runtime and must preserve its // local store when accessed again through GridClient. { let agent = client.self_(); assert_eq!( agent.agent_id(), UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap() ); assert_eq!( agent.session_id(), UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap() ); } let inventory_id = UUID::new_with_string("44444444-4444-4444-4444-444444444444".into()).unwrap(); let inventory = client.inventory(); let mut store = inventory.store().expect("authenticated inventory store"); let mut item = InventoryItem::new_with_uuid(inventory_id).unwrap(); item.base.set_name("fake-grid inventory marker".into()); store.set_item(inventory_id, item.base); assert!( client .inventory() .store() .unwrap() .contains_with_uuid(inventory_id) .unwrap() ); assert!(!client.ais_client().is_available()); assert_eq!(client.appearance().last_update_received_cof_version(), -1); assert!( client .assets() .create_asset_wrapper(AssetType::Texture) .is_ok() ); let _avatars = client.avatars(); assert!(client.animesh().get_player(UUID::zero()).unwrap().is_none()); client.objects().request_object(first.clone(), 1).unwrap(); assert!(client.parcels().current_parcel().is_none()); assert!(client.grid().regions_read_only().is_empty()); assert!(client.friends().friend_list().is_empty()); assert_eq!(client.groups().current_group_count(), -1); assert!(client.marketplace().listings_by_id().is_empty()); let _terrain = client.terrain(); let _environment = client.environment(); let _sound = client.sound(); let _estate = client.estate(); let cancelled_search = CancellationTokenSource::new(); cancelled_search.cancel(); assert!(matches!( client .directory() .search_people("Ada Lovelace".into(), Some(cancelled_search.token()),), Err(Error::Cancelled) )); let (marketplace_error_sender, marketplace_error_receiver) = channel(); let marketplace_order = Arc::clone(&manager_event_order); let _marketplace_error_guard = client.marketplace().subscribe_error(Arc::new(move |_| { marketplace_order.lock().unwrap().push("marketplace_error"); marketplace_error_sender.send(()).unwrap(); })); client.marketplace().fetch_listings(None).await.unwrap(); marketplace_error_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(); second_server.commands.send(ServerCommand::Handoff).unwrap(); manager .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( second_server.endpoint, second.handle, true, None, second.size_x, second.size_y, ) .unwrap(); assert_eq!(manager.current_sim(), Some(second.clone())); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Circuit(0x1234) ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Movement(0x1234) ); let diagnostics = format!("{manager:?} {caps:?} {downloads:?} {first:?} {second:?}"); for secret in [ "seed-secret", "queue-secret", "asset-secret", "plain-secret", ] { assert!(!diagnostics.contains(secret)); } assert!(first.stats.get_recv_packets().unwrap() > 0); assert!(first.stats.get_sent_packets().unwrap() > 0); assert!(second.stats.get_recv_packets().unwrap() > 0); assert!(second.stats.get_sent_packets().unwrap() > 0); wait_until(|| queue_calls.load(Ordering::Acquire) >= 2); let returned_inventory = vec![UUID::new_with_string("33333333-3333-3333-3333-333333333333".into()).unwrap()]; second_server .commands .send(ServerCommand::LogoutReply(returned_inventory.clone())) .unwrap(); manager.logout_with_method().unwrap(); assert_eq!( logout_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(), returned_inventory ); assert_eq!( second_server .reports .recv_timeout(Duration::from_secs(1)) .unwrap(), ServerReport::Logout( UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap(), UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap(), ) ); wait_until(|| queue_cancelled.load(Ordering::Acquire)); assert!(!queue_done.load(Ordering::Acquire)); assert!(!caps.is_event_queue_running()); assert!(!manager.connected()); assert!(manager.simulators.is_empty()); assert_eq!(manager.pending_logout_tasks(), 0); assert!(!first.connected()); assert!(!second.connected()); downloads.dispose().unwrap(); assert!(downloads.is_disposed()); assert!(!downloads.dispatcher_running()); assert_eq!(downloads.active_download_count(), 0); assert_eq!( *manager_event_order.lock().unwrap(), ["event_queue_running", "marketplace_error", "logged_out"] ); let client_cancellation = client.cancellation_token(); client.dispose_with_method().unwrap(); assert!(client_cancellation.is_cancellation_requested()); let uris = request_uris.lock().unwrap(); assert!(uris.iter().any(|uri| uri.contains("/login"))); assert!(uris.iter().any(|uri| uri.contains("/seed"))); assert!(uris.iter().any(|uri| uri.contains("/queue"))); assert!(uris.iter().any(|uri| uri.contains("/asset"))); } #[tokio::test(flavor = "current_thread")] async fn redirect_reuses_hashed_credentials_and_failure_is_typed_without_partial_session() { let requests = Arc::new(Mutex::new(Vec::::new())); let recorded = Arc::clone(&requests); let calls = Arc::new(AtomicUsize::new(0)); let handler_calls = Arc::clone(&calls); let handler = HttpMessageHandler::new(move |request, _| { let recorded = Arc::clone(&recorded); let index = handler_calls.fetch_add(1, Ordering::Relaxed); async move { recorded.lock().unwrap().push(request); if index == 0 { login_http_response(OSD::Map(HashMap::from([ ("login".into(), OSD::String("indeterminate".into())), ( "next_url".into(), OSD::String("http://login.test/redirected".into()), ), ("next_duration".into(), OSD::Integer(0)), ("message".into(), OSD::String("Redirecting".into())), ]))) } else { login_http_response(OSD::Map(HashMap::from([ ("login".into(), OSD::Boolean(false)), ("reason".into(), OSD::String("key".into())), ("message".into(), OSD::String("Denied".into())), ]))) } } }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 1_000; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let callback_calls = Arc::new(AtomicUsize::new(0)); let observed_calls = Arc::clone(&callback_calls); manager .register_login_response_callback_with_login_response_callback( NetworkManagerLoginResponseCallback::from_handler(move |_, _, _, _, _| { observed_calls.fetch_add(1, Ordering::Relaxed); }), ) .unwrap(); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "http://login.test/login".into(); assert!( !manager .login_with_login_params_cancellation_token(params, None) .await .unwrap() ); assert_eq!(manager.login_status_code(), LoginStatus::Failed); assert_eq!(manager.login_error_key(), "key"); assert_eq!(manager.login_message(), "Denied"); assert!(!manager.connected()); assert!(manager.current_sim().is_none()); assert_eq!( callback_calls.load(Ordering::Relaxed), 0, "the C# callback only runs for a successful login" ); let passwords: Vec<_> = requests .lock() .unwrap() .iter() .map(|request| { let OSD::Map(map) = OSDParser::deserialize_with_bytes(request.body.clone()).unwrap() else { panic!("request must be map"); }; map["passwd"].as_string().unwrap() }) .collect(); assert_eq!(passwords.len(), 2); assert_eq!(passwords[0], passwords[1]); assert_ne!(passwords[0], "plain-secret"); } #[tokio::test(flavor = "current_thread")] async fn successful_authentication_with_invalid_simulator_clears_partial_session() { let handler = HttpMessageHandler::new(|_, _| async move { let mut response = successful_login_response( "127.0.0.1:1".parse().unwrap(), "http://caps.test/seed?token=must-not-survive", ); let OSD::Map(ref mut fields) = response else { unreachable!(); }; fields.insert("sim_port".into(), OSD::Integer(0)); login_http_response(response) }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 1_000; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "http://login.test/login".into(); assert!( !manager .login_with_login_params_cancellation_token(params, None) .await .unwrap() ); assert_eq!(manager.login_status_code(), LoginStatus::Failed); assert!(manager.current_sim().is_none()); assert!(manager.simulators.is_empty()); assert!(!manager.connected()); assert!(manager.login_seed_capability().is_none()); let response = manager.login_response_data.as_ref().unwrap(); assert_eq!(response.session_id(), UUID::zero()); assert_eq!(response.secure_session_id(), UUID::zero()); assert!(response.seed_capability().is_empty()); assert!(response.account_level_benefits().is_none()); } #[tokio::test(flavor = "current_thread", start_paused = true)] async fn login_timeout_is_typed_and_leaves_no_partial_session() { let handler = HttpMessageHandler::new(|_, _| async move { std::future::pending::().await }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 25; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "http://login.test/login".into(); assert!( !manager .login_with_login_params_cancellation_token(params, None) .await .unwrap() ); assert_eq!(manager.login_status_code(), LoginStatus::Failed); assert_eq!(manager.login_error_key(), "timeout"); assert_eq!(manager.login_message(), "Login timed out"); assert!(manager.current_sim().is_none()); assert!(manager.login_seed_capability().is_none()); } #[tokio::test(flavor = "current_thread")] async fn canceled_login_finishes_failed_without_retaining_simulator_work() { let handler = HttpMessageHandler::new(|_, cancellation| async move { cancellation.cancelled().await; login_http_response(OSD::Map(HashMap::new())) }); let mut client = GridClient::new().unwrap(); client.settings().timing().login_timeout = 1_000; client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); let manager = client.network(); let mut params = manager .default_login_params( "Ada".into(), "Lovelace".into(), "plain-secret".into(), "MetaCrate".into(), "1.0".into(), ) .unwrap(); params.uri = "http://login.test/login".into(); let cancellation = CancellationTokenSource::new(); cancellation.cancel(); assert!( !manager .login_with_login_params_cancellation_token(params, Some(cancellation.token())) .await .unwrap() ); assert_eq!(manager.login_status_code(), LoginStatus::Failed); assert_eq!(manager.login_error_key(), "canceled"); assert!(!manager.connected()); assert!(manager.simulators.is_empty()); }