Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
2348 lines
84 KiB
Rust
2348 lines
84 KiB
Rust
#![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::{
|
|
CompleteAgentMovementPacket, DisableSimulatorPacket, LogoutReplyPacket,
|
|
LogoutReplyPacketInventoryDataBlock, LogoutRequestPacket, Packet, PacketAckPacket,
|
|
PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket,
|
|
SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket,
|
|
};
|
|
use libremetaverse::{
|
|
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<F: std::future::Future>(future: F) -> F::Output {
|
|
use std::task::{Context, Poll, Wake, Waker};
|
|
|
|
struct ThreadWake(thread::Thread);
|
|
|
|
impl Wake for ThreadWake {
|
|
fn wake(self: Arc<Self>) {
|
|
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<Mutex<()>> = 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<PacketEventDictionary> = 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, Error> {
|
|
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,
|
|
ExpectLogout,
|
|
LogoutReply(Vec<UUID>),
|
|
Stop,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
enum ServerReport {
|
|
Ack(u32),
|
|
Circuit(u32),
|
|
HandshakeReply(u32),
|
|
Economy,
|
|
Logout(UUID, UUID),
|
|
Movement(u32),
|
|
Ping(u8),
|
|
}
|
|
|
|
struct FakeServer {
|
|
endpoint: SocketAddr,
|
|
commands: Sender<ServerCommand>,
|
|
reports: Receiver<ServerReport>,
|
|
handle: Option<JoinHandle<()>>,
|
|
}
|
|
|
|
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<PacketType> {
|
|
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<u8> {
|
|
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<ServerReport>,
|
|
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".to_vec();
|
|
let mut bytes = handshake.to_bytes_with_method().unwrap();
|
|
bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED);
|
|
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::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<PacketType>) {
|
|
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 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);
|
|
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::<UUID>::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::<HttpRequest>::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::<HttpRequest>::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::<HttpResponse>().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());
|
|
}
|